S.O.L.I.D principles in Dart

S.O.L.I.D It’s an acronym for 5 design principles for writing code which is maintainable, scalable and easier to understand.

Alba Torres
6 min readFeb 28, 2023

The term SOLID is an acronym for five famous design principles, Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, which we will examine below.

Hopefully after this article we all have a more clear understanding of each of these concepts.

Single Responsibility Principle

This principle states that a class should only have a single reason to change. In other words, you should create classes with a single “responsibility” so that they’re easier to maintain and harder to break. If a Class has many responsibilities, it increases the possibility of bugs.

Goal:

This principle aims to separate behaviours so that if bugs happen as a result of our change, this won’t affect other unrelated behaviours.

Example:

Bad practise:

In this example we have a class named “shapes” that handles 3 different methods: to calculate the area, to paint the shape and to get a request.

This is breaking the Single Responsibility Principle due to the fact that this class has 3 reasons to change.

Good practise:

A way to change this would be to have 3 different classes (shapeAreaCalculator, ShapePainter and ShapesOnline) where in each of them we “do something”, and then we can call them in the abstract class.

This way, if we want to change something in calculation, we do it in calculation class instead of the shape class, as we did in the last example.

Open/Closed Principle:

This principle states that a class should be open for extension but close for modification.

If you want the Class to perform more functions, the ideal approach is to add to the functions that already exist NOT change them.

So what this principle wants to say is: that we should be able to add new functionality without touching the existing code for the class. And this is because whenever we modify the existing code, we are taking the risk of creating bugs.

Goal:

The goal is to extend a Class’s behavior without changing the existing behavior of that Class with the help of interfaces and abstract classes.

Example:

Bad practise:

In this example we have different shapes and we want to calculate the area of each of them. Both Rectangle and Circle respect the SRP as they only have a single responsibility. The problem is inside AreaCalculator, because if we added other shapes, we would have to add more if conditions.

Good practise:

Thanks to the interface, now we have the possibility to add or remove as many classes as we want without changing AreaCalculator. For example, if we added class Square implements Area it would automatically be “compatible” with the double calculate(…) method.

Liskov Substitution Principle

When a child Class cannot perform the same actions as its parent Class, this can cause bugs. The child Class should be able to do everything the parent Class can do. This process is called Inheritance.

The Liskov Substitution Principle suggests that a child class must guarantee the “usage conditions” of its parent class plus more implementations it wants to add.

The picture shows that the parent Class delivers Coffee(which could be any type of coffee). So it is acceptable for the child Class to deliver Cappucino because it is a specific type of Coffee, but it is NOT acceptable to deliver Water.

Goal:

Of this principle is to enforce consistency so that the parent Class or the child Class can be used in the same ways without any errors.

Example:

Bad practise:

Here we have two shapes and we want to get the area of them.

We have a big logic problem here; a square must have 4 sides with the same length but the rectangle doesn’t have this restriction.

At this point the area will return 4 time 8 which is 32 which is wrong! Should be 8 times 8 which is 64.

We should not extend square from rectangle because it is not interchangeable. This principle says that we can extend child class and will do the same as parent class, which in this case it is not true.

Good practise:

We basically create a class that we call FourSidedShape and we set the parameters we will need. Then each class (square and rectangle) can extend from it and make the different calculations needed to get the Area of each of them.

In the rectangle case we will set left and right as Height and top and bottom and width, and in Square example we will set the size with setSize function.

Interface Segregation Principle:

Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.

The principle states that many client-specific interfaces are better than one general-purpose interface.

What this means is that: you should create small interfaces with minimal methods. For example, it’s better to have 8 interfaces with 1 method instead of 1 interface with 8 methods.

Goal:

This principle aims at splitting a set of actions into smaller sets so that a Class executes ONLY the set of actions it requires.

In this photo we can not really see it properly but the robot has no antennas and cant execute the action on the left, but on the right image we see the specification “robots that can move the antenas” -> should move the antenas.

Example:

Bad practise:

In this example we have an abstract class that we call worker and has two functions: work and sleep.

This example would work for the class humans because when extended to workers class, it executes both functions: work and sleep.

But in the case of robots it is a bad practice because Robots don’t need to sleep and when extended to the abstract class, the method called “sleep” will be useless. but it still needs to be there otherwise the code won’t compile. To solve this, let’s just split Worker into multiple interfaces:

Good practise:

This approach is better because there are no useless methods and we’re free to decide which behaviors should the classes implement.

Dependency Inversion Principle:

DIP states that we should code against abstractions and not implementations. So extending an abstract class or implementing an interface is good but descending from a concrete class with no abstract methods is bad.

Dependency injection (DI) is a very famous way to implement the DIP.

The idea is that we isolate our class behind a boundary formed by the abstractions it depends on. If all the details behind those abstractions change, then our class is still safe.

Goal:

This principle aims at reducing the dependency of a high-level Class on the low-level Class by introducing an interface.

Example:

Bad practise:

Let’s see this example.

In this case what we are trying is that for each Algorithm we want to do a specific thing to secure the file. So for AlgoES we will do ABC() and for AlgoRSA we will do DEF();

This is a bad practice because we are not using Dependency Injection. This has dependency on the concrete class and for each algorithm we would need to change the secureFile() function.

Good practise:

In this case, we create an interface class where we call encrypt(), a concrete class where we set the specific method for each algorithms to secure the file and with dependency injection we call secureFile easily.

This is a short explanation of the S.O.L.I.D principles and I hope you enjoyed it.

Please Clap if you learned something new today .Thanks in advance.

For any queries feel free to reach out to me .

--

--