SOLID: What it is and why it matters

11 Jan 2023
Ignacio Gonzalez

Ignacio Gonzalez

See author's bio and posts

Although it has been a few years since I started to work as a Software Developer, it wasn't that long ago that I came to the realisation that I was looking at programming through the wrong prism.

Programming isn't about solving the problem straight away by using perfect form, it is about solving issues in a way that is sustainable.

Sustainable programming allows you to come back if the problem suffers a metamorphosis, or if the requirements change.

Perfection is good, but purely focusing on it isn't. It is better to have respect for yourself and your team by being a good ‘scout’ and leaving the camp cleaner than you found it, when solving a problem.

Putting the Soft into Software

Let's think about the word Software.  The fact that it has the prefix Soft is no coincidence.

Software is basically a program running on a machine that dictates its behaviour.

In this context, this is something that instantly clashes with the concept of hardware, as a person who produces software needs to be able to change it with minimal effort, to be able to adapt to the new way the machine needs to behave, whereas a person who produces hardware does not follow the same process.

This difference is mainly due to the costs of hardware development.

People often get confused with the purpose of software.  They will cite costs and revenue when talking about commissioning software. 

However, Software main’s purpose is not to produce revenue it is to adapt to any new circumstances that may appear.  If it creates revenue, then that is a bonus, but not its primary function.

What is SOLID?

SOLID is an acronym that represents 5 of the most popular principles to create maintainable and flexible code, firstly introduced by Robert C. Martin (aka Uncle Bob).

The 5 principles of SOLID

  1. Single Responsibility Principle: A class should have only one reason to change. This means that a class should have a single, well-defined responsibility, and all of its methods and properties should be related to that responsibility.
  2. Open/Closed Principle: A class should be open for extension but closed for modification. This means that a class should be designed in such a way that it can be easily extended to add new functionality, without needing to modify the existing code.
  3. Liskov Substitution Principle: Derived classes should be substitutable for their base classes. This means that any derived class should be able to be used in place of its base class without breaking the application.
  4. Interface Segregation Principle: Clients should not be forced to depend on methods they do not use. This means that interfaces should be designed to be as small and focused as possible, so that clients only need to implement the methods that they actually need.
  5. Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. This means that classes should depend on abstractions, such as interfaces, rather than depending on concrete implementations of other classes. This makes it easier to change the implementation of a class without breaking the code that depends on it.

From my perspective, SOLID (amongst other things) wants software to be as ‘soft’ as possible by leveraging the 5 principles that would help developers keep the ‘north star’ on sight: being able to create a sustainable environment to design and maintain software.

This is actually more relevant to our profession than you may think.

In professions where the impact of a mistake can have tragic or global consequences, regulations are often produced either on a professional or national scale.  Think of doctors or lawyers.

To err may be human, but preventing those mistakes in the first place is better.

Software developers do not have regulatory bodies, however, this does not stop us from creating rules and guidelines, which are used by us to create sustainable environments and to protect our work from past errors.

Each country and industry that we work in, as software developers, will have different guidelines, rules, regulatory bodies and often laws, yet at the core of our work, the software, we are unregulated.

Personally, I believe that technology is far from being at its peak. New predictive models, new phones and new machines that help people on a daily basis. What would happen if we realised something was wrong in the software design of the airport control system or in a ventilator?

It would be a catastrophe, and who would be the ones to blame? Developers.

It is true that taking these products/solutions into production is regulated and audited. 

But we are basically using our time to create guidelines for that particular product instead of focusing on using our time to devise regulations that would save a lot of time during the inception and building of the mentioned product the right way. Of course, we would need specific tests depending on the industry that our product is tackling, but sustainable software is expected in all the industries nevertheless.

 

This is why, I believe SOLID should be the foundation concept that every developer keeps in mind, as part of those official standards/guidelines.

Understanding and applying SOLID in your work?

So, how do I go about achieving this in my day to day work?  I use the following 'mental pictures'.

Single Responsibility Principle (SRP): states that a unit has only one reason to change. As a note, SRP is sometimes also defined as ‘Units should do one thing only’ but I find this a little bit misleading as even though I think that a unit should do one thing, it is about who deals with the unit and when. By stating that a unit must change because of only one reason, we are making sure that module is only dependent on one trigger/business actor and can be managed independently, without affecting the rest of the units.

Figure 1

Figure 1. SRP comparison

Figure 1 shows an example of how a single unit (left part of the Figure 1) contains different functionality that is dependent on different actors. Leaving aside the validity of the design and the naming in the example, the objective of the picture is to highlight that saving employees into whatever database technology we use concerns a different actor (developers) than calculating their salary (finance department) and booking holidays (HR department). Each of these actors may have different requirements and may need changes in the code for different reasons.

Open/Close Principle (OCP): states that code must be open for extension and closed for modification

The way I picture this in my mind is by thinking that when you use a strategy to solve a problem, the issue does not stop there. You need to design your strategy not only to solve a problem, but to solve it and be flexible to keep solving other problems that may arise. I do not mean implementing ahead of time (YAGNI violation), I mean to implement code knowing that in the future more and new strategies will be needed, so you better place the units in a way that will make this comfortable for developers.

Figure 2

Figure 2. OCP comparison

Left side of Figure 2 shows how the code would be close to extension, as creating another implementation of a shape (let’s say Rectangle class) would imply we need to change code in class AreaCalculator. However, in the right side of Figure 2, adding a class Rectangle would not affect class AreaCalculator. We solved the problem of calculating areas and at the same time, we left the code knowing that we are ready to adapt it to know shapes in the future.

Liskov Substitution Principle (LSP): I found a lot of different ways of describing LSP. ‘Children classes should be able to substitute parent classes’, ‘Clients should be able to use children classes instead of parent classes with no variability in behaviour’, etc. However, the one I took with me is the following one:

Children must commit to parent’s promises

For this one, there is a popular example about a rubber duck:

Imagine you need to implement a system in which different types of ducks are in place. It would not be crazy to reach the conclusion that all ducks are birds. So, following the idea of reusing code, we design an interface called Bird which introduces 3 different new method signatures: eat(), fly() and makeSound().

We could think of an endless amount of duck types and variants, however, one day, your manager asks for a rubber duck to be added…

Problem, we are now pushed to make RubberDuck class implement Bird, but this ‘thing’ does not fly nor eat, it only makes sounds. So now, wherever we go, in the code, we need to make sure that it knows that if we are dealing with a rubber duck, the behaviour needs control, because it does not eat nor fly..

Figure 3

Figure 3. LSP comparison

On the left side of Figure 3, RubberDuck did not commit to its parent promises as it cannot implement two of the methods that were needed. A way of solving this, is by splitting the behaviour in different interfaces and make sure each child is following the necessary behaviour.

Interface Segregation Principle (ISP): In the case of this principle I struggled a lot when comparing it with the previous one, let’s analyse it.

ISP dictates that units should not implement behaviour that they will not use. This principle encourages to split the system in smaller pieces and reuse them smartly instead of gathering functionalities and make children inherit or implement behaviour that they will not use.

Figure 4

Figure 4. ISP comparison

As you can see in Figure 4, the solution we had to use was to segregate the functionalities in different interfaces in order to be able to play with them the way we needed. That also respected the ISP. However, ISP and LSP’s objectives are different. LSP advocates to respect the legacy from parent classes so children can substitute parents without causing disruption. On the other hand, ISP states that there is no reason a unit may have a function/ method, it is not going to use, the reason is simple, it creates confusion.

Dependency Inversion Principle (DIP): It would be wrong to say this is my favourite principle because is like saying ‘I like drinking water more than breathing air’, but I am really fond of this principle because it is the one that allowed me to understand the reason of Object Oriented Programming and its relationship with polymorphism.

DIP states that high and low level modules should depend on abstractions rather than on other concretions. This is done via a strategy called polymorphism.

Figure 5

Figure 5. DIP comparison

Figure 5 shows how DIP works: making UserService depend on a database driver in a direct way means that we are coupled to that database technology as changing it, would mean changing UserService too. On the other hand, if we introduce an abstraction that represents the contract our business rules need to contact a database driver, UserService does not know what type of database we are using, and that means a lot.

The key takeaway of this principle is understanding how Object Oriented programming shines because of a lot of things, but mostly because of the use of polymorphism. Polymorphism makes this principle possible, which means, modules are independent from each other, this level of independence allows different teams/people to work in parallel on different modules and finally, independent deployment of those modules.

I am not going to get deeper into the concept of Clean Architecture as it has way more nuances, but here is a summary that I like to remember because it is linked to the idea we have just discussed: Clean Architecture is Uncle Bob’s idea of how components should be arranged/communicated in a system. The idea is that the inner modules know ABSOLUTELY nothing about outer modules. Higher level modules and higher business policies are enclosed in the inner circles, whereas lower level modules (also known as plugins in the Plugin Architecture) are placed outside and they are details in the system. As Figure 61 shows, a key concept here is the Dependency Rule (Arrows will always point to the inside meaning that the flow goes into the inner layers, where those components know nothing about the outer ones). This is achieved thanks to DIP.

Figure 6

Figure 6. The Clean Architecture

In conclusion, the use of SOLID principles helps produce not only good architecture but efficient architecture. It is also about separating concerns and creating a design that allows sustainable development. Being flexible and agile is not achieved by only focusing on the process dimension of Agile like Scrum, but also working our ‘technical’ muscle via methodologies and principles in the development area (Like Extreme Programming). As a side note, SOLID is a guideline that exists to help us in our paths as developers, sometimes we need to make compromises, while maybe violating some of the principles to protect the integrity of the business. Everything is about balancing trade-offs and advantages.

My name is Nacho González. I previously joined Codurance as Craftsperson in Training (CiT) and after going through the Academy I became a Software Craftsperson. The idea behind this blog post was to try to offer my insights on this topic and help people that are at the same stage I am; learning as much as we can!