The first 5 principles of Object Oriented Design with Dart.
S.O.L.I.D The first 5 principles of Object Oriented Design with Dart
I’ve found a very good article explaining the S.O.L.I.D. principles, if you are familiar with PHP, you can read the original article here: S.O.L.I.D: The First 5 Principles of Object Oriented Design.
But since I am a Dart developer, I’ve adapted the code examples from the article into Dart.
- Dart helps you craft beautiful, high-quality experiences across all screens, with:
- A client-optimized language
- Rich, powerful frameworks
- Delightful, flexible tooling
S.O.L.I.D is an acronym for the first five object-oriented design(OOD)** principles** by Robert C. Martin, popularly known as Uncle Bob.
These principles, when combined together, make it easy for a programmer to develop software that is easy to maintain and extend. They also make it easy for developers to avoid code smells, easily refactor code, and are also a part of the agile or adaptive software development.
S.O.L.I.D. STANDS FOR:
- S — Single responsibility principle
- O — Open closed principle
- L — Liskov substitution principle
- I — Interface segregation principle
- D — Dependency Inversion principle
# Single-responsibility Principle
A class should have one and only one reason to change, meaning that a class should have only one job.
For example, say we have some shapes and we wanted to sum all the areas of the shapes. Well, this is pretty simple, right?
First, we create our shapes classes and have the constructors set up the required parameters. Next, we move on by creating the AreaCalculator class and then write up our logic, to sum up, the areas of all provided shapes.
To use the AreaCalculator class, we simply instantiate the class and pass in a list of shapes, and display the output at the bottom of the page.
The problem with the output method is that the AreaCalculator handles the logic to output the data. Therefore, what if the user wanted to output the data as json or HTML or something else?
All of that logic would be handled by the AreaCalculator class, this is what SRP frowns against; the AreaCalculatorclass should only sum the areas of provided shapes, it should not care whether the user wants json or HTML.
So, to fix this you can create a SumCalcOutputter class and use this to handle whatever logic you need to handle how the sum areas of all provided shapes are displayed.
The SumCalcOutputter class would work like this:
Now, whatever logic you need to output the data to the user is now handled by the SumCalcOutputter class.
# Open-closed Principle
Objects or entities should be open for extension, but closed for modification.
This simply means that a class should be easily extendable without modifying the class itself. Let’s take a look at the AreaCalculator class, especially it’s sum method.
If we wanted the sum method to be able to sum the areas of more shapes, we would have to add more if/else blocks and that goes against the Open-closed principle.
There is a way we can make this sum method better is to remove the logic to calculate the area of each shape out of the sum method and attach it to the shape’s class.
Now, to calculate the sum of any shape provided should be as simple as:
Now we can create another shape class and pass it in when calculating the sum without breaking our code. However, now another problem arises, how do we know that the object passed into the AreaCalculator is actually a ‘shape’ or if the shape has a method named area?
Coding to an interface is an integral part of S.O.L.I.D, a quick example is we create an interface ( abstract class in dart which gets implemented), that every shape implements:
In our AreaCalculator sum method we can check if the shapes provided are actually instances of the ShapeInterface, otherwise, we throw an exception:
# Liskov substitution principle
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
All this is stating is that every subclass/derived class should be substitutable for their base/parent class.
Still making use of out AreaCalculator class, say we have a VolumeCalculator class that extends the AreaCalculatorclass:
In the SumCalculatorOutputter class:
If we tried to run an example like this:
The program does not squawk, but when we call the HTML method on the $output2 object we get an error informing us of a list to string conversion.
To fix this, instead of returning an array from the VolumeCalculator class sum method, you should simply:
The summed data as a float, double or integer.
# Interface segregation principle
A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.
Still using our shapes example, we know that we also have solid shapes, so since we would also want to calculate the volume of the shape, we can add another contract to the ShapeInterface:
Any shape we create must implement the volume method, but we know that squares are flat shapes and that they do not have volumes, so this interface would force the Square class to implement a method that it has no use of.
ISP says no to this, instead you could create another interface called SolidShapeInterface that has the volume contract and solid shapes like cubes e.t.c can implement this interface:
This is a much better approach, but a pitfall to watch out for is when type-hinting these interfaces, instead of using a ShapeInterface or a SolidShapeInterface.
You can create another interface, maybe ManageShapeInterface, and implement it on both the flat and solid shapes, this way you can easily see that it has a single API for managing the shapes. For example:
Now in AreaCalculator class, we can easily replace the call to the area method with calculate and also check if the object is an instance of the ManageShapeInterface and not the ShapeInterface.
# Dependency inversion principle
The last, but definitely not the least states that:
Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.
This might sound bloated, but it is really easy to understand. This principle allows for decoupling, an example that seems like the best way to explain this principle:
First, the MySQLConnection is the low-level module while the PasswordReminderis high level, but according to the definition of D in S.O.L.I.D. which states that Depend on Abstraction not on concretions, this snippet above violates this principle as the PasswordReminder class is being forced to depend on the MySQLConnection class.
Later if you were to change the database engine, you would also have to edit the PasswordReminder class and thus violates Open-close principle.
The PasswordReminder class should not care what database your application uses, to fix this again we “code to an interface”, since high-level and low-level modules should depend on abstraction, we can create an interface:
The interface has a connect method and the MySQLConnection class implements this interface, also instead of directly type-hinting MySQLConnection class in the constructor of the PasswordReminder, we instead type-hint the interface and no matter the type of database your application uses, the PasswordReminder class can easily connect to the database without any problems and OCP is not violated.
According to the little snippet above, you can now see that both the high-level and low-level modules depend on abstraction.
Honestly, S.O.L.I.D might seem to be a handful at first, but with continuous usage and adherence to its guidelines, it becomes a part of you and your code which can easily be extended, modified, tested, and refactored without any problems.
# Complete code examples