SOLID Principles in iOS Development
π© Ok, you probably already hear this term, but in practice, what's mean? When uses? How uses? Keep reading for FINALLY LEARN
First of all, SOLID is a s a mnemonic acronym for five design principles intended to make software design more understandable, flexible, and maintainable
In all tutorial, i will show SOLID Principles in action and codes that don't follow SOLID
So, let's go π
S - Single Responsibility Principle
A classe should have one reason to change ( to exists )
Tips to apply
Ask yourself for every entity ( classes / functions ) : this entity, does more than one thing?
You shouldn't have use the word and when have to talk the responsibility to entity
π’ NOT Single Responsibility Principle apply
class Handler_NOT_SOLID {
func handle() {
let data = requestDataToAPI()
guard let dataReceive = data else { return }
let array = parse(data: dataReceive)
saveToDB(array: array)
}
private func requestDataToAPI() -> Data?{
// send API request and wait the response
return nil
}
private func parse(data:Data)->[String]?{
// parse the data and create an array
return nil
}
private func saveToDB(array:[String]?){
// save array in a database
}
}
Can you see?
Handler_NOT_SOLID class have several responsibilities
- Send request to API
- Create an array with data receive
- Save array in a database
π Single Responsibility Principle apply
class Handler_SOLID {
let apiHandler: APIHandler
let parseHandler: ParseHandler
let dbHandler: DBHandler
init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
self.apiHandler = apiHandler
self.parseHandler = parseHandler
self.dbHandler = dbHandler
}
}
class APIHandler {
func requestDataToAPI() -> Data?{
// send API request and wait the response
return nil
}
}
class ParseHandler {
func parse(data:Data) -> [String]?{
// parse the data and create an array
return nil
}
}
class DBHandler {
func saveToDB(array:[String]?){
// save array in a database
}
}
Now, each entity have just one responsibility
O - Open/Closed Principle
A software entity should be open to extension, but closed for modification.
Tips to apply
- If you want to modify a class every time a new behavior is added, something isn't quite right
- If/else/switch statements don't be used to modify a behavior
π’ NOT Open/Closed Principle apply
class Vehicles_NOT_SOLID {
func printData() {
let cars = [
Car_NOT_SOLID(name: "Batmobile", color: "Black"),
Car_NOT_SOLID(name: "SuperCar", color: "Gold"),
Car_NOT_SOLID(name: "FamilyCar", color: "Grey")
]
cars.forEach { car in
print(car.printDetails())
}
let buses = [
Bus_NOT_SOLID(type: "School bus"),
Bus_NOT_SOLID(type: "Minibus"),
Bus_NOT_SOLID(type: "Minicoach")
]
buses.forEach { bus in
print(bus.printDetails())
}
}
}
class Car_NOT_SOLID {
let name:String
let color:String
init(name: String, color: String) {
self.name = name
self.color = color
}
func printDetails() -> String {
return "name : \(name) color :\(color)"
}
}
class Bus_NOT_SOLID {
let type:String
init(type: String) {
self.type = type
}
func printDetails() -> String {
return "bus type : \(type)"
}
}
Can you see?
When printData receive other type of object, we have to add more rules to work
If you want to add the possibility to print also the details of a new class, we should change the implementation of printData every time we want to log a new class
π Open/Closed Principle apply
protocol Printable {
func printDetails() -> String
}
class Vehicles_SOLID {
func printData() {
let cars:[Printable] = [
Car_SOLID(name: "Batmobile", color: "Black"),
Car_SOLID(name: "SuperCar", color: "Gold"),
Car_SOLID(name: "FamilyCar", color: "Grey"),
Bus_SOLID(type: "School bus"),
Bus_SOLID(type: "Minibus"),
Bus_SOLID(type: "Minicoach")
]
cars.forEach { car in
print(car.printDetails())
}
}
}
class Car_SOLID:Printable {
let name: String
let color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
func printDetails() -> String {
return "name : \(name) color :\(color)"
}
}
class Bus_SOLID: Printable {
let type: String
init(type: String) {
self.type = type
}
func printDetails() -> String {
return "bus type : \(type)"
}
}
We don't need change behavior of printData, just make a layer between printData and the class
L - Liskov Substitution Principle
Objects should be replaceable with instances of their subtypes without altering the correctness of that program.
Tips to apply
- Instead of one monolithic interface, break an interface up based on what implementers should be doing
- Keeps consumers from having to much power
π’ NOT Liskov Substitution Principle apply
class Rectangle_NOT_SOLID {
var width: Double = 0
var height: Double = 0
var area: Double {
return width * height
}
}
class Square_NOT_SOLID: Rectangle_NOT_SOLID {
override var width: Double {
didSet {
height = width
}
}
}
// MARK: - Implementations
func printArea(of rectangle: Rectangle_NOT_SOLID) {
rectangle.width = 10
rectangle.height = 4
print(rectangle.area)
}
let rect = Rectangle_NOT_SOLID()
printArea(of: rect) // 40
let square = Square_NOT_SOLID()
printArea(of: square ) // 40
See that printArea(of rectangle:Rectangle_NOT_SOLID) return the same result with different types instead return specific value of each class
π Liskov Substitution Principle apply
protocol Polygon {
var area :Double { get }
}
class Rectangle_SOLID: Polygon {
let width:Double
let height:Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
var area: Double {
return width * height
}
}
class Square_SOLID: Polygon {
let side:Double
init(side: Double) {
self.side = side
}
var area: Double {
return pow(side, 2)
}
}
/// MARK: - Implementations
func printArea(of polygon:Polygon){
print(polygon.area)
}
let rect = Rectangle_SOLID(width: 10, height: 40)
printArea(of: rect) // 400.0
let square = Square_SOLID(side: 10)
printArea(of: square) // 100.0
I - Interface Segregation Principle
(M)any client-specific interfaces are better than one general-purpose interface
Tips to apply
- Instead of one monolithic interface, break an interface up based on what implementers should be doing
- Keeps consumers from having to much power
π’ NOT Interface Segregation Principle apply
//MARK:- Fat Interface (Protocol)
protocol GestureProtocol {
func didTap()
func didLongPress()
func didSwipe()
}
class RichButton_NOT_SOLID: GestureProtocol {
func didTap() {
print("tap button")
}
func didLongPress() {
print("long press")
}
func didSwipe() {
print("swipe")
}
}
class PoorButton_NOT_SOLID: GestureProtocol {
func didTap() {
print("tap")
}
func didLongPress() {}
func didSwipe() {}
}
See that PoorButton_NOT_SOLID class have methods that not are usable
π Interface Segregation Principle apply
protocol TapGesture {
func didTap()
}
protocol LongPressGesture {
func didLongPress()
}
protocol SwipeGesture {
func didSwipe()
}
class RichButton_SOLID: TapGesture, LongPressGesture, SwipeGesture{
func didTap() {
print("tap button")
}
func didLongPress() {
print("long press")
}
func didSwipe() {
print("swipe")
}
}
class PoorButton_SOLID: TapGesture {
func didTap() {
print("tap button")
}
}
Now we remove all unnecessary methods
D - Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Tips to apply
- Weβre used to this : High level -> (uses) Low level
- High level -> (expects) interface <- ( fulfills ) low level
π’ NOT Dependency Inversion Principle apply
class SaveData_NOT_SOLID {
let filesSystemManager = FilesSystemManager_NOT_SOLID()
func handle(data:String){
filesSystemManager.save(data: data)
}
}
class FilesSystemManager_NOT_SOLID {
func save(data:String){
// save data
}
}
With this, we have just a way to save Data. If we want use any DataBase?
π Dependency Inversion Principle apply
protocol Storage {
func save(data:Any)
}
class SaveData_SOLID {
let storage:Storage
init(storage: Storage) {
self.storage = storage
}
func handle(data: Any){
self.storage.save(data: data)
}
}
class FilesSystemManager_SOLID: Storage {
func save(data: Any) {}
}
class MySQLDataBase: Storage {
func save(data: Any) {}
}
Now, we can use any storage method
π Thanks for read!! I hope that you have understand all principles about SOLID and how apply in your day by day for build a better software. In doubt, please put your comment below and i will try to help.
Top comments (1)
Nice article!
However for the exemple on the Liskov Substitution Principle, you could still have square extending rectangle.