Skip to main content

SOLID Design Principles in Swift

What are solid principles in programming world?

solid design principles in swift
5 SOLID design principles in swift
SOLID is acronym used in software programming for making software design more understandable, flexible, scalable and maintainable. Every Software Developer must be aware of 5 SOLID principles in order to deliver good quality code

SOLID stands for what?
S -  Single Responsibility principle
O - Open Closed Principle
L - Liskov substitution Principle
I - Interface segregation Principle
D - Dependency Inversion Principle

If we apply 5 SOLID principles while creating iOS/Mac Apps then the benefits which we will get are as follow:
·      We will have flexible code which can be changed easily.
·      Software code becomes more reusable.
·      Software developed will be robust, scalable and stable.
·      Code is loosely couple which means dependency between the elements is low.

Now let’s discuss each principle one by one.

1.)  Single Responsibility Principle

This principle states that class should have only one single  responsibility.
Suppose we have a HomeViewController which fetches data from API, parses the API response  and save the received data in the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class HomeViewController{
    
    func getData(){
        let jsonData = getDataFromApi()
        let dataString = parseData(data: jsonData)
        saveDataToDatabase(data: dataString)
    }
    private func getDataFromApi()-> Data{
        // Fetch data from API and return the data
        return Data()
        
    }
    private func parseData(data:Data)-> String{
        //parsing data and return string
    }
    
    private func saveDataToDatabase(data:String){
        //saving data to database
    }
}

Now, if we see how many responsibilities does this class have then it perform following three tasks
·      Getting data from API.
·      Parsing data received from API
·      Saving data in to the CoreData

So above code is breaking the single responsibility principle. Let’s divide three tasks mentioned above into three classes as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class HomeViewController1{
    
    let dataManager = DataManager()
    let jsonParser = JSONParser()
    let coreDataManager = CoreDataManager()
    func getData(){
     let jsonData = dataManager.getDataFromApi()
     let dataString = jsonParser.parseData(data: jsonData)
     coreDataManager.saveDataToDatabase(data: dataString)
    }
}

class DataManager{
    func getDataFromApi()-> Data{
        // Fetch data from API and return the data
        return Data()
    }
}

class JSONParser{
    func parseData(data:Data)-> String{
        //parsing data and return string
        return ""
    }
}

class CoreDataManager{
    func saveDataToDatabase(data:String){
          //saving data to database
      }
}

Now our code is following Single Responsibility Principle because each class is performing only one single task. This principle makes our classes as clean as possible and it is easy to test.

2.)  Open closed principle

 According to this principle, classes should be open for extension but closed for modification.
If we want to create a class which is easy to maintain in future then it should follow two important points.
·      Open for extension : We should be able to add functionality to the class without much efforts.
·      Closed for modification: we must extend the class without changing its actual behaviour.

Let’s understand OCP with an example. Suppose we have a vehicle class which iterates an array of Cars and prints detail of each car.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Vehicle {
    func printVehicleData() {
        let cars = [Car(color: "White"),
                   Car(color: "black"),
                   Car(color: "blue")]
        cars.forEach { car in
            print(car.printDetails())
        }
    }
}
class Car {
    let color: String
    init( color: String) {
        self.color = color
    }
    func printDetails() -> String {
        return "car color is \(color)"
    }
}


Let’s say the new requirement comes and we have to print the details of new class called Truck due to which we have to change the implementation of printVehicleData() method which clearly breaks the OCP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Vehicle {
    func printVehicleData() {
            let cars = [Car(color: "White"),
            Car(color: "blue")]
            cars.forEach { car in
              print(car.printDetails())
            }
            let trucks = [Truck(brandName: "TATA"),
            Truck(brandName: "Mahindra")]
            trucks.forEach { truck in
                print(truck.printDetails())
            }
       }
}
class Car {
    let color: String
    init( color: String) {
        self.color = color
    }
    func printDetails() -> String {
        return "car color is \(color)"
    }
}
class Truck {
    let brandName: String
    init( brandName: String) {
        self.brandName = brandName
    }
    func printDetails() -> String {
        return "Truck brandName is \(brandName)"
    }}



We can solve above problem by using new protocol Printable which will be implemented by Car and Truck classes. In this way we can print vehicles details of any class which adopts Printable protocol.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protocol Printable {
    func printDetails() -> String
}
class Vehicle {
  func printVehicleData() {
   let vehicles:[Printable] = [Car(color: "White"),
                              Car(color: "black"),
                              Car(color: "blue"),
                              Truck(brandName: "TATA"),
                              Truck(brandName: "Mahindra")]
        vehicles.forEach { vehicle in
            print(vehicle.printDetails())
        }
    }
}
class Car:Printable {
    let color: String
    init( color: String) {
        self.color = color
    }
    func printDetails() -> String {
        return "car color is \(color)"
    }
}
class Truck:Printable {
    let brandName: String
    init( brandName: String) {
        self.brandName = brandName
    }
    func printDetails() -> String {
        return "Truck brandName is \(brandName)"
    }}

3.)  Liskov Substitution Principle (LSP)

This principle states that whenever derived class extend the base class then it should not break the base class behaviour at any time.
Suppose we have DataManager class which saves some string in Database. Then a new requirement comes in, where sometimes we have to save a string only if its length is greater than 10. Therefore we decided to create a new subclass FilteredDataManager.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class DataManager{
    func save(string: String) {
        // Save string into Database
    }
}
class FilteredDataManager: DataManager {
    override func save(string: String) {
        guard string.count > 10 else { return }
        super.save(string: string)
    }
}

But unfortunately, this breaks LCP because in the subclass we have added the precondition that string must be greater than 10. According to LCP, derived class should not modify the base class implementation. So in order to resolve this problem, we should remove the Filtered DataManager class and  can add precondition to filter strings in save method of DataManager class itself.

1
2
3
4
5
6
class DataManager {
    func save(string: String, minlength: Int = 0) {
        guard string.count >= minlength else { return }
        // Save string into Database
    }
}
4.)  The Interface Segregation Principle (ISP)
This principle states that instead of having general interface we should create different interfaces (protocols) that are specific to each client. Additionally, client doesn’t have to implement the methods that it doesn’t use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
protocol AnimalProtocol {
    func walk()
    func swim()
    func fly()
}

struct Animal: AnimalProtocol {
    func walk() {}
    func swim() {}
    func fly() {}
}
 
struct Whale: AnimalProtocol {
    func walk() {}
    func fly() {}
    func swim() {}
}
Lets’ understand this with help of an example
In above example we created the Animal protocol which includes displacement methods for various animals. However whale adopted the Animal protocol but there are two methods fly() and walk() which it doesn’t implement. So above code is breaking the interface segregation principle. To resolve this, we can create three new protocols with one method each. Let’s see below code how we can do it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
protocol WalkProtocol {
    func walk()
}

protocol SwimProtocol {
  func swim()
}

protocol FlyProtocol {
  func fly()
}

struct Whale: SwimProtocol {
    func swim() {}
}
 
struct Cat: WalkProtocol{
    func walk(){}
}

So as we can see now that there are three different protocols for different displacements so whenever we create new struct we can adopt any protocol according to our need.
5.)  Dependency Inversion Principle
According to this principle, high level modules should not depend upon low level modules and both high and low level modules should depend upon abstraction.

The main aim of this principle is to reduce the dependency between the modules and thus incorporate lower coupling between classes.
Lets understand with the help of example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class FileHandler {

    let fileManager = FileSystemManager()
    func handle(string: String) {
        fileManager.save(string: string)
    }
}

class FileSystemManager {
    func save(string: String) {
        // Open a file, saving value and closing it
    }
}

In above code, FileHandler class save the string in the file system. It calls method of FileSystemManager which manages how to save the string in the file system. Here FileSystemManager is low level module which can be reuse in other projects but here the problem exist with high level module FileHandler which is not reusable because it is tightly couple with FileSystemManager. We should make FileHandler class reusable so that it can deal with different storages like database, cloud etc. we can solve this dependency using Storage protocol in which FileHandler will adopt that abstract protocol without caring the kind of storage used. Using this approach we can switch from saving string in filesystem to database anytime. Have a look on below code on how we can achieve it.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FileHandler {
    let storage: Storage
    init(storage: Storage) {
        self.storage = storage
    }
    func handle(string: String) {
        storage.save(string: string)
    }
}
protocol Storage {
   func save(string: String)
}
class FileSystemManager: Storage {
    func save(string: String) {
        print("file system handler called")
        // Open a file, saving value and closing it
       
    }
}
class DatabaseManager: Storage {
    func save(string: String) {
        print("database handler called")
        // Connect to the database,execute query
    }
}

If we follow SOLID principles while creating apps for iOS platform then we can definitely increase the quality of the code and our components would become more reusable and maintainable.

If you enjoyed reading this article🙂, then please don't forget to share it with your friends and do subscribe this blog to receive more technical posts in future via email.

Comments

  1. Hi, I want to express my gratitude to you for sharing this fascinating information. It's amazing that we now have the ability to share our thoughts. Share such information with us through blogs and internet services.
    Visit site

    ReplyDelete
  2. Amazing! Its a genuinely remarkable piece of writing, I
    have got much clear idea on the topic from this post.
    Google Belgie

    ReplyDelete

Post a Comment

Popular posts from this blog

Lifecycle of React Native Component [2020 Edition]

What are the life cycle methods of React Native Component? A component's life cycle in React Native can be divided into 4 phases: React Native Component life cycle phases Mounting:  In this phase,  component instance is created and inserted into the DOM. Updating: In updating phase, a react component is said to be born and it start growing by receiving new updates. Unmounting: In this phase, a react component gets removed from actual DOM. Error Handling: It is called when any error occurs while rendering the component. Now let's discuss about different methods that gets called during these phases. Mounting phase Below are the methods which gets called when instance of component is created and inserted into the DOM. Constructor() static getDerivedStateFromProps() render() ComponentDidMount() Constructor() constructor ( props ) { super ( props ) ; this . state = { employeeId : 0 } ; } It is first method which gets called in the l

What's new in iOS 13

Hi guys, Lets have a quick review on what new features Apple has released in its latest iOS 13 version. iOS 13 version iOS 13 makes old iPhone faster, last longer  - In iOS 13, Face ID unlocking will be 30% faster than before , app launch times are two times faster. Apple also found a way to make app downloads smaller, up to 60% on average. iOS 13 Dark Mode  - Apple introduced new dark mode option which changes the entire look of the operating system from light to dark. All native Apple apps feature Dark Mode support, and third-party apps can use Dark Mode APIs to add Dark Mode integration. iOS 13 features a QuickPath keyboard  - With iOS 13, Apple's default QuickType keyboard will be incorporating swipe-to-type, a popular way of sliding across the keyboard to form words. Before this, we have to use extensions like Google’s Gboard and SwiftKey Sign - In with Apple - Now, we can use Apple sign In to  conveniently sign in to third-party accounts.Sign-in with A