What is SOLID?

It is an acronym, introduced by Michael Feathers, that introduces five object oriented design principles intended to make software design more understandable, flexible and maintainable.

Single Responsibility Principle

Every module should have only one responsibility and reason to change.

In simpler words, your objects should do only one thing.

BAD

struct User {
    let name: String
    let age: Int
    let email: String
    let password: String
    
    func register() {}
}

The register function doesn't act on a User property but registers the User. This object stores data and has a behaviour.

GOOD

struct User {
    let name: String
    let age: Int
    let email: String
    let password: String
}

class UserSignUp {
    func register(user: User) {}
}

Open-Close Principle

Every module should be open for extension but closed for modification.

In other words, you should be able to add behaviour to your objects without modifying them.

BAD

struct Rectangle{
    let width: Double
    let height: Double
}

struct Circle{
    let radius: Double
}

class AreaCalculator {
    func Area(shapes:[Any]) -> Double {
        var area: Double?
        shapes.forEach { shape in
            if shape is Rectangle {
                guard
                    let rectangle = shape as? Rectangle
                    else { return }
                area = rectangle.width * rectangle.height
            } else {
                guard
                    let circle = shape as? Circle
                    else { return }
                area = circle.radius * circle.radius * .pi
            }
        }
        return area ?? 0
    }
}

Every time we add a different Shape we need to modify the function which is going to grow bigger and bigger.

GOOD

protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    func area() -> Double {
    return width * height
    }
}

struct Circle : Shape {
    let radius: Double
    func area() -> Double {
        return radius * radius * .pi
    }
}

Now adding different shapes does not imply modifying code, but extending it.

Liskov Substitution Principle

Modules should be replaceable with instances of their subtypes.

In other words, a subclass should override the parent class' methods in a way that does not break functionality from a client's point of view.

BAD

class Bird {
    func fly() {}
}

class Robin: Bird {
    override func fly() {
        // some code to fly
    }
}

class Ostrich: Bird {
    
    override func fly() {
        // it's a bird but can't fly
    }
}

GOOD

class Bird {}

class FlyingBird {
    func fly() {}
}

class Robin: FlyingBird {
    override func fly() {
        // some code to fly
    }
}

class Ostrich: Bird {}
You can now use both Ostrich and Robin as a Bird, only Robin can fly.

Interface Segregation Principle

Many client-specific interfaces are better than one general-purpose interface.

In other words, make your interfaces small. An object should not be forced to implement a method they don't use just because it is in the interface.

BAD

protocol Flying {
    func startEngine()
    func fly()
}

class Airplane: Flying {
    func startEngine() {
        // some code to start engine
    }

    func fly() {
        // some code to fly
    }
}

class Bird: Flying {
    func startEngine() {
        // doesn't have an engine!!
    }

    func fly() {
        // some code to fly
    }
}

GOOD

protocol Flying {
    func fly()
}

protocol EngineStarting {
    func startEngine()
}

class Airplane: Flying, EngineStarting {
    func fly() {
        // some code to fly
    }

    func startEngine() {
        // some code to start engine
    }
}

class Bird: Flying {
    func fly() {
        // some code to fly
    }
}

Dependency Inversion Principle

A module should depend upon abstractions, not on concrete implementations.

In other words, by using abstractions between your dependencies you achieve decoupling.

BAD

class Company {
    var developers: [Developer]
    var productOwners: [ProductOwner]

    init(developers: [Developer], 
         productOwners: [ProductOwner]) {
        self.developers = developers
        self.productOwners = productOwners
    }
    
    func work() {
        developers.forEach { $0.work() }
        productOwners.forEach { $0.work() }
    }
}

class Developer {
    func work() {
        // some code to work
    }
}

class ProductOwner {
    func work() {
        // some different code to work
    }
}
To add ProductOwner I need to add another variable with a different type.

GOOD

protocol Employable {
    func work()
}

class Company {
    var employees: [Employable]
    
    init(candidates: [Employable]) {
        employees = candidates
    }
    
    func work() {
        employees.forEach { $0.work() }
    }
}

class Developer: Employable {
    func work() {
        // some code to work
    }
}

class ProductOwner: Employable {
    func work() {
        // some different code to work
    }
}
To add ProductOwner no changes are necessary and I created and abstraction to decouple Company and Developer.

Conclusion

By applying these principles you are a step closer to achieving quality code. There are plenty of good and detailed articles out there, I tried to keep this one short and simple. Hope this was helpful.