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.
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.
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.
ReplyDeleteVisit site
Amazing! Its a genuinely remarkable piece of writing, I
ReplyDeletehave got much clear idea on the topic from this post.
Google Belgie