Understanding the SOLID Principles Using Swift

Power up your Swift codebases with object-oriented class design

Celal TOK
Better Programming

--

Photo by Wesson Wang on Unsplash

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

--

--