SOLID Design Principles

Desing principles are extremely important in software development. Sometimes due to the project deadlines, we tend to not to follow the SOLID principles or forget to implement the same. This will lead to a bad design, tight coupling, difficult to unit test, poor performance etc.

In this article let us learn about what is SOLID principles and how to implement it in our day to day programming.

As the name implies, SOLID design principles are the set of 5 principles which are very much required to write a better code.  

  1. Single Responsibility Principle
  2. Open Close Principle
  3. Liscov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

1. Single Responsibility Principle

As the name says, each and every class/struct should have a single responsibility. 

Consider the example of an UserAccountSettingsViewController as shown below:

1. UserAccountSettingsViewController

In the above example, the class has four functions. 

The method parseUserDataResponse performs an URLSession data task. Also the method saveUserAccountInformationToCoredata performs coredata save operation.

  1. createUserAccount - This would be a valid method to be in the class
  2. deleteUserAccount- This would be a valid method to be in the class 
  3. parseUserDataResponse - This method to parse userResponse should not be used in this class.
  4. saveUserAccountInformationToCoredata - This method to save user account should not be used in this class.

Instead of calling URLSession method in the class UserAccountSettingsViewController, we could create a new NetworkManager class and have all the URLSession related tasks there. 

Simillarly, we could create a new CoredataManager class to setup the coredata stack and coredata save methods.

Methods can be segregated as shown below:

class NetworkManager {
    static func fetchData(completion: @escaping (User) -> Void) {
        // Perform URLSession datatask here
    }
}
class CoredataManager {
    // Set up Coredata stack
    // Add user account to coredata
    // Delete user account from coredata
    static func save() {
        // Perform user account save to coredata
    }
}

After separating the implementation details of Network call and coredata save methods in its dedicated class, the final UserAccountSettingsViewController will look like below:

2. Implementation of Single Responsibility

2. Open Closed Principle

Open close principle states that the entities like classes, structs etc should be open for extension but closed for modification.

Consider the below Animal class which accepts name in the initialization. It also contains the printAnimalSound method which prints the sound of that particular animal as shown below:

3. Class Animal

Now, let us create the instance of the Animal class in the ViewController and print the sound method as shown below:

4. Create Animal Object in View Controller

The output will be as shown below:

5. Dog Sound
Open/Close principle Violation: 

Let us consider the Animal class. It has an init which takes name as the parameter and a function which prints the sound of the animal. Currently printAnimalSound method has a condition to handle the sounds of the animals dog and cat. But in future, if I want to print the sound of the animal lion, there is no option. I have to add another if condition to handle the animal lion. This is a violation of Open/Closed principle. 

Apply Open/Closed Principle:

In this section, let us modify the Animal class and apply the open/closed principle.

Let us create the AnimalProtocol and have a method signature printAnimalSound. Let us also create a Dog class and Lion class and confirm to the AnimalProtocol as shown below:

6. Class Lion and Dog confirm to AnimalProtocol

Now, in the View Controller let us create an instance of dog and lion classes as shown below:

7. Lion and Dog Objects in ViewController

With the above modifications, in future if we want to create a new animal, we could easily create the animal class and confirm to the AnimalProtocol to implement the printAnimalSound method. 

Hence with the help of protocols, we have implemented the Open/Closed Principle.

3. Liscov Substitution Principle

Liscov Substitution Principle states that Objects of the SuperClass should be replaceable with the objects of its SubClass without breaking the application.  This is in one way an extension of the Open/Closed Principle.

Let us consider a very basic example of Square and a Rectangle. We all know that square is also a rectangle, just that all the sides will be equal.

Let us create a subclass Square from Rectangle and see the results.

8. Rectangle and Square classes

In the above example we could see that there are two classes Rectangle and Square. Class square is a subclass of rectangle since square is also a rectangle. 

Both the classes rectangle and square has a method to calculate the area. Since all the sides are equal in a square, we override the width  variable and set the length same as the width. 

According to LSP, all the subclasses should stick to the contract set by the super class. Meaning, the subclasses should not alter the behaviour of the super class. 

In the class Square, we could see that it is overriding the width variable to set the length same as width. This is the clear violation of Liscov Substitution Principle.

Apply LSP:

To apply LSP in a proper way, let us create a protocol Shape and both the classes Rectangle and Square confirm to the protocol Shape

9. Apply LSP

4. Interface Segregation Principle

Interface segregation principle states that the modules like classes/structs etc should not be forced to implement the methods that it does not use. 

Let us consider a protocol OfficeRoutine as shown below:

8. OfficeRoutine Protocol

Let us now create a PersonOneOfficeRoutine class which confirms to OfficeRoutine protocol as shown below:

PersonOneOfficeRoutine
9. PersonOneOfficeRoutine class

We can see that the class PersonOneOfficeRoutine confirms to the OfficeRoutine protocol. This means that this class needs to implement all the methods that are mentioned in the protocol.

There would exist a person whose office routine may not include morningCoffeeBreak or attendingDemo or an eveningSnacks but still need to implement these methods just by confirming to the OfficeRoutine protocol. 

This will result in a bulkier protocol. 

To avoid this, let us split the protocol into multiple smaller protocols as shown below:

10. Smaller Protocol

As we can see above, the previous OfficeRoutine protocol is split into multiple smaller protocols. This way we could confirm to the required protocols and implement those methods. 

This is all about the Interface segregation Principle.

5. Dependency Inversion Principle

Dependency Inversion principle states that High level modules should not depend on the low level modules, both should depend on the Abstractions.

Let us consider the example as shown below:

11. High level module dependent on low level module

In the above example, we have a class DataHandler that takes APIOperation in the init. It also has a performFetchData method.

Next, we have a class APIOperation to make a RESTApi call and fetch the data from the server. 

In future, there might come a situation where Data Handler has to fetch the data from multiple sources like API/Firebase depending on different use cases. 

But here the High level module DataHandler depends on the low level module APIOperation.

This leads to a tight coupling and hard to unit test. To mitigate this let us apply Dependency Inversion Principle and restructure the code

Applying Dependency Inversion Principle:

  1. Let us create a protocol named DataFetchable. Let us have a method signature fetchData() in the protocol.
  2. In the DataHandler class, let use replace the apiOperation property with DataFetchable
  3. In the DataHandler class, replace apiOperation in the init with DataFetchable

Thus with the above 3 steps DataHandler class is not dependent on the low level module APIOperation, but the abstraction that is a protocol.  

In future if we want to fetch data from the Firebase, we could create a new FirebaseOperation class confirming to DataFetchable protocol.

Usage of this can be seen like below:

 

12. Apply Dependency Inversion Principle

Now, with the above modifications DataHandler is not dependent on the low level modules like APIOperation or the FirebaseOperation.

The DataHandler class is reusable without any modification and can be unit testable as well.

Based on different use cases, APIOperation and FirebaseOperation functions be called like below:

Leave a Comment

Your email address will not be published. Required fields are marked *