Being SOLID in Dart.
Ever since my journey to coding started my only goal was to become a better and better programmer day by day, But during the time I found myself becoming good at problem-solving but lacked the quality of Software Development I didn’t know what but something extremely important was missing from my vision. I was in search of something Solid but what? …
And the Day arrived and my mentor/friend introduced me to the concept of SOLID Principles of Software along with Design Patterns (Our only focus, for now, is SOLID Principles), and I thought let's give this a try and my search was over. I have found what I was really looking for.
So why SOLID is really a big deal in Development well, in this case, I recall my early days of coding where many time even for small changes need to be made in code I need to face devastating consequences as I need to go to different usage to refactor my code to make it work, everything was ok for me at point of time but I really had an instinct as this is a bad practice and my code didn’t scale and even was not maintainable at as a change needed to be made many parts of code turned to be scrap code.
So what’s the role of SOLID???
Well SOLID makes out code flexible, maintainable, reusable this helps to avoid a large part of our code suddenly turned into scrap code.
This SOLID actually comprises 5 individual principles defined by each letter of the word SOLID.
1. SRP — Single Responsibility principle
The class should only change for a single reason.
Ok Now, what this really means. Let’s have a look.
Seeing our User class we know that the User class should only be responsible for handling user details but here it does a lot more than that, we also have imposed the user authentication with validator for email.
Does this stand strong in favor of the Single Responsibility Principle?
Does this implementation have a single reason to change in the future?
No, Acc to this principle the best case must be to make separate classes for user details, user authentication, and validators.
So now let’s refactor to fit the principle.
After this, we have 2 classes with one mixin in the dart, Now as we can look each and every chunk of code is responsible for individual change and their responsibility is isolated so it becomes easy to maintain and extend User details, UserAuth, and Validators in future if demand arises.
with this let’s move to the next principle.
2. OCP — Open Closed Principle
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
This aims for extending its behavior but avoids any modification in source code.
Let’s start with an example.
Here class rectangle represents a shape and the only responsibility of our area class is to print the calculated area by rectangle class.
Till now, everything looks cool but let’s say there is a need to add a new shape to our code say Square and Area class should work with both shapes, well in this case our code gets modified too.
Ok, this was some messy code as calculateArea() in Area class was implemented using an if-else_if-else and this if-else ladder would grow as every time a new shape is added to our source code and here we have violated the open-closed principle as we needed to modify our Area class.
So now, let’s implement this by governing the principle of open-Closed.
Ok now, we implemented our Shape as an abstract class, and every time we add a new Shape class to our code our Area class is not modified i.e we are open to adding new shapes by implementing Shape but closed to modify our Area class.
So moving to our next principle.
3. LSP — Liskov’s Substitution Principle
This principle stands for the statement that a child class should be able to replace/substitute its parent class.
This means, If B is the subclass of A then B should be able to replace the object of A in any use-case e.g A passed as a parameter in a method so even if we pass the B (which is A’s child class) in replacement of A the method and its behavior should not break giving a weird output or throw errors).
So let’s walk through the code.
We know a Square is a rectangle with width = height, But as we extend our Square class to a rectangle we need to change the definition of calculateArea() for square. According to Liskov’s substitution principle child class should be able to replace parent class without breaking the behavior of the implementation but here when we replace the Rectangle object with the child Square object in the third assignment and we see a weird result.
Rectangle squr = Square(8, 5);print(squr.calculateArea()); -- OUTPUT = 64 [ Expected OUTPUT -- 40 ]
Ok, we have broken the principle because as we expect a Rectangle with a result of 40 and also Square class should be accepting a single parameter of side.
No matter what we assign to Shape be Rectangle or Square we can see that it maintains its behavior.
Now moving to the next principle.
4. ISP — Interface Segregation Principle
Don’t impose methods on clients if they are of no use, simple and straight If something is useless for me I should not be forced to keep it.
So let’s see what the problem is?
A Rectangle is a 2 Dimensional Shape and Cube is 3 Dimensional Shape Rectangle that has Area but as it is a 2-dimensional shape surface area and volume is useless for its implementation similarly Cube is 3 dimensional so the area is useless for its implementation and since both of the class implements from the shape abstract class, just to make compiler happy we have imposed these methods which are irrelevant to them.
So to avoid this instead of creating one generic Interface to be implemented on all clients we should create many client-specific interfaces. — extract of Interface Segregation Principle.
So let’s refactor our code by keeping ISP in mind.
Removing the single generic class we have separated this into two client-specific abstract classes and freed the clients from implementing usable methods.
Now finally moving to our last principle.
5. DIP — Dependency Inversion Principle
- High-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
This principle is all about the loose coupling of your classes making them more localized and connecting them with abstract class. It’s better to connect the high-level class to the low level via abstract class. Due to this, we will encounter less work to make changes in either of the classes when required, affecting fewer code blocks.
and further for the second part.
As details should depend upon the abstraction this helps the abstraction to not get affected if the details are changed.
This principle really helps to maintain our code for future modification at a lower hectic.
Ok now let’s see what actually it helps in.
Okay, what’s wrong with this implementation actually nothing but the only thing is that we have declared using a concrete class and both the classes are tightly coupled with each other that is our UserBloc higher class is depends on the lower class of NetworkCalls which violets the DIP.
Now let’s implement this using DIP in mind.
Now we have loosely coupled our UserBloc higher level class to lower-level class NetworkCalls via NetworkTask abstract class and the dependency of Network Task is passed as a parameter while instantiating the UserBloc in the first place.
Here UserBloc depends on abstraction rather than concrete class for its implementation.
Start implementing things in your personal projects to get a real hand practice because it takes some time to take a good grasp on this topic.
So, at last, there is no strong and concrete rule to apply this pattern the only thing this does is make our life easy when the point comes to maintain and reuse our code and gives us a feeling of being a PRO developer. So start your first step towards being a Pro Dev and if you are already, do share this with the passionate beings who want to take their first step in this journey.