π‘ Recommendation: Return to this article as many times as necessary to clarify the following volumes, as there are concepts that complement these.
Previous Volume: Cleaner Flutter Vol. 1: Intro to CLEAN
Next Volume: Cleaner Flutter Vol. 3: Dominating Entities
After starting in the world of programming we all reach a point where we have to look back on the road and review some of the lines of code that we have written, either 1 day ago to remember an idea or years ago to review the implementation of any module of our software.
Many times in these glances at the code of the past we come across a list of problems such as:
- Having to search among many files for the answer to what we are looking for.
- Not understanding the code we wrote.
- Not understanding the code we write.
These problems start since we start a project because we do not spend enough time to have a clear idea not only of what we are going to do, but also of how we are going to do it.
We have to develop code imagining what would happen if I return in 2 years to review it, this ability to program clean and understandable code is essential to facilitate development, especially if you work in a team.
What is SOLID?
SOLID is the acronym for a set of principles that help us develop more maintainable code that also allows easy extension without compromising code already developed.
In other words ...
Write more code without damaging what already works.
We can even see them as a set of guidelines to follow.
Now we are going to explore each of the principles, which can be applied to any programming language, but I am going to cover them using Dart language since it is the language used by the Flutter framework.
Before continuing it is important to note that these were first introduced by Uncle Bob, I leave you a link in case you want to see his explanation: SOLID Principles Uncle Bob.
S: Single Responsibility Principle
A class must have one, and only one, reason to change.
To explain this principle we can imagine a messy room, as we have all had it at some point, perhaps even now that you are reading this.
But the truth is that within this, everything has its place and everything should be in its designated place.
To put the analogy aside, this principle tells us more specifically:
- A class must have a unique responsibility (applies to methods, variables, entities, etc).
- There is a place for everything and everything should be in its place.
- All the variables and methods of the class must be aligned with the objective of the class.
By following these principles, we achieve smaller and simpler classes that have unique objectives. Also we avoid giant classes with generic properties and methods that can be redundant in development.
Let's see an example:
Let's take a look at this signUp ()
function. This could be the method we call from our UI layer to perform the user sign-up process.
In the code we see that there is functionality of creation, validation, conversion to JSON, and even the call to the database that is normally an API call, so we are clearly not fulfilling the principle.
This is one of the most common mistakes, especially in Flutter, since developers easily make the mistake of combining different things within the same class or methods.
β οΈ In other articles and videos we will see how this applies to Clean Architecture ....
Done, I got it, now ... how do I apply it?
To follow the Single Responsibility Principle
correctly, we could create methods and classes with simple and unique functionalities.
In the example method signUp ()
many things are done with different objectives, each of these functionalities could be separated into an individual class with a single objective.
The validation
We could implement a class that is responsible for performing the validation, there are many ways to do this, one of them can be to use formz
which is a Flutter package that allows us to create classes for a data type and perform a validation
Do not focus too much on the logic of the code, the important thing is to understand that now the validation is decoupled from the rest of the logic with the validator ()
method.
Here is the link to formz in pub.dev.
The connection to the data source
The other error is to call an API from the business logic or UI, this would not fulfill the principle since the connection with the API is a complex functionality by itself, so it would be best to implement a class as repository to which we pass the parameters that we are going to send and delegate the rest of the process to it.
This repository concept is key to meeting the Clean Architecture standards but we can see that the principle of Single Responsibility is behind this whole idea.
By implementing these classes applying the principle we would achieve a simpler method compared to how we started with:
Simpler, more maintainable and decoupled.
O: Opened/Closed Principle
An entity must be open to extend, but closed to modify.
Este principio nos dice, en resumen, que debemos extender de la entidad para agregar nuevo cΓ³digo en vez de modificar el existente.
This principles tell us that we must extend our classes to add new code, instead of modifying the existing one.
The first time we read this it can be a bit confusing but it just is:
Don't modify what already works, just extend and add new code.
n this way, we can develop without damaging the previously tested code. To understand this principle we can see an example provided by Katerian Trjchevska at LaraconEU 2018
Let's imagine that our app has a payment module that currently only accepts debit / credit cards and PayPal as payment methods.
At a first glance at the code we may think that everything is fine, but when we analyze its long term scalability, we realize the problem.
Let's imagine that our client asks us to add a new payment method such as Alipay, gift cards and others.
Each new payment method implies a new function and a new else if
in thepay ()
method and we could say that this is not a problem, but if we keep adding code within the same class, we would never achieve a stable, ready for production code.
By applying the open / closed principle, we can create an abstract class PayableInterface
that serves as a payment interface, in this way each of our payment methods extends this abstract class[Payment Method Name] extends PayableInterface
and it can be a separate class that is not affected by modifications made to another.
After having our payment logic implemented, we can receive a parameter with the paymentType
that allows us to select the PayableInterface
indicated for the transaction and in this way we do not have to worry about how thepay ()
method makes the payment, only to make a type of filtering so that the correct instance of the interface is used; be it Card, PayPal or Alipay.
In the end we would have a method like this where we can see that the code was reduced to only 3 lines and it is much easier to read.
It is also more scalable since if we wanted to add a new type of payment method we would only have to extend from PayableInterface
and add it as an option in the filtering method.
π I know these concepts of abstractions and instances are confusing at first but throughout this series of articles and by practice I promise they'll become simple concepts.
L: Liskov Substitution Principle
We can change any concrete instance of a class with any class that implements the same interface.
The main objective of this principle is that we should always obtain the expected behavior of the code regardless of the class instance that is being used.
To be able to fulfill this principle correctly there are 2 important parts:
- Implementation
- Abstraction
The first we can see in the previous example of Open / Closed Principle when we have PayableInterface
and the payment methods that implements it asCardPayment
and PaypalPayment
.
In the implementation of the code we see that it doesn't matter with implementation we choose, our code should continue to work correctly, this is because both make a correct implementation of the PayableInterface
interface.
With this example the idea is easy to understand but in practice there are many times that we perform the abstraction process wrong, so we cannot truly make a great use of the principle.
If you are not very familiar with concepts such as interface, implementation, and abstraction this may sound a bit complex but let's see it with a simple example.
This is one of the iconic images of the principle as it makes it easy to understand.
Let's imagine that in our code we have a class called DuckInterface
.
This gives us the basic functionality of a duck like fly
,swim
, quack
and we would have theRubberDuck
class that implements the interface.
At a first glance we could say that our abstraction is fine since we are using an interface that gives us the functionality we need, but the fly ()
method does not apply to a rubber duck, imagine that our program is going to have different Animals with shared functionality such as flying and swimming, so it would not make sense to leave this method on the DuckInterface
interface.
To solve this and comply with the Liskov Principle we can create more specific interfaces that allow us to reuse code, which also makes our code more maintainable.
With this implementation, our RubberDuck
class only implements the methods it needs and now, for example, if we need an animal that fulfills a specific function such as swimming, we could use any class that implements theSwimInterface
interface. This is because by fulfilling the Liskov Principle we can switch any declaration of an abstract class by any class that implements it.
I: Interface Segregation Principle
The code should not depend on methods that it does not use.
At first this could seem to be the simplest principle but for this very reason, at the beginning, it can even confuse us.
In the previous principles we have seen the importance of using interfaces to decouple our code.
This principle ensures that our abstractions for creating interfaces are correct, since we cannot create a new instance of an interface without implementing one of the methods defined by them. The above would be violating the principle
This image shows the problem of not fulfilling this principle, we have some instances of classes that do not use all the interface methods, which lead to a dirty code and indicates bad abstraction.
It is easier to see it with the typical anima example, this is very similar to the example we saw from Liskov.
π‘ At this point the examples become similar but the important thing is to see the code from another perspective.
We have an abstract class Animal
that is our interface, it has 3 methods defined eat ()
,sleep ()
, andfly ()
.
If we create a Bird
class that implements the animal interface we don't see any problem, but what if we want to create the Dog class?
Exactly, we realize that we cannot implement the fly ()
method because it does not apply to a dog.
We could leave it like that and avoid the time needed to restructure the code since we logically know that this would not affect our code, but this breaks the principle.
The mistake is made by having a bad abstraction in our class and the right thing to do is always refactor to ensure that the principles are being met.
It may take us a little longer at the moment but the results of having a clean and scalable code should always be priorities.
A solution to this could be that our Animal
interface only has the methods shared by animals likeeat (), sleep ()
and we create another interface for thefly ()
method. In this way, only animals that need this method implement its interface.
π₯ Almost there! Last SOLID principle ...
D: Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both must depend on abstractions.
In my opinion, this should be the first principle that every developer should understand.
This principle tells us:
- You never have to depend on a concrete implementation of a class, only on its abstractions (interfaces).
- Same as the image presented in Volume 1 of this series of A Cleaner Flutter we follow the rule that modules High-level modules should not strictly rely on low-level modules.
To understand it more simply, let's look at the example.
Nowadays, every app or software that is developed needs to communicate with the outside world. Normally this is done through code repositories that we instantiate and call from the business logic in our software.
Declaring and using a concrete class, such as the DataRepository ()
within BusinessLogic ()
, is a very common practice and is one of the common mistakes that makes our code not very scalable.By depending on a particular instance of a class we surely know it will never be stable because you are constantly adding code to it.
To solve this problem, the principle tells us to create an interface that communicates both modules. You can even develop a the whole functionality of the business logic and UI of an app by depending on a interface which hasn't been implemented.
This also allows better communication in a team of developers because when creating an interface, everyone is clear about the objectives of the module and from that definition, it can be verified that the SOLID principles are being met.
With this implementation, we create a DataRepositoryInterface
that we can then implement inDataRepository
and the magic happens inside the class that uses this functionality when we do not depend on a concrete instance but instead on an interface we could pass as parameters any concrete class that implements this interface.
It could be a local or external database and that would not affect the development since I repeat it again we do not depend on a single concrete instance, we can use any class that complies with the implementation of the interface.
And this, ladies and gentlemen, is what allows us to fulfill the magic word of Clean Architecture: Decoupling!
Wrap up ...
I remind you that these are principles, not rules, so there is no single way to follow them, their use and compliance with the code will depend on each project, since for many of these the objectives of the project are key to make decisions. Just as something within the scope of a project can be considered small it may under other requirements become something large.
I hope now you have a better idea of what the SOLID principles are and how to apply them. For any questions or comments you can contact me through my social media accounts and if you learned something do not hesitate to share it with your fellow developers and friends, so that as a community we continue to improve and develop high quality and scalable projects.
Also, if you liked this content, you can find even more and keep in touch with me on my social networks:
Top comments (1)
Very, very good article! So much enjoyed. Thanks to the author.