Hey folks! Today we will talk about the SOLID principles that I am still learning. If you are a newcomer to software or a junior-level developer, sometimes it can be hard to write the code in a more readable way. Moreover, the code you write must be open to other improvements to be made. If you’re new to the software, you know it’s hard to do both at the same time. So how do we get rid of this complexity or disorder? At this point, design principles and architectures come into play. I will now try to explain these principles, which were given to me to learn on the first day of my internship on iOS, simply. Let’s start!
What are exactly SOLID Principles?
The SOLID Principles consist of five principles that form the basis of object-oriented programming. The main purpose of these principles is to enable the developer to write the code in a more readable and development-friendly way. This acronym was created by Robert C. Martin. (We all know him as Uncle Bob.) Its expansion is as follows:
S → Single Responsibility Principles
O → Open-Closed Principles
L → Liskov Substitution Principles
I → Interface Segregation Principles
D → Dependency Inversion Principles
By using these principles, the problems caused by bad architecture can be eliminated. We must not forget that things get pretty difficult when you memorize them and try to apply them to code. Although it may seem easy to implement them on simple code snippets, it is quite difficult to understand how these principles will be reflected in the code when the code becomes more complex. That’s why we need to try to understand the logic instead of memorizing it. This was one of the mistakes I made, don’t memorize it. Try to understand.
Now we will examine each principle under separate headings with code examples.
Single Responsibility Principles
A class should have one, and only one, a reason to change.
Every class, module, or method we write should have a single responsibility. When a class has multiple responsibilities, it’s hard to understand and keep track of operations. If we give each class only the responsibilities we want it to do, it will increase the readability of the code. Doing this will make it easier to adopt other functions that will be added to the class later on. So how can we show this in code?
Let’s have one editor friend whose name is Damla. She is the editor of a blog team in the field of STEM. What are Damla’s duties? Damla checks the articles from the authors. She corrects typos and revises the article if necessary. Then, prepares the article for publication. These are Damla’s duties. Now let’s see this in code.
We have a class called BlogTeam. The BlogTeam class contains functions for an article’s review and publication process. This notation is incorrect and does not conform to SRP. This class has multiple responsibilities. The BlogTeam class handles both reviews and publishing operations. There is a problem here. So how can we fix this? Let’s create an Editor class. The editor class should have only editorial responsibilities. Let’s create one main function for all responsibilities and collect all processes. Then, perform the Publish operations in a class called Publisher. Okay, we’ve divided the responsibilities. So where should they be found? We know that all these transactions actually belong to the blog team class. Finally, let’s use it with the help of objects in the StemBlogTeam class.
When we examine the final version, we can see that two different responsibilities are carried out by two different classes.
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
In simplest terms, a class should be open to extension but closed to modification.
- Open for extension
- Closed for modification
Things we add can extend the class or change its behavior, but we must not modify the class while doing this.
Suppose we have a piece of code that show hospital workers. As you can see, we have an Employee class and we keep some variables in this class. Nurse and Doctor classes have job descriptions for these employees. Is the if-else mess inside our main class, the Hospital class, the right approach? For example, when we want to add a new employee, let this employee be a cleaning staff. We’ll have to go inside an if-else block again. Isn’t it complicated to both write and read? We will have to modify the main class for each new employee. This is against the open-close principle.
So how do we fix this? Let’s try this.
Let’s take a look at the new version. We have a protocol called Employee and this protocol has a function and some variables. Our Nurse and Doctor classes will adopt the Employee protocol. Because they are employees. In our previous example, we mentioned that each of our worker classes has some task. For example, our nurse class was checking vitals. Actually, this function is the nurse’s “work” function. The good news is our Employee protocol gave us a work function. Then we can write in it. We can do the same for our doctor class. Now let’s consider the last part. All of our employees belong to a hospital. And we want to know what these hospital workers do. We have a work function inside the Hospital class, and the “employee” we wrote as a parameter adopts the Employee protocol. Now we will be able to see all the work functions that we have added to the employees using this parameter.
Let’s first look at the output of this code.
let hospital = Hospital()
let doctor = Doctor(name: "Jane", surname:"Walker")
As an output we will see:
Jane is a doctor and these are her duties. Well, let’s say a new employee joins the hospital. Let the person who comes by cleaning staff.
Our cleaning staff friend attended the hospital. Likewise, when we want to see the output of this code, we will see that there is no change in the main class. We expanded our class and made no modifications. The operation is complete.
Liskov Substitution Principles
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
This principle is directly related to object-oriented programming. LSP specifies that any function that works with a class must also work with any of its subclasses.
Let’s say we have a code where we can see the members of a paid club that works with the membership system. We have two types of members: students and teachers. Examining the code, we see that the Member class has a function and is overridden by other classes. Well, teachers will pay fees, but will students? Students will not pay fees because they are students. In this case, we cannot say that students are members. Because they do not carry all the features of the superclass. Let’s edit the code like this:
We now receive the payment function in the protocol. And we can see that only the Teacher class has adopted it. Looking at the Member class, no function requires us to override. In other words, subclasses now have all the properties of the superclass.
Interface Segregation Principles
Clients should not be forced to depend upon interfaces that they do not use.
Interfaces are not containers like a class. We need to think of them as a skill brought into the class. Rather than a single fat interface, many smaller interfaces are preferred, based on groups of methods, each serving a submodule.
An interface is called “fat” when has too many members/methods, which are not cohesive and contain more information than we want. This problem can affect both classes and protocols.
We have created a protocol that includes certain capabilities. The Pelican class can have all the features of this protocol. But the Dog class does not have the fly property. In other words, we have added an extra feature to the Dog class that it will not use.
We created a separate protocol for each feature. Now classes can only adopt the required protocols.
Dependency Inversion Principle
High-level modules should not depend upon low-level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
Dependencies between classes need to be minimized. A change to subclasses should not affect superclasses. This principle focuses on reducing dependencies between modules and providing lower coupling between classes.
DIP is very similar to the Open-Closed Principle: the approach used, to have a clean architecture, is decoupling the dependencies. You can achieve it thanks to abstract layers.
Which instance the user chooses depends on a low-level module. High-level modules should not depend on low-level modules. When choosing a credit card as a payment method, it is necessary to make changes to the code. This also falls under OCP violation. This is a completely wrong example.
We have a protocol called PaymentProtocol, which provides us with the payment method function. We also have a class for each payment method and these classes adopt a protocol called PaymentProtocol. Our main class is the PaymentManager. Here too we have a function called pay and this function takes a paymentMethod parameter that adopts PaymentProtocol. The function will be called according to the user's preferred payment method. Since all payment methods adopt PaymentProtocol, whichever method is chosen, the choice will depend on the interface, not the implementation.
It will probably take time to understand which rules we violate in our projects. Although, this is a good start to improving the quality of the code we write. I guess that’s all I have to say. Happy coding!