What is SOLID and Why You Should Care About It

CAIO WANDERLEY
12 min readMay 26, 2022

--

Introduction

In the past few months I’ve been studying about clean code, good practices and how to improve my coding skill, not only in the technical part (frameworks and languages) but also the theory, like algorithms, data structures, design patterns and the subject of this post, the SOLID principles.

In my journey I’ve learned to value well-written code and I would like to share that same view in this post and help you to improve your coding skills too.

Why is it important?

Have you ever written a program that after some time implement a new feature turned to be so difficult that anything you change could affect all the program? Well I need to say that this is the consequence of a bad code written without the SOLID principles

Good Code = Changeable

Bad Code = Concrete

Don’t feel bad if you are writing bad code, it’s actually pretty normal, some things like ending deadlines, client pressure or even The Broken Window Theory are factors that contribute to it and even the best programmers sometimes do this. What you need to be aware of, it’s that writing a solution with bad code isn’t really writing a solution or completing your deadline, you’re just delaying the inevitable, breaking the program.

If you write bad code just to complete an specific deadline be aware that the next one will be even more difficult to complete

What is it?

So what is SOLID anyway? The acronym extends for the five Object Oriented Design (OOD) principles by Robert C. Martin (also known as Uncle Bob) and it’s important for the coding of good and maintainable code.

Those principles help the programmer write cleaner code, separating responsibilities, decreasing couplings, facilitating refactoring and encouraging code reuse, telling us how to arrange our functions and data structures into classes.

Some improvements that you can achieve with SOLID are

  • Make the code more understandable, clear and concise;
  • Make the code more flexible and tolerant to changes;
  • Increase the code adherence to OO principles;

At the end of this article you will be aware of each principle and how they can improve your code and help you become a better programmer.

Single Responsibility Principle (SRP)

The concept of this principle is that the class should be responsible for one and only one thing or as Robert C. Martin would say

A class should have only one reason to change.”

The structure (class or method) must have a clear and concise task. This centralizes responsibilities and change points in central and easy-to-identify places, making code maintenance much simpler. The thought you should always think is “What part of my code could change after some time?”, the part that belongs to this thought must have your own function or class.

Let’s look at an example of code that doesn’t apply this principle

Right away we can notice that something isn’t right and really isn’t. The code above is being responsible for check the maximum speed, check what is the action and finally execute it accordingly.

The above approach bring a lot of downsides, for example, and if we needed to change the max_speed check part of the code? We would need to change two blocks of code for it. Or if a new action needs to be added? One more if block? Doesn’t seems right.

To improve this code we must separate each block in methods. Let’s start with the vehicle movements.

We just improved a lot of our code, now each method has your responsibility, but is still missing one change, the speed check block, this block should have your own method which will be used inside the forward and the backward methods.

Let’s imagine that in the future this check speed functionality needs to be changed, add more lines of code for example, if this part isn’t in one method a part then we would need to change two methods (forward, backward) just to add the same functionality.

So let’s make this improvement and finish our refactoring for this principle.

Now any modification in check speed can be made inside check_maximum_speed and the change will be available for any other method using it. With this our code it’s ready for production (for now).

Summary

  • Separation of responsibility: Each method is responsible for only one thing, so if a new action appears we can just create a new method.
  • Shared implementation of check_speed: This way any modification that may be necessary can be coded only once in this method

Open Closed Principle (OCP)

This principle is based on the fact that every piece of software, at least that ones which is in production, will suffer extensions, new features will be added all the time. Knowing that, we need to write our code in a way that this extensions should be easy to make.

Objects or entities should be open for extension but closed for modification.

What this statement stands for is that instead of changing a class to add a new feature we should create a new one that will have this feature for us, that is, instead of modify existing classes we extend our code to have new ones.

Let’s take the code below as an example of bad implementation of this principle.

We could just place the attribute vehicle_type inside the print like so: print(”The {self.model} {self.vehicle_type} ...") , but for the sake of explanation we are doing this way.

The thing is that we have a vast quantity of condition structures (if and elif) and it doesn’t stop there. Let’s imagine that now we want to add a new type of vehicle, a boat for example, this would require a new elif block, and if we feel the need of a plane or an helicopter? Can you see the problem here? The code is getting huge and without any separation.

Imagine that instead of a print statement for each vehicle type we had hundreds of lines of code to make it works, and now we have the need to change the helicopter vehicle? Would be incredibly hard to find and test the code turning the code almost impossible to maintain over time.

Now that we know why the above code it’s bad, let’s make it better. First of all let’s create some class that will dictate the methods that should be implemented in each class, in python we call it protocols.

Now let’s create the classes. They must inherit from the protocol and implement the required methods.

Now our software is way cleaner and maintainable, each class is responsible for one type of vehicle and if we feel the need to add one more we just need to create a new class and inherit from the protocol. This way the code is easier to test and debug, increasing the maintainability of the software and the productivity of the team.

Summary

  • Separation of concerns: Now each class is responsible for one type of vehicle, increasing maintainability and facilitating tests
  • Facilitation in code extension: Now to create a new vehicle we just need to inherit the Vehicle protocol and implement the methods

Liskov Substitution Principle (LSP)

This principle states:

Let q(x) be a property 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

This statement says that every subclass or derived class should be replaceable for their base or parent class. For example let’s imagine that our application needs to implement a Gas station to fill the vehicles. Let’s see a way that we could do this.

First let’s add two new variables in our Vehicle protocol, this way all the classes that implement this protocol will be forced to implement these variables. They are necessary for the implementation of the gas station functionality.

Now that all our classes knows about the variables we can set them in each of the constructors like so:

Okay, now that is all set let’s move to the gas station class. For this class we are going to have two functions, a private one called _calculate_bill(price, quantity), which is responsible for the total price calculation, and a public one called fill_in(vehicle, _type, quantity) that is going to fill the vehicle tank and call the _calculate_bill function to get the bill. Here is the code for this class:

Now if we test the code above with the client below we are going to get the desired results!

Output:

Alright!! All seems good and really is, but months have passed and now the team find themselves in the need to implement electric vehicles in the application. So let’s do it.

First let’s separate the things a little bit. We are going to create two new protocols, one for the electric vehicles and another for gasoline vehicles.

For our GasVehicle protocol all we had to do is to pass the two variables that we have created earlier and remove them from the Vehicle protocol. Now let’s create the ElectricVehicle protocol.

For this one we created two new variables(battery_capacity,battery_level) that will have the very same purpose of the fuel_capacity and fuel_level variables of the gasoline vehicles. They have a different name since electric vehicles doesn’t use fuel to move, they use energy and batteries to store it.

Now let’s replace the inheritance and create a new electric vehicle.

Here we inherited the ElectricCar from Car because both shares the same methods differing only in the variables. And finally let’s add a new type of “fuel” to the GasStation price_table variable.

Now let’s change our client a little bit to add the electric vehicle and test it.

Output:

Ops we got an error! Actually it’s not a surprise since the electric vehicle doesn’t have the fuel_capacity and fuel_level variables. That’s why this implementation goes against the LSP, the ElectricVehicle class is inherited from the Car class but still it can't be its replacement. Let's fix this.

The GasStation needs a way to get all the necessary information from the vehicle and also needs to change it to fill the battery/tank without being so coupled to the variable names.

So let’s make some improvements. Since the best way to access these variables is through the vehicle itself, let’s get rid from the GasStation all lines that needs to access them and pass it to the vehicle protocols.

First let’s add two new methods inside the protocols. The first method that we are going to add is going to be responsible for the calculation of the quantity necessary to fulfill the vehicle while the second one will actually fill the tank/battery of the vehicle

The last thing we have left is to change the GasStation class to use those methods

And now the output that we receive is this:

We still have a little detail to fix. Yes, the code is working, but if you notice in the Vehicle protocol you will realize that we don’t dictate that the classes should implement these new methods. If we run a mypy in our code we are going to receive two errors:

And that’s a true but easy to fix error, we just need to add both methods in our protocol

And the improvements are complete!!

Summary

  • Now the ElectricCar is replaceable for the Car class and vice-versa and the same replaceable characteristic can be achieved with any other vehicle.
  • The GasStation is no longer coupled to the vehicles anymore

Interface Segregation Principle (ISP)

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.

What if a new vehicle is needed? One that flies? Could we insert it in the code as it is? Well let’s see. Take the bellow code and see what I think is the most common thinking for a programmer who does not use the ISP principle.

We have a couple of problems with that implementation.

  1. It doesn’t work: The problem is that the Plane class doesn’t have the brake and backward methods, which is required by the Vehicle protocol, and because of that it can’t be instantiated (it will raise an error).
  2. Missing methods into the protocol: The methods fly and land are general methods that every flying vehicle must have, not only the airplane, this means that it should be in a protocol as a way to standardize those type of classes

A way to fix the instantiation issue would be to implement the method brake and the method backward in the Plane class and just throw an error if the client tries to use it.

Well now it’s working, so technically the problem is solved right? Not for this principle.

According to the Interface Segregation Principle the client (Plane class) shouldn’t be forced to implement those methods, after all it will never use them.

For the sake of this article I am supposing that flying vehicles are only being used in the sky and, because of this, they can’t brake or go backwards

A better approach would be to create two new protocols, one for road vehicles and another for flying vehicles.

And for each vehicle we inherit the right protocol and implement the mandatory methods, like so:

Here the classes inherit from two protocols. For the python interpreter the only one that it matters it’s the second one, but if we run mypy ISP.py we are going to receive a typing error in the case that the requirements of the first aren’t met.

Perfect! Now all the classes just need to inherit methods that they will use and none of them are depending of dictator interfaces that force them to implement something that they won’t need. This way we have a more organized code and separated by use cases.

Summary:

  • Created two new protocols: Was created the RoadVehicle and FlyVehicle protocols for the sake of decoupling the flying and road vehicles inside the code.
  • Implemented protocols: After created, the protocols were implemented in each concrete vehicle class

Dependency Inversion Principle (DIP)

Entities must depend on abstractions, not on concretions.

To explain this principle let’s create a driver for our cars objects because now Uber is in our customer portfolio and want us to implement this feature. So how we could do this is creating a Driver class.

Our class will receive the car object at the time of it’s creation and have a method that will receive a route and drive through it. Now let’s implement the functionality.

Here we are walking through each route instruction and getting the respective method name in the Car class and calling it with getattr function.

This implementation has a problem. The Driver class depends on a concrete implementation (Car) which goes against the DIP principle since this principle states that a class should only depends on abstractions and not concretions. But why is this a problem? Let’s take a look on this client code to explain the reason.

If you run this client it will work as expected, but this only happens because python isn’t a typed language or has a typing compiler as typescript for example. The problem raises when we use mypy DIP.py to check the types:

As you can see mypy it’s complaining about the type of the variable carbeing the concrete class Car instead of RoadVehicle.

To fix this code and to follow the DIP principle we need to replace the Car for it’s abstraction RoadVehicle.

With this code we could pass any vehicle that implements the RoadVehicle protocol without any complaints from mypy.

We could also implement the ISP in this code, all we would need to do is create a Driver protocol which require a drive method and create a FlyDriver class to drive flying vehicles like a plane or a helicopter. This class would inherit from the protocol just like the RoadDriver (the old Driver class) and has your own instructions_to_methods variable.

Summary:

  • Implemented a drive functionality: Implemented a Driver class to handle the drive feature.
  • Modified the Driver to meet the principle: Changed the dependency of the vehicle variable to an abstract class, the RoadVehicle protocol.

Conclusion

In this post we talked about the importance of these principles and how they work to improve your code.

This principles will help you to increase the life time of your next projects and turn you in a better programmer. Remember that any one can create a solution to a problem, but a solution that will remain years without being rewrite and easy to maintain it’s one of the things that makes you a good programmer.

Because of this you need always try your best to write good and maintainable code and if you just can’t do it because of whatever reason we have refactoring techniques for that 😎.

Reading List

Books:

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Clean Code
  • Clean Architecture
  • Refactoring: Improving the Design of Existing Code

Sites:

Bibliography

https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

https://www.treinaweb.com.br/blog/principios-solid-single-responsability-principle

https://en.wikipedia.org/wiki/Broken_windows_theory

https://refactoring.guru/refactoring/techniques

--

--