Coding the SOLID Principles
By: Liat Kompas
Tags: Clean Code, Solid, Solid Principles, Programming

More than once I have found myself struggling with my code to fix one tiny bug or extend some feature. Then, I learned the SOLID principles.

Using these principles helps me write smaller pieces of code, with a single responsibility for each, and without unnecessary if-else statements. My code now is loosely coupled, and it helps me keep my code simple, maintainable, readable, testable, and easily extended.

Photo by Emile Perron on Unsplash

SOLID principles are object-oriented design principles introduced by Robert C. Martin (Uncle Bob) in his paper “Design Principles and Design Patterns”, but they were named SOLID later by Michael Feathers.

The principles are:

Single Responsibility Principle

Open/Closed Principle

Liskov Substitution Principle

Interface Segregation Principle

Dependency Inversion Principle

Let’s dive deeper and understand what each principle is.

Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

Every function or class should have only one job to do.

As Uncle Bob defines it later, “This principle is about people”. Indeed, talking about people instead of classes and functions makes it easier to understand. For example, everyone in my family has their own responsibilities, usually, it is what they do best. My responsibility is to do the laundry, but (luckily) not cooking.

Take a look at the User class below, the class purpose is to hold the user details and perform the user actions, such as Registration action. The Registration function does a lot of things, opens a connection to the DB, saves user details, sends a confirmation email, and writes an event log if needed.

When we write our code like this, every change in the DB engine, email server, or logging logic will affect our class. These changes are for sure none of the user class’s responsibilities. The User class's responsibility is to take care that the user data is saved into the DB. It should not take care of how the data is saved, what the connection string is, or which kind of DB engine we use.

For this reason, this is a violation of the SRP.

To fix that we will move the responsibility of DB actions to the DBUtil class, and add messaging and logging services.

After separating the responsibilities, the User class will be more readable, and it will be changed only if the registration logic or data has changed.

Open/Close Principle (OCP)

A module should be open to extension but closed for modification.

This means that we need to avoid changing our existing code and add new functionality by adding a new class.

In our messaging service, we check which delivery message type is required by using an if-else statement and handling all the messaging logic. This way when new delivery types are added, or the logic of the existing one is changed, we need to scan and test the entire code.

To avoid OCP violation we will add IMessagingService interface, a class for every type of delivery message, and MessagingFactory class to implement a Factory pattern.

To send a message we use the Messaging factory that returns the relevant concrete object.

Now, adding a new delivery type does not change our service at all. Yes, we need to change our messagingFactory but it a single and known place to change. Following OCP and avoiding modification helps us keep the code stable, maintainable, easily extendable, and avoid breaking existing code.

Liskov’s Substitution Principle (LSP)

This principle was introduced by Barbara Liskov.

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

or in other words,

Objects in a program should be replaceable with instances of their sub-types without altering the correctness of that program.

It is necessary that every derived class can be placed instead of its base without breaking the code.

LSP is often described with the duck test: “If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction”. If we took a duck and try to replace it with a toy duck it will not fly, it is not really a duck. So toy duck class cannot implement a duck class.

To implement LSP, the derived class should follow several rules in order to save the base class behavior and properties. For example: the “Signature rule”, explains that we need to save on the base class method signature. We are doing that by saving the numbers of arguments and their types. The same applies to the return types and set of exceptions.

Our next example demonstrates a violation of LSP by overriding the method exceptions and returning a different set of exceptions than the base class.

Our system has two types of users, registered and guest users. Both are implementing the IUser interface. One of the actions of registered users is UpdatePassword. Our Guest class is using the same interface IUser, therefore it is forced to implement UpdatePassword functionality. The result is, we throw an exception because the guest user doesn’t have any password to update.

To Fix this violation we write two interfaces:

IUser with shared functionality between both registered guest users.

IRegisteredUser implements IUser and extends registered user functionality.

For additional reading check Barbara Liskov’s paper: Behavioral Subtyping Using Invariants and Constraints.

Interface Segregation Principle (ISP)

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

In other words, “clients shouldn’t be forced to depend on methods they do not use”.

This principle sounds familiar and very close to the Single Responsibility Principle. Unlike the SRP in this principle, we use the client perspective. If the client should use one function we don’t want it to depend on an interface that includes more methods than it needs. Or as Uncle Bob writes in his paper:

If you have a class that has several clients, rather than loading the class with all the methods that the clients need, create specific interfaces for each client and multiply inherit them into the class.

Let's assume we need to send notifications to our users. For that, we create a notification class with one function Send. This function gets an IUser object to get the user email using GetEmailNotification property.

Our user class implements a set of functions that is irrelevant to the Notification class, therefore we violate the ISP.

To solve this we define a new interface INotifiable with GetEmailNotification. In the notification service, we get INotifiable instead of IUser.

Now the GuestUser class implements IUser and INotifiable interfaces.

This way, our Notification class depending only on a small interface with one function, without depending on a big interface with a bunch of irrelevant functions.

Furthermore, if we need to use the notification service for other types in the future, we can easily do it without implementing all IUser functionality.

Dependency Inversion Principle (DIP)

High level modules should not depend on low level modules. Both should depend on abstractions.

Our User class implements function Registration that gets as an argument SQLDBUtil class to handle data manipulation in the DB. It is obvious that any change in the database engine will cause a change in the Registration function. For example, if we need to start using MongoDB instead of SQL, we need to change our Registration function to use MongoDBUtil. Moreover, probably we have plenty of classes using our SQLDBUtil. Think about how much work you need to do in order to implement this change.

Here is our violation example:

To avoid that violation we declare a new IDBUtil interface that SQLDBUtil implements and changes the Registration function to get IDBUtil.

Notice how the User class gets IDBUtil instead of the concrete SQLDBUtil :

In this way, when we are changing the DB engine from SQL to Mongo we just need to modify the piece of code that creates the DB instance and not the User class.

Now User class depends on the IDBUtil abstraction instead of the low-level class SQLDBUtil.

Final Thoughts

Remember, the SOLID are principles, not rules.

SOLID is a tool to make your code readable, maintainable, and extendable.

Use it wisely.