DEV Community

MKilmer
MKilmer

Posted on

SOLID Principles in iOS Development

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

Alt Text

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Can you see?

Handler_NOT_SOLID class have several responsibilities

  1. Send request to API
  2. Create an array with data receive
  3. 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
    }
}

Enter fullscreen mode Exit fullscreen mode

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

  1. If you want to modify a class every time a new behavior is added, something isn't quite right
  2. 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)"
    }
}

Enter fullscreen mode Exit fullscreen mode

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)"
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Instead of one monolithic interface, break an interface up based on what implementers should be doing
  2. 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

I - Interface Segregation Principle

(M)any client-specific interfaces are better than one general-purpose interface

Tips to apply

  1. Instead of one monolithic interface, break an interface up based on what implementers should be doing
  2. 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() {}

}
Enter fullscreen mode Exit fullscreen mode

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")
      }
}

Enter fullscreen mode Exit fullscreen mode

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

  1. We’re used to this : High level -> (uses) Low level
  2. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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.

Github Project

Top comments (1)

Collapse
 
guillaume_lesouchu_d97a8 profile image
La pieuvre

Nice article!
However for the exemple on the Liskov Substitution Principle, you could still have square extending rectangle.