SOLID explained with iOS examples
Tags: solidprinciples, ios, swift
Published: 2024-06-20 09:14:05

The SOLID principles are a set of guidelines for designing software that is easy to maintain and extend.

## 1. Single Responsibility Principle (SRP)

> A class should have only one reason to change, meaning it should only have one job or responsibility.

```swift
// WRONG: A class that handles both user authentication and user data storage
class UserManager {
    func authenticateUser(username: String, password: String) -> Bool {
        // Authentication logic
        return true
    }

    func saveUserDetails(user: User) {
        // Save user details logic
    }
}

// CORRECT: Separate classes for authentication and data storage
class Authenticator {
    func authenticateUser(username: String, password: String) -> Bool {
        // Authentication logic
        return true
    }
}

class UserStorage {
    func saveUserDetails(user: User) {
        // Save user details logic
    }
}
```

## 2. Open/Closed Principle (OCP)

> Software entities should be open for extension but closed for modification.

```swift
// WRONG: A class that needs to be modified to support new types of notifications
class NotificationManager {
    func sendNotification(type: String) {
        if type == "email" {
            // Send email notification
        } else if type == "sms" {
            // Send SMS notification
        }
    }
}

// CORRECT: Extendable notification types using protocol
protocol Notification {
    func send()
}

class EmailNotification: Notification {
    func send() {
        // Send email notification
    }
}

class SMSNotification: Notification {
    func send() {
        // Send SMS notification
    }
}

class NotificationManager {
    func sendNotification(notification: Notification) {
        notification.send()
    }
}
```

## 3. Liskov Substitution Principle (LSP)

> Subtypes must be substitutable for their base types without altering the correctness of the program.

```swift
// WRONG: A subclass that breaks the functionality of the superclass
class Bird {
    func fly() {
        print("Flying")
    }
}

class Ostrich: Bird {
    override func fly() {
        // Ostrich can't fly, so this method should not exist here
        fatalError("Ostriches can't fly!")
    }
}

// CORRECT: Using protocol to define a contract for flying birds
protocol Flyable {
    func fly()
}

class Sparrow: Flyable {
    func fly() {
        print("Flying")
    }
}

class Ostrich {
    // Ostrich does not conform to Flyable because it can't fly
}
```

## 4. Interface Segregation Principle (ISP)

> Clients should not be forced to depend on interfaces they do not use.

```swift
// WRONG: A single interface with too many responsibilities
protocol Worker {
    func work()
    func eat()
}

class Developer: Worker {
    func work() {
        // Coding
    }

    func eat() {
        // Eating lunch
    }
}

class Robot: Worker {
    func work() {
        // Coding
    }

    func eat() {
        // Robots don't eat, so this method is not applicable
    }
}

// CORRECT: Separate interfaces for different responsibilities
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class Developer: Workable, Eatable {
    func work() {
        // Coding
    }

    func eat() {
        // Eating lunch
    }
}

class Robot: Workable {
    func work() {
        // Coding
    }
}
```

## 5. Dependency Inversion Principle (DIP)

> High-level modules should not depend on low-level modules. Both should depend on abstractions.

```swift
// WRONG: High-level module depends on low-level module
class Database {
    func save() {
        // Save to database
    }
}

class UserRepository {
    private let database = Database()

    func saveUser() {
        database.save()
    }
}

// CORRECT: High-level module depends on an abstraction
protocol Storage {
    func save()
}

class Database: Storage {
    func save() {
        // Save to database
    }
}

class UserRepository {
    private let storage: Storage

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

    func saveUser() {
        storage.save()
    }
}
```