SOLID Design Principles

1. Introduction

SOLID is a popular set of design principles that are used in object-oriented software development. SOLID principles are the design principles that enable us to manage most of the software design problems. The term SOLID is an acronym for five design principles intended to make software designs more flexible and maintainable. SOLID principles were first mentioned by Robert C. Martin. The SOLID acronym was first introduced by Michael Feathers.

SOLID stands for following design principles:

  • S: Single Responsibility Principle
  • O: Open Closed Principle
  • L: Liskov Substitution Principle
  • I: Interface Segregation Principle
  • D: Dependency Inversion Principle

1.1. Benefits of using SOLID principles

Using SOLID principles:

  • promotes loose coupling of code.
  • increases readability, extensibility and maintenance of code.
  • increases reusability of code.
  • increases testability of code.

2. Single Responsibility Principle

A class should have only one reason to change.

Every module or class should have responsibility of a single functionality, and that functionality should be entirely encapsulated by the class.

Each class or module should focus on a single task and should be related to a single purpose. If possible we should try to break down the purpose to the smallest unit possible. However, how small the task should be depends on the choice of the developer. The benefit of using Single Responsibility Principle is that the code of class becomes cleaner and classes become small. Smaller classes make it easier to test.

2.1 Example of Single Responsibility Principle

Suppose you have a requirement to calculate the interest and print in on the console or a disk. You can choose to write the logic in a single class, but that is not a good design. The class which has the responsibility of calculation should not be responsible to print the sum. Using the Single Responsibility Principle, you can create following two classes:

  • Class to calculate interest.
  • Class to print output on console or on a disk.

3. Open Closed Principle

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

The code should be designed and written in such a way that new functionality should be added with minimum changes in the existing code. The code should be designed in such a way that it allows for adding new functionality as classes, keeping existing code unchanged. Robert C. Martin considered this as the most important principle of object-oriented design.

The simplest way to implement Open Closed Principle is to implement the new functionality in the new derived classes. If we don’t follow this principle then the new functionality implementation will be done by changing the existing code. This will result in testing the entire code after development and in increase of cost for the organization. This may also break the Single Responsibility Principle as new functionality will be added in the same class. Not following this principle results in increase of the code of the class and maintenance overhead.

3.1 Example of Open Closed Principle

AbstractMap in Java is an abstract class. More specific implementations of this class are ConcurrentHashMap and HashMap. Both are used to fulfill different requirements. The usual way to use child class is:

Parent p = new Child();

In future, if you want to add new functionality you can add it to Child class. The Parent class provides the generalization of behavior whereas the child class provides more specified behavior.

4. Liskov Substitution Principle

Introduced by Barbara Liskov states that:

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

If a program is using a Base class, then the reference to the Base class can be replaced with Derived class without affecting the functionality of the program. We can also state that the Derived types must be completely substitutable for their base types. This principle is an extension of Open Closed Principle.

Following are few implementation guidelines:

  • No new exceptions can be thrown by the subtype.
  • Clients should not know which specific subtype they are calling.

4.1 Example of Liskov Substitution Principle

In Open Closed Principle we discussed the AbstractMap class has a derived class as a HashMap. Now assume a situation that HashMap class shows a behavior of a List. This is a very bad decision and design. We expect the derived class to show the same behavior as the derived class, in our case HashMap should show behavior of the AbstractMap. Wherever we are using a Parent, we should we able to use a Derived class. If we break this contract then Parent p = new Child() will have no meaning.

5. Interface Segregation Principle

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

We should not enforce clients to implement interfaces that they don’t use. Instead of creating one big interface we can break down it to smaller interfaces.

5.1 Example of Interface Segregation Principle

Let us now understand what will happen if we don’t follow this principle. Suppose we have an interface with 100 methods. If a class implements this interface, it has to provide implementation of all these classes. If the methods are of no use, even then the class has to provide an empty implementation. If there is another class which has to implement some particular set of methods, then it has to provide the empty implementation of all methods. This contract is binding to the implementing class. Rather than doing this we can break the interface into smaller interfaces and the classes can implement the interface they want.
For example, if a class has to calculate the interest, then it should implement only the interface which has methods for interest calculation. It should not implement the interface which has methods to print the interest.

6. Dependency Inversion Principle

One should “depend on abstractions, [not] concretions”. Abstractions should not depend on details whereas the details should depend on abstractions.

In simple terms, high-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other.

Dependency Inversion Principle consists of two parts:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

6.1 Example of Dependency Inversion Principle

Let us understand this with the help of a real world example. Suppose you want to eat a burger and you go to a restaurant. What if you have to prepare the burger yourself. Then will it be worth to visit a restaurant? Instead of preparing the burger yourself, you tell the cashier the type of burger you want. Cashier acts as an interface for you to get your burger. Someone else prepares a burger and serves at your table.
Dependency inversion principle works in the same manner. Rather than creating the dependency (your requirement, burger in this case) yourself, you define what dependency you want and that dependency is created and delivered to you with the help of an interface.