SOLID Software Design

--

Learn how to apply the SOLID principles and save the day with a maintainable code.

As software engineers, we spend 90% of the time reading code 📚 .

And a part of being a software engineer is to maintain some old projects developed by others, fix bugs, and update some features …

But sometimes when you look at the code 😵 you will ask what is this?

A tightly coupled code 😬.

Fortunately, there is a way to prevent this from happening😉.

The SOLID principles 🤩, but what are SOLID principles?

What are SOLID principles?

SOLID is an acronym for five computer programming principles intended to make software designs more maintainable and avoid refactoring and code rot.

SOLID stands for:

S: Single-responsibility principle

O: Open–closed principle

L: Liskov substitution principle

I: Interface segregation principle

D: Dependency inversion principle

Why we need SOLID principles?

Let’s assume that we have this application :

Now let’s imagine that we need to update the Reporting module.

And to do that, we need to update the Order and the Documents modules.

This will lead us to two things: code fragility and code rigidity.

Code fragility: when we change a block of code on a module that results in bugs on other modules.

Code rigidity: meaning the code is difficult to change.

To fix those issues, we must adopt the SOLID principles.

The Single-responsibility principle :

The SRP principle states that every module, class, or method should have one reason to change.

A class should have only one reason to change

by Robert C. Martin

To understand the principle, let’s look at this example :

You notice that we have multiple responsibilities and multiple reasons to change. Inside the sendNotificationmethod, we have two logic:

the first for sending email notifications and the second for sending SMS notifications.

Let’s identify some reasons to change for this class :

  • when we need to change the SMS provider,
  • when we need to change email logic,
  • when we need to update the Clientclass example, we change the phoneattribute to phoneNumber

To fix that, we must create separated classes of each responsibility:

You notice we create separated classes to deals with SMS and Email and we used them inside the NotificationSender class.

This way the code is easy to maintain and test.

The Open–closed principle :

The OCP states that classes and functions should be open for extension, but closed for modification.

Open for extension means that an entity should be extendable to behave in a new way.

Closed for modification means that we can add new features without changing the existing code.

Let’s imagine that our application looks like this :

Now if we change the ClassA that will break ClassB , ClassC ,ClassD

You got the idea, 🤓.

A better solution is to create a new class that holds the new features :

Let’s imagine that we have this class:

This is a simple Tax class that calculates the tax for full-time employees.

After some time, the client tells us to update this implementation to deals with interns (no taxes) and part-time employees (they don’t have the insurance tax) 😅.

Let’s implement these new features without the OCP:

You notice we change the source code of the Tax class, imagine we have new types of employees we will update this class every time, and then we will deal with plenty of bugs and issues because the Tax class used in other classes this means that we have a high risk to break the app.

Of course, we have a better way 😉 to use the open-closed principle.

The easiest way to apply the OCP is to use inheritance :

The inheritance is a good way to apply the OCP; we don’t change the source code of the Shape class. But we introduced coupling between it and the Rectangle class. In a real example, you will use inheritance with concrete classes that have many methods, and sometimes we don’t need this coupling.

What I loved about software engineering is that always there is a better way, 🤗 .

With the Strategy pattern, we use interfaces instead of inheritance :

Now let’s apply the OCP on the tax example :

We start by creating an interface TaxCalculator with the calculate method.

Then we create a FullTimeEmployeeTax class that implements the TaxCalculator interface and we define the calculate method.

We did the same thing as before. Notice that we define an implementation of calculate method in each class and now we have a nice clean class that focuses only on calculating tax for a specific employee.

The Liskov substitution principle :

The LSP states if class B is a subclass of A, then class B can be replaced with class A without altering or damaging the program. If you find this definition a little abstract, 🥴 don’t worry, I will explain it in more detail.

Let’s look at this example :

We have a ClassB extends the ClassA if we respect the LSP, then the ClassB can be substitutable with the ClassA.

ClassA classTest= new ClassB(); 

The best way to understand the LSP is by providing violated examples and then fixing them.

In mathematics, a Square is a Rectangle. Then let’s look at this implementation:

We have a class Rectangle that can calculate the area and we create another class Square that extends the Rectangle class.

Rectangle shape = new Square();
shape.setWidth(50);
shape.setHeight(30);
shape.calculateArea(); // will retrun 900

You notice that there is something odd, we can set the height and width of the square. That hardens the precondition,🤯.

As a result, we conclude the class Square is not fully substitutable with the class Rectangle .

The solution is to separate the two classes and break the wrong relationship.

Let’s see another example that violates the LSP with the partially implemented interface:

You notice the ClientAccount is not fully implement the BanckAccount interface 😟.

Now to fix this we should separate the logic into two different interfaces :

And now we respect the LSP 😁.

The LSP is all about relationships and we need to ask this question before designing our classes class Y is fully substitutable with class X?

I want to mention that incorrect relationships between classes lead to bugs and side effects.

The Interface segregation principle :

The ISP states we should not force clients to depend on methods it does not use, instead, we split large interfaces into smaller ones.

I want to mention that this principle applies to abstract classes too.

The ISP reinforces other SOLID principles. If we keep interfaces small, then the classes that implement this interface have a high chance of substituting it (the LSP) and classes are more focused on one purpose (the SRP).

Now, before we apply the ISP, let’s see some violation of this principle :

Fat interfaces are interfaces with many methods. Then, we may have classes that do not fully support the interface, and this violates the ISP and violates the LSP.

Also, interfaces with low cohesion violate the ISP.

The ISP is very easy to implement all you need to do is to split the huge interfaces into smaller ones.

Dependency inversion principle :

The DIP states that High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

I think you need some clarification about what are High-level modules, low-level modules, and abstraction?

Let’s look at this system :

High-level modules are the business logic or the features for our application and they tell us what the application should do.

Low-level modules are the sub-modules that help the higher ones to achieve their goals.

If you look at the example above, you see the Payment module is a high-level module but to perform the payment we need other sub-modules (notification, networking…) they called low-level modules.

You can think that a high-level module orchestrates low-level modules to achieve the application's needs.

Abstraction means something is not concrete. In programming languages, we can refer to that by interfaces and abstract classes.

The best example to understand the DIP is a repository :

In our case, the UserManagement is the high-level module and SqlUserRepository is the low-level module. The issue with this implementation that the UserManagement has a direct dependency on the SqlUserRepository and that violates the DIP.

To fix this, we should do something like this :

And now, we can call the UserRepository interface inside the isVerified method instead of the SqlUserRepository .

After applying the DIP, you notice that the SqlUserRepository (low-level module) and the UserManagement (high-level module) depend on UserRepository (abstraction).

Conclusion :

Coupling is the main reason for killing software and leads to code fragility and code rigidity. SOLID principles help us create a design that can evolve and grow. And you can think of them as the foundation of clean software design.

If you enjoyed this article, please clap it up 👏 and share it so that others can find it! Follow me to get more of me 😄.

Join FAUN: Website 💻|Podcast 🎙️|Twitter 🐦|Facebook 👥|Instagram 📷|Facebook Group 🗣️|Linkedin Group 💬| Slack 📱|Cloud Native News 📰|More.

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇

--

--