Usage of SOLID principles in iOS development iOS 27.02.2022

You can read about SOLID principles with examples in Java.

In software engineering, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

In short, you should keep in your mind

  • Each class should have only one responsibility.
  • Class should be opened for extension but closed for modification.
  • Child class should not break the parent class type definitions.
  • Implement only what you need.
  • Depend on abstractions, not on concretions.

S: Single Responsibility principle

Each class should have only one responsibility or should have just one reason to change.

If there is a need to change the class for more than one reason then that defies the single responsibility principle.

How many responsibilities?

class Employee {
    var name: String = ""
    var age: Int = 0

    func calculatePay() -> Pay  {...} // calculating
    func save() {...} // database access
    func describeEmployee() -> String {...} // reporting
}

The correct answer is three.

Here we have pay 1) calculation logic with 2) database logic and 3) reporting logic all mixed up within one class. If you have multiple responsibilities combined into one class, it might be difficult to change one part without breaking others.

To solve the issue we should split the class into three different classes, with each having only one responsibility: database access, calculating pay and reporting, all separated.

O: Open-Closed principle

A class should be open to extension, but closed for modification.

If you want to modify a class every time a new behavior is added then that defies the open-closed principle.

In short, you should be able to add almost any new feature to your class easily and will not have to re-structure your whole class as we all know that going through re-structuring could result in additional time in development and also testing.

Let’s use simple code example to illustrate the idea.

protocol DriveableProtocol {
    func drive() -> String
}

class Car: DriveableProtocol {
    let vendor: String

    init(vendor: String) {
        self.vendor = vendor
    }

    func drive() -> String {
        return "vendor: \(vendor), engine is on"
    }
}

class Bicycle: DriveableProtocol {
    let color: String

    init(color: String) {
        self.color = color
    }

    func drive() -> String {
        return "bicycle color: \(color), pedals are available "
    }
}

// implementation

class Transport {
    func report() {
        let items: [Driveable] = [
            Car(name: "Nissan"),
            Bicycle(color: "Black")
        ]

        items.map { 0.drive() }
    }
}

L: Liskov Substitution principle

We should be able to use subclasses instead of the parent classes which class they have extended, without the need to make any changes in our code. In simple words, the child class must be substitutable for the parent class. Since child classes extended from the parent classes, they inherit their behavior.

Instead of one monolithic protocol, break an protocol up based on what implementers should be doing.

protocol ShapeProtocol {
    var area: Float { get }
}

class Rectangle: ShapeProtocol {
    let width: Float
    let height: Float

    init(width: Float, height: Float) {
        self.width = width
        self.height = height
    }

    var area: Float {
        return width * height
    }

}

class Square: ShapeProtocol {
    let side: Float

    init(side: Float) {
        self.side = side
    }

    var area: Float {
        return pow(side, 2)
    }
} 

func report(of shape: ShapeProtocol){
    print(shape.area)
}

// implementation

let rect = Rectangle(width: 3, height: 4)
report(of: rect)

let square = Square(side: 5)
report(of: square)

I: Interface Segregation principle

This principle states that once an protocol becomes too fat, it needs to be split into smaller interfaces so that client of the protocol will only know about the methods that pertain to them.

Instead of one monolithic protocol, break an protocol up based on what implementers should be doing.

protocol AnimalProtocol {
    func eat()
    func sleep()
}

protocol FlyingAnimalProtocol {
    func fly()
}

class Cat: AnimalProtocol {
    func eat() {
        print("Cat is eating fish")
    }

    func sleep() {
        print("Cat is sleeping")
    }
}

// implementation

class Bird: AnimalProtocol, AnimalProtocol {
    func eat() {
        print("Bird is eating feed")
    }

    func sleep() {
        print("Bird is sleeping")
    }

    func fly() {
        print("Bird is flying")
    }
}

D: Dependency Inversion Principle

This principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions and abstractions should not depend upon details. Details should depend upon abstractions.

With this theory, high level modules like the view controller, should not depend directly on low level things, like a networking component. Instead, it should depend on abstractions or in Swift term, protocol. The point here is to reduce coupling.

protocol StorageProtocol {
    func save(data: Any)
}

class FilesSystem: StorageProtocol {
    func save(data: Any) {}
}

class DataBase: StorageProtocol {
    func save(data: Any) {}
}

// implementation

class SaveData {
    let storage: Storage

    init(storage: Storage) {
        self.storage = storage
    }

    func handle(data: Any){
        self.storage.save(data: data)
    }
}