SOLID Principles Applied To Swift
A maintainable component. Reusable. Just a dream? Maybe not. SOLID principles, may be the way.
SOLID is an acronym named by Robert C. Martin (Uncle Bob). It represents 5 principles of object-oriented programming: Single responsibility, Open/Closed, Liskov Substitution, Interface Segregation and Dependency Inversion.
Thanks to these principles, you can solve the main problems of a bad architecture:
- Fragility: A change may break unexpected parts—it is very difficult to detect if you don’t have a good test coverage.
- Immobility: A component is difficult to reuse in another project—or in multiple places of the same project—because it has too many coupled dependencies.
- Rigidity: A change requires a lot of efforts because affects several parts of the project.
Of course, as Uncle Bob pointed out in a his article, these are not strict rules, but just guidelines to improve the quality of your architecture.
Principles will not turn a bad programmer into a good programmer. Principles have to be applied with judgement. If they are applied by rote it is just as bad as if they are not applied at all.
You must be smart enough to understand when apply these principles.
The Single Responsibility Principle (SRP)
THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.
Every time you create/change a class, you should ask yourself: How many responsibilities does this class have?
Let’s see an example:
How many responsibilities does this class have?
Handler retrieves the data from the API (1), parses the API response, creating an array of
String, (2) and saves the array in a database (3).
Once you consider that you have to use in the same class Alamofire for the api request, ObjectMapper for the parsing and the CoreData stack to save the data in the database, you will start understanding the smell of this class.
You can solve this problem moving the responsibilities down to little classes:
This principle helps you to keep your classes as clean as possible. Moreover, in the first example you couldn’t test
saveToDB directly, since those were private methods. After the refactor, you can easily do it testing
The Open-Closed Principle (OCP)
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.
If you want to create a class easy to maintain, it must have two important characteristics:
- Open for extension: You should be able to extend or change the behaviours of a class without efforts.
- Closed for modification: You must extend a class without changing the implementation.
You can achieve these characteristics thanks to the abstraction.
As example, we have a class
Logger which iterates an array of
Cars and prints the details of each car:
If you want to add the possibility to print also the details of a new class, we should change the implementation of
printData every time we want to log a new class—breaking OCP:
We can solve this problem creating a new protocol
Printable, which will be implemented by the classes to log. Finally,
printData will print an array of
In this way, we create a new abstract layer between
printData and the class to log, allowing the print of other classes like
Bicycle and without changing the
The Liskov Substitution Principle (LSP)
FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.
Inheritance may be dangerous and you should use composition over inheritance to avoid a messy codebase. Even more if you use inheritance in an improper way.
This principle can help you to use inheritance without messing it up. Let’s see the main problems which break LSP:
We have a class
Handler, it has the responsibility to save a string in a Cloud service. Then, the business logic changes, and, sometimes, you must save the string just if its length is greater than 5. Therefore, we decide to create a subclass
This example breaks LSP because, in the subclass, we add the precondition that
string must have a length greater than 5. A client of
Handler doesn’t expect that
FilteredHandler has a different precondition, since it should be the same for
Handler and all its subclasses.
We can solve this problem getting rid of
FilteredHandler and adding a new parameter to inject the minimum length of characters to filter:
We have a project where we must compute the area of some rectangle objects—so we create the class
Rectangle. After a couple of months, we need to compute also the area of square objects—so we decide to create a subclass
Square. Since in a square we need just a side to compute the area—and we don’t want to override the computation of
area—we decide to assign the same value of
With this approach, we break LSP because if the client has the current method:
the result should always be the same in the both calls:
Instead, the first one prints
10 and the second one
4. This means that, with this inheritance, we have just broken the postcondition of the
width setter which is:
((width == newValue) && (height == height)).
We can solve it using a protocol with a method
area, implemented by
Square in different ways. Finally, we change the
printArea parameter type to accept an object which implement this protocol:
The Interface Segregation Principle (ISP)
CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.
This principle introduces one of the problems of object-oriented programming: the fat interface.
An interface is called “fat” when has too many members/methods, which are not cohesive and contains more information than we really want. This problem can affect both classes and protocols.
Fat interface (Protocol)
We start with the protocol
GestureProtocol with a method
After some time, you have to add new gestures to the protocol and it becomes:
SuperButton is happy to implement the methods which it needs:
The problem is that our app has also a
PoorButton which needs just
didTap. It must implement methods which it doesn’t need, breaking ISP:
We can solve the problem using little protocols instead of a big one:
Fat interface (Class)
We can use, as example, an application which has a collection of playable videos. This app has the class
Video which represents a video of the user’s collection:
And we inject it in the video player:
Unfortunately, we are injecting too many information in the method
play, since it needs just
You can solve this problem using a protocol
Playable with just the information which the player needs:
This approach is very useful also for the unit test. We can create a stub class which implements the protocol
The Dependency Inversion Principle (DIP)
A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.
This principle is the right one to follow if you believe in reusable components.
DIP is very similar to Open-Closed Principle: the approach to use, to have a clean architecture, is decoupling the dependencies. You can achieve it thanks to abstract layers.
Let’s consider the class
Handler, which saves a string in the filesystem. It calls, internally,
FilesystemManager which manages how to save the string in the filesystem:
FilesystemManager is the low-level module and it’s easy to reuse in other projects. The problem is the high level module
Handler, which is not reusable because is tightly coupled with
FilesystemManager. We should be able to reuse the high level module with different kind of storages like a database, Cloud and so on.
We can solve this dependency using a protocol
Storage. In this way,
Handler can use this abstract protocol without caring of the kind of storage used. With this approach, we can change easily from a filesystem to a database:
This principle is very useful also for testing. You can easily use a stub class—which implements
Storage—and test if
handle calls the method
save of the
Storage object injected:
If you follow SOLID principles judiciously, you can increase the quality of your code. Moreover, your components can become more maintainable and reusable.
The mastering of these principles is not the last step to become a perfect developer, actually, it’s just the beginning. You will have to deal with different problems in your projects, understand the best approach and, finally, check if you’re breaking some principles.
You have 3 enemies to defeat: Fragility, Immobility and Rigidity. SOLID principles are your weapons. Enjoy!