[TIL-3] SOLID Principles, Explanation and Example

davidasync
8 min readDec 6, 2020

--

During my performance review at my current company I got a suggestion from my lead that I need to read about SOLID principles. Back then I only skimmed about SOLID principles and just thought that it’s good to know about that and not go through the details.

Lately during my spare time, I’m reading linkedin posts at my linkedin’s feed. Some people post job vacancies and some of them demand solid understanding about SOLID Principles as their minimum qualification.

Therefore I interested to know deeper about that, and compile it to this article.

What is SOLID Principles ?

The SOLID principles were first introduced by Robert C. Martin (uncle Bob) in his 2000 paper, Design Principles and Design Patterns. Uncle Bob is also the author of bestselling books Clean Code and Clean Architecture, and is one of the participants of the “Agile Alliance”.

These concepts were later built upon by Michael Feathers, who introduced us to the SOLID acronym.

Martin’s and Feathers’ design principles encourage us to create more maintainable, understandable, and flexible software. Consequently, as our applications grow in size, we can reduce their complexity and save ourselves a lot of headaches further down the road!

The following 5 concepts make up our SOLID principles:

  1. Single Responsibility
  2. Open/Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

Some of the principles might sound gripping. But worry no more, they can easily understand by example (hopefully).

S — Single Responsibility Principle (SRP)

a class should do one thing and therefore it should have only a single reason to change.

Let’s kick things off with the single responsibility principle. As we might expect, this principle states that a class should only have one responsibility. Furthermore, it should only have one reason to change.

To make it more technical: only one potential change such as data model / container, database logic, logging logic, monitoring logic, and etc.

How does this principle help us to build better software? Let’s see a few of its benefits:

  1. Testing — A class with one responsibility will have far fewer test cases
  2. Lower coupling — Less functionality in a single class will have fewer dependencies
  3. Organization — Smaller, well-organized classes are easier to search than monolithic ones

Take a look at this following codes that represent of a simple Car object.

Now, let’s create an invoice class that will contain logic for creating invoice and calculate the Car price.

Our class violates the Single Responsibility Principle in multiple ways.

  1. printInvoice method that contains our printing logic. The SRP states that our class should only have a single reason to change, and that reason should be a change in the invoice calculation for our class. But in this architecture, if we wanted to change the printing format, we need to change the class. This is why we should not have printing logic mixed with business logic in the same class.
  2. save method. It is also an extremely common mistake to mix persistence logic, api call logic, and other persistence related things with business logic.

To fix these violations we can separate printing logic and persistence logic into 2 classes, so we don’t need to modify the Invoice class for that purpose

O — Open-Closed Principle (OCP)

classes should be open for extension and closed to modification

Modification is changing the code of an existing class, and extension is adding new functionality.

We should be able to add new functionality without touching the existing code for the class.

This is because whenever we modify the existing code, we are facing the risk of creating potential bugs. So we should avoid touching the tested and reliable production code if possible.

This requirement can be done with the help of interfaces, inheritence or abstract classes.

Let’s go back to our Invoice class, and we want to add functionality to save the invoice into the database.

Unfortunately, this design is not satisfied with the Open Closed Principle, because this class is not expandable in the future. For example if we want to save the Invoice to another persistence or different datastore. We need to modify the InvoicePersistence class, so it’s not Closed to modification.

To solve this design problem, we need to refactor the class to obey Open Closed Principle,

  1. change InvoicePersistence to Interface class
  2. Add method that called save()
  3. Each persistence class will implements save() method

With this design, our persistence logic is easily extendable, if we want to add more persistences.

For example if someday we want to save our Invoice to another database or other datastore, we can easily create a class that implements the save() method.

L — Liskov Substitution Principle (LSP)

A sub-class must be substitutable for its super-class

The Liskov Substitution principle was introduced by Barbara Liskov in her conference keynote “Data abstraction” in 1987.

Barbara Liskov and Jeannette Wing formulated the principle clearly in a 1994 paper as follows:

Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.

Robert Martin made the definition sound more smoothly and concisely in 1996 :

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.

or we can just read it,

Subclass/derived class should be substitutable for their base/parent class.

In real projects this violation is hard to detect because sometimes things that sound right in natural language don’t quite work in code.

Take a look at Vehicle class, we have startEngine() function at this class. We know that Car is a Vehicle and Bicycle is a Vehicle. The “is a” makes you want to model this with inheritance.

But when we create the design based on this sense it will violate Liskov Substitution Principle. Because startEngine() that is called from Bicycle and Vehicle have different behaviour.

The Bicycle class will throw Exception and the Vehicle class will run the start engine logic. Therefore It will make the Bicycle.java (sub class) not substitutable with Vehicle class (super class).

Based on my personal observation in real projects, this violation can most usually be recognized by a method that does nothing, or even can’t be implemented.

To fix this problem we can refactor those classes by separating the Vehicle without engine and Vehicle with engine.

I — Interface Segregation Principle (ISP)

A Client should not be forced to implement an interface that it doesn’t use.

This rule means that we should break our interfaces in many smaller ones, so they better satisfy the exact needs of our clients.

The principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.

Similar to the Single Responsibility Principle, the goal of the Interface Segregation Principle is to minimize the side consequences and repetition by dividing the software into multiple, independent parts.

Take a look at these classes that implements Worker interface. Because RobotWorker implements Worker, it is forced to override the sleep method albeit RobotWorker doesn’t need sleep. This is a simple example of Interface Segregation Principle violation.

As we can see, the interfaces don’t violate the Interface Segregation Principle. The implementations don’t have to provide empty methods or throw unimplemented methods. This keeps the code clean and reduces the chance of unexpected erros.

The Interface Segregation Principle is an important concept while designing and developing applications. Adhering to this principle helps to avoid bloated interfaces with multiple responsibilities. This eventually helps us to follow the Single Responsibility Principle as well.

D — Dependency Inversion Principle (DIP)

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.

By implementing the Dependency Inversion Principle, the modules can be effectively changed by different modules, simply changing the dependency module and High-level module won’t be influenced by any progressions to the Low-level module.

In this article (2000), Uncle Bob sums up this principle as follows:

“If the OCP (Open-Closed Principle) states the goal of OO architecture, the DIP states the primary mechanism”.

These two principles are in reality related and we have applied this pattern before while we were examining the Open-Closed Principle.

There’s a typical misconception that dependency inversion is just another approach to state dependency injection. In any case, the two are not the equivalent.

In the above code despite Injecting MongoDbComponent class in CarDataAccessor class but it relies upon MongoDbComponent . High-level module CarDataAccessor ought not rely upon low-level module MongoDbComponent.

In the event that we need to change the connection from MongoDbComponent to PostgresComponent , we need to change hard-coded constructor injection in CarDataAccessor class.

CarDataAccessor class ought to rely upon Abstractions not upon concretions.

In the above code, if we want to change the database connection from MongoDbComponent to PostgresComponent, we have no compelling reason to change constructor injection in the CarDataAccessor class. Since here the CarDataAccessor class relies on Abstractions, not on concretions.

--

--

davidasync
davidasync

Written by davidasync

The Joy of discovery is one of the best things about being a software developer ~ Eric Elliott

No responses yet