Skip to main content

What is SOLID design principles?

The short answer is, the bricks in the wall! ðŸ˜Š

Let's check it via an example

Imagine, we are participating in a bigger project with multiple developers for a long time. We have just started the project from zero without any common agreements on how to build up the project. There are no best practices on how to develop the project, we all just apply the changes ad-hoc, as it is. The goal is to finish the project fast!

...time passes by...

We have already spent quite a lot of time on the project next to the other developers, and we are starting to think it is getting harder and more complicated to implement new features in the app -> it is challenging to figure out which code to modify, what is safe to be extended. Things get even more complicated when requirements change at the very last minute, and the project's code is not dynamic enough to handle these unexpected changes in an acceptable time. And we have these many new annoying, unexpected bugs! ..and this list could go further on and on..

Let's visualize what's happening in our project:


It seems bad, we have to spend more and more effort keeping the project alive and we have fewer and fewer new features. Our users will be very sad when we can not deliver the features promised :(

We would like to have something like the following:


If we would spend more effort on understanding the whole project with its problems, then if we would spend more effort on planning and setting up the basics of the project, then today it could be more convenient, fast, and safe to add new features.
The reason is simple, everyone tries to use the same (or similar) ruleset, there are fewer easter eggs.

Let's see what can we do to improve our situation

We will try to apply the SOLID principles!

Single responsibility

- We have no idea what to modify to add the new features!
- We do not know what component/class does what! OMG, Are there side effects and hypotheses in the implementations?

Every class should have one responsibility. Your classes should try to solve only one problem.

If you do not know how to define and fix one single problem, then I suggest you start writing summary comments on the top of your classes and write down what you are going to solve with that class - what is the responsibility of that class.
You can also apply this principle on both lower and higher levels: you can apply it to methods or sub-systems.

Let's take an example. We are working on a system of a school, and our task is to implement exam-related features and here is the result:


Looks strange, right? Both professor and student are the same Person in the system. Both can AnserExam and VerifyAnswers. It must be very hard to distinguish which feature should work in which way inside of this class.

Let's clean this up a little bit:

What do we see here, the Person is still there? -> Yes, but it defines the common attributes and behaviors of the Professor and Student in our system. Both of them have names and phone numbers, they also need to get some kind of TrainingResult. The prof needs the result of his class (e.g.: avg of student results), while the student needs his single result of the training (e.g.: to check, whether he passed the exams).

Open-closed principle

- Why do we need to modify so much code to add this simple function?
- Look, I just simply modified this member of this class in its child, and now nothing works! Why can't I modify it, why does it break down the feature(s)? How should I follow (and figure out) the rules of this variable/parameter, who knows it?

Software entities should be open for extension but closed for modification. 

Let's check our earlier example with a little plus information. In our school's system, everyone is registered, everyone has a TrainingResult. If someone has no need for TrainingResult, then this behavior will just notify us with a message 'Person does not have training result!'. Let's apply this default logic to those, who are not Professors, nor Students.

So, every Person in our school's system can get the TrainingResult. However, there are two unique roles in our school, one is the Professor, who is interested in his class' results and the other one is the Student, who is interested only in his training's result.
Therefore, in the project's code, we just simply defined two new specific classes with extending the original class, without modifying the old (default) logic. Later these two new specific classes got some more specific behaviors and attributes, and those do not exist on the parent, because they must not be mixed together.

Liskov substitution principle

- Look, I just broke the app by simply replacing this service with one of its more specific children!

Subclasses should be substitutable for their base classes.

In our school's system, there is a KitchenService. The KitchenService works for money. They give the Person of the school food in return for money. The KitchenService will ask for less money for the food from the Students and Professors, because when the KitchenService gives the food to the Person to eat, then the Professors will eat separately, at a very fancy table, while for example, an average person will eat in the crowd with the Students.
The KitchenService does not really care which Person is who in the School's System because they will find their table to have lunch at. The KitchenService will just tell them '..here is your food, now Eat..' :) 


Interface segregation principle

- Why is there one huge interface? What is the additional information it tells me?
- Why do we need to implement so many interfaces?

Many client-specific interfaces are better than one general-purpose interface.

Let's go back to the Single responsibility principle's example. We used to have one huge class that we split into three. And now let's add one big interface for all properties and behaviors. We have the IPerson and we implement it on all the Person, the Professor and Student classes:


It's already smelly, isn't it? Due to placing every behavior and property to the same interface, and due to implementing it on many classes with different responsibilities, we are forced to add features to classes/systems that we do not want to. For example, we have to define the PrepareExamQuestions in the Student class, even when it will never happen they will prepare an exam for each other.

Let's clean this up a little bit:


As you can see, we split up the IPerson interface into multiple interfaces. From the new structure, we can clearly see, everyone in the School's System is IHungry (has the ability to eat something). It will be extended by the IPerson, and the IPerson is extended further to IProfessor and Istudent. All of these interfaces will be implemented by a class and those classes are not forced to implement something that should not be there.

Dependency inversion principle

- Why do we need to modify so much code to add a simple function?

Depend upon abstractions, (not) concretions.

In the previous examples, we saw, we have a KitchenService and it can handle the payment and the preparation of the food. Once the SchoolManager decided, to change it to a more fancy one, because he found a bug in his soup.

Let's check the KitchenService whether it is possible to change them to a new one:


Oh-oh, there is no option to inject the new KitchenService into the SchoolManager. The OperateKitchen does not have any parameters, and there is no open constructor, also there is are no properties -> there is no way to change the current KitchenService.

Let's see how can we fix it:


We have defined a new interface, the IKitchenService. We have also added the option to inject the current IKitchenService via the constructor of the SchoolManager. With this step, we can easily change from the buggy KitchenService to the FancyKitchenService.

Useful links

  1. Wikipedia - SOLID
  2. Microsoft - What is SOLID?
  3. Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)
  4. Some more design principles and design patterns: Design Principles and Design Patterns
You will find many awesome and more sophisticated articles about this topic; here I have tried to introduce it from a different perspective, using simple kitchen language.

Comments