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.
- Single Responsibility Principle
- Open Close Principle
- Liscov Substitution Principle
- Interface Segregation Principle
- 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:
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.
createUserAccount - This would be a valid method to be in the class
deleteUserAccount- This would be a valid method to be in the class
parseUserDataResponse - This method to parse userResponse should not be used in this class.
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. 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:
Now, let us create the instance of the Animal class in the ViewController and print the sound method as shown below:
The output will be as shown below:
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:
Now, in the View Controller let us create an instance of dog and lion classes as shown below:
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.
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
.
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:
Let us now create a PersonOneOfficeRoutine class which confirms to OfficeRoutine protocol as shown below:
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:
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:
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:
- Let us create a protocol named DataFetchable. Let us have a method signature
fetchData()
in the protocol. - In the DataHandler class, let use replace the apiOperation property with DataFetchable
- 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:
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: