Understanding the SOLID Principles Using Swift
Power up your Swift codebases with object-oriented class design
I have been trying to improve my code quality, write more readable and developable code for a while. One of the most important steps in this process is to apply SOLID principles in code. Let’s see what is this SOLID?
SOLID are the five principles of object-oriented class design. It is a set of rules and best practices to follow while designing a structure.
(S) Single Responsibility Principle
Basically, this principle emphasises that each developed module takes only one responsibility. The object and/or class is only responsible for one task from the moment it begins.
Let’s first look at the code that does not comply with the SRP, and works under normal conditions.
We have a DataHandler
class, which creates data, parses the data, and finally saves the data it has created. If you take this code and try to run it, it will compile without any problems. So what is the problem or problems here?
When we look at the code, the DataHandler
class has multiple responsibilities, such as parsing , saving, and creating data.
As a solution, the current responsibilities can be moved to different classes:
(O) Open/Closed Principle
In simple terms, Entities should be open for extension but closed for modification.
Explanations can be confusing at times. Let’s review the code:
Let’s imagine we have a PaymentManager
. Let this manager support cash and Visa payment methods in the first stage. So far everything is great. After a while, we had to update the manager and we are expected to add MasterCard feature as a new feature.
Let’s create a function called makeMasterCardPayment
like the previous functions. Great, our code will continue to work. We complied with the requirements but we broke a rule that the class must be closed for modification. For a task that does a similar job, we shouldn’t add anything new to the class.
Let’s see how we can solve this problem:
Let’s define the main task in the PaymentManager
in an abstract structure (protocol), this structure will answer the requirements that PaymentManager
expects.
We also created a separate class for each Payment method, these classes will be in the abstract structure that PaymentMamager
expects. So we can add as many new payment methods as we want without making any changes in the manager.
So we kept our PaymentManager
class open to extensions but closed to modifications.
(L) Liskov Substitution Principle
Objects should be replaced with instances of their subclasses without altering the behavior. After this short explanation, let’s talk about the code.
Suppose we have a class of rectangles, the rectangles have a width and a height, and their product is equal to the area.
Whether we have a square class, theoretically a square is a rectangle, so we can inherit the class square from the class rectangle. So far everything is great.
The following setSizeAndPrint
function expects a rectangle type variable and assigns the rectangle width and height by default. It’s okay to call this function for the rectangle class, because width = 4
, height = 5
, area = 20
.
But the same is not true for a square that inherits from the rectangle class because the two sides of a square are equal. We can’t just assign 4 and 5 by default and expect it to behave like the class it inherits.
At this point, classes that can’t act as inherited classes and need situation-specific development breaks the LSP.
As a solution, it is aimed for each class to perform its own tasks within itself, by keeping the common tasks between classes in a certain abstract structure (protocol).
As in the example above, the common task between the Rectangle
and Square
classes is to calculate the area of the object. Both the rectangle and square classes inherit the Polygon abstract structure after this task is defined in a common protocol. Thus, each class fulfills the necessary tasks within itself and there is no need to make any special developments. Classes behave just like the structure they inherit.
(I) Interface Segregation Principle
In summary, Clients should not be forced to depend upon interfaces that they do not use. No code should be forced to depend on methods it does not use.
Let’s jump right into the code and see the problem practically.
Let’s have an abstract structure called Worker
, and in general, we expect those who inherit from the Worker class to be able to do the eat and work tasks.
First, let’s have a class called Human
and this class inherits from the abstract structure Worker. Theoretically, we expect a person to both eat and work. Then we needed a robot structure and we inherited it from the Worker structure because the robot can work.
The problem starts here because Worker protocol has two functions, one is work and one is eating, there is no problem for the work function because the robot can run, but since we inherit from the worker structure, we have to add the eat function as well, which causes an unnecessary responsibility to be passed to the class. This is an ISP break.
In order to solve this problem, we must divide our responsibilities, which have an abstract structure, into basic parts.
We are creating a new abstract structure called Feedable
for the eat function, and the Workable
abstract structure for the work function. Thus, we have divided our responsibilities.
Now the Human
class will inherit from Feeble
and Workable
, and the Robot
class from Workable
only.
Thus, we do not impose any unnecessary responsibility on any class and we create a structure suitable for the ISP.
(D) Dependency Inversion Principle
DIP theoretically high-level modules should not import anything from low-level modules. Both should depend on abstractions and Abstractions should not depend on details. Details should depend on abstractions.
Let’s look at our example below with theoretical information.
We have an employee
structure and this structure has a work function. We also have an Employer
structure and this structure enables existing employees to work.
An employer
object is created in the run function and by default, it takes the array Employee
. Again, everything is fine so far, probably our project will work, but there is something we missed here. The Employer
structure is directly linked to the non-abstract Employee
structure. This is the point where we need the DIP.
Using the theoretical knowledge of DIP, we know that structures should depend on an abstract model.
So, we created an abstract Workable
structure and depend the Employee
class to Workable
so that the Employee
structure retains its original functions.
The point is that the Employer
class now expects the array of the abstract struct Workable
instead of the array Employee
. Thus, we have linked the dependency of the Employer
structure to an abstract module. This means that the Employer
structure has come to the point where it can run any structure depend to the Workable
module.
I hope you enjoyed the article. You can find the all codes on my GitHub:
References Links
1.https://en.wikipedia.org/wiki/Dependency_inversion_principle
2.https://www.youtube.com/watch?v=rndiYu8If-I&list=PL_csAAO9PQ8ZIh89P2re5fziX9kaI8Yhx&index=1
3.https://en.wikipedia.org/wiki/SOLID
4.http://www.principles-wiki.net/principles:open-closed_principle
5.https://en.wikipedia.org/wiki/Dependency_inversion_principle