DEV Community

Marwan Ayman
Marwan Ayman

Posted on • Edited on

SOLID Principles in Swift: A Practical Guide

The SOLID principles are a set of guidelines for writing maintainable and scalable software. They were first introduced by Robert C. Martin in his book “Agile Software Development, Principles, Patterns, and Practices”. These principles are widely used in object-oriented programming languages, such as Swift.

Single Responsibility Principle (SRP)

The single responsibility principle states that a class should have one, and only one, reason to change. This means that a class should have a single, well-defined responsibility. In other words, a class should have only one reason to exist and all its methods should be related to that reason.

class User {
    var name: String
    var email: String
    init(name: String, email: String) {
        self.name = name
        self.email = email
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the User class has only one responsibility: to store and retrieve information about a user. It has no other responsibilities, such as sending an email or validating the user’s information. This makes it easy to understand and maintain.

Open-Closed Principle (OCP)

The open-closed principle states that a class should be open for extension, but closed for modification. This means that a class should be designed in such a way that new functionality can be added without modifying the existing code.

protocol Shape {
    func area() -> Double
}

class Rectangle: Shape {
    var width: Double
    var height: Double

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

    func area() -> Double {
        return width * height
    }
}

class Circle: Shape {
    var radius: Double

    init(radius: Double) {
        self.radius = radius
    }

    func area() -> Double {
        return Double.pi * radius * radius
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have a protocol “Shape” which has a function area(). The class “Rectangle” and “Circle” conforms to the Shape protocol and implements the area() function. Now, if we need to add a new shape, for example, a triangle, we can just create a new class “Triangle” which conforms to the Shape protocol and implements the area() function. We don’t need to modify the existing class, which makes it easy to add new functionality and maintain the code.

Liskov Substitution Principle (LSP)

The Liskov substitution principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. This means that a subclass should be a subtype of its superclass, and should be able to replace any instance of its superclass without affecting the correctness of the program.

Example:

class Car {
  func startEngine() {
    print("Engine started")
  }
}

class ElectricCar: Car {
  override func startEngine() {
    print("Electric motor started")
  }
}

let car: Car = ElectricCar()
car.startEngine()  // Output: "Electric motor started"
Enter fullscreen mode Exit fullscreen mode

In the above example, we have a Car class and a ElectricCar subclass that inherit from it. Since ElectricCar overrides the startEngine() method and the ElectricCar instance can be assigned to a variable of type Car, LSP is satisfied.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a class should not be forced to implement interfaces it doesn’t use. In other words, a class should only be required to implement the methods it needs.

protocol Printable {
  func printDocument()
  func scanDocument()
  func faxDocument()
}

class Printer: Printable {
  func printDocument() {
    // Code to print document
  }

  func scanDocument() {
    // Not implemented
  }

  func faxDocument() {
    // Not implemented
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we have a Printable protocol with three methods. The Printer class only needs to implement the printDocument() method, but it is forced to implement the other two methods as well. To adhere to ISP, we can split the Printable protocol into two separate protocols: Printable and Scanable.

protocol Printable {
  func printDocument()
}

protocol Scanable {
  func scanDocument()
}

class Printer: Printable {
  func printDocument() {
    // Code to print document
  }
}

class Scanner: Scanable {
  func scanDocument() {
    // Code to scan document
  }
}
Enter fullscreen mode Exit fullscreen mode

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions. In other words, a class should not depend on a specific implementation, but rather on an abstraction.

Let’s take a look at an example where DIP is not followed:

class Database {
  func save(data: String) {
    // Code to save data to database
  }
}

class User {
  var database = Database()

  func save() {
    database.save(data: "user data")
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the User class depends directly on the Database class. If we need to change the implementation of the database, we would have to change the User class as well. This creates a tight coupling between the two classes and makes the code less flexible and harder to maintain.

To adhere to DIP, we can create an abstraction in the form of a protocol and have both the User and Database classes depend on it.

protocol DataStorage {
  func save(data: String)
}

class Database: DataStorage {
  func save(data: String) {
    // Code to save data to database
  }
}

class User {
  var dataStorage: DataStorage

  init(dataStorage: DataStorage) {
    self.dataStorage = dataStorage
  }

  func save() {
    dataStorage.save(data: "user data")
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the User class depends on the DataStorage protocol, rather than a specific implementation. This allows us to change the implementation of the database without affecting the User class.

DIP allows us to create more flexible and maintainable code by decoupling classes and making them depend on abstractions rather than specific implementations. It is an important principle to keep in mind when designing and implementing software.

Another way to follow DIP is to use Dependency injection. Dependency injection is a design pattern that allows us to provide an object with its dependencies, rather than creating them directly within the class. This allows us to change the implementation of the dependencies without affecting the class using them.

class Database: DataStorage {
  func save(data: String) {
    // Code to save data to database
  }
}

class User {
  var dataStorage: DataStorage

  init(dataStorage: DataStorage = Database()) {
    self.dataStorage = dataStorage
  }

  func save() {
    dataStorage.save(data: "user data")
  }
}
Enter fullscreen mode Exit fullscreen mode

This way we can use a different implementation of the DataStorage protocol and use it in the User class

class CloudStorage: DataStorage {
  func save(data: String) {
    // Code to save data to cloud
  }
}

let user = User(dataStorage: CloudStorage())
Enter fullscreen mode Exit fullscreen mode

Conclusion

In conclusion, the SOLID principles are a set of guidelines that help us create more maintainable and flexible code. These principles, Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle and Dependency Inversion Principle, are essential to consider when designing and implementing software.

By following these principles, we can create classes that are easy to understand, test, and change. They also help us avoid common design problems such as tight coupling and fragile code.

In this article, we have seen examples of how to implement SOLID principles in swift. By following these principles, we can create code that is more maintainable, flexible and easy to understand. These principles should be considered as a guide to help make our code more robust and scalable.

In short, SOLID principles are a set of guidelines that help us create more maintainable and flexible code. By following these principles, we can create code that is more robust and scalable, which is essential for building high-quality software.

Reach me out here

Top comments (0)