The SOLID Principles

Ankit
7 min readSep 5, 2023
Image Source: www.freecodecamp.org

SOLID principles are the 5 main pillars upon which modern software engineering stands tall. They serve as a beacon of guidance for developers seeking to create code that’s more than just functional as well as truly exceptional. Each alphabet of SOLID represent one principle each which can help us in developing highly efficient, scalable and loosely coupled systems.

S Single Responsibility Principle (SRP). This principle tells us to design a class such that it does only one thing, fostering clarity and simplicity.

O Open-Closed Principle (OCP). The principle encourages us to extend our capabilities from the already existing set of classes and not modifying them for every new requirement, thus ensuring that our software evolves gracefully.

L — Liskov Substitution Principle (LSP). This principle talks about the parent-child class relationship. It says “Derived types must be completely substitutable for their base types”. (Don’t worry if you’re still confused, we’ll dicuss this in detail later)

I — Interface Segregation Principle (ISP). This principle pushes us to craft small, client-focused interfaces, reducing the burden of unnecessary dependencies.

D — Dependency Inversion Principle (DIP). The principles encourages high-level modules to depend on abstraction rather than concrete classes.

By adhering to SOLID principles, developers empower themselves to create software systems that are agile, adaptable, and resilient in the face of change.

Lets understand each principle better with examples

Single Responsibility Principle

The Single Responsibility Principle(SRP) states that a class should have only one reason to change. In essence, a class should do one and only one thing, be it managing object’s data, new object creation etc. By adhering to the SRP, the code becomes more maintainable, easier to understand, and less prone to bugs.

Let’s see an example of which disobeys SRP

Here, the Employee class disobeys the SRP as it is responsible for managing employee data while also calculating the income tax(which is generally used by finance dept.).

A better design approach can be:

Separating the responsibilities into Employeeand IncomeTaxCalculator classes makes our code bug-free and it also allows the classes to follow Single Responsibility Principle.

Open Closed Principle

The principle states that software entities (such as classes, modules, and functions) should be open for extension but closed for modification. In other words, we should be able to add new functionality or behavior to a class without changing its existing source code. The principle heavily focus on getting rid from tight coupling.

Let’s take an example of a Text Editor application you’ve written in Java, which can help you work with .txt, .pdf, .docx type files. A bad design for such an application can look like:

The TextEditor class is not closed for modification. To support a new document format (e.g., adding support for ".html" files), we would need to modify this class, which can lead to code fragility and increased complexity.

A better approach is to adhere to the Open-Closed Principle by designing the Text Editor to be extensible without modifying existing code. We can achieve this through the use of polymorphism and an abstract base class or interface.

In this improved design, each document type (e.g., TXTDocument, PDFDocument, DOCXDocument) implements the Document interface, providing its own open and save methods. If we, want to add support for a new document format, we simply create a new class that implements the Document interface, without modifying any existing code. This adheres to the Open-Closed Principle and promotes a more extensible and maintainable text editor application.

Liskov’s Substitution Principle

Liskov’s Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if you have a class hierarchy, you should be able to substitute any subclass for its parent class without breaking the program’s behavior.

To understand Liskov’s Substitution Principle, let’s first look at an example of bad design that violates it.

In the above code, the method turnOnEngine() will work fine for objects of Vehicle and Car type, but for Bicycle our code will throw an exception and fail. The Bicycle class in the above affects the correctness of our program by breaking the behavior offered by its base class.

To fix the issue we can introduce an `EngineController` interface to clearly define which vehicles can accelerate which cannot.

Car implements the EngineController interface, while Bicycle does not. Now, we are sure that any vehicle which implements EngineController, behaves as expected, adhering to Liskov's Substitution Principle.

Interface Segregation Principle

ISP suggests that we should avoid creating large, monolithic interfaces that force implementing classes to provide methods they don’t need. Instead, we should design interfaces that are tailored to the exact requirements of the classes that implement them. This principle helps in achieving a more modular, maintainable, and flexible codebase.

Let’s take an example of a Notification Sender application which can send Email notifications, SMS notifications and Slack notifications.

A bad design could look something like:

In the above, we can see that all the concrete classes are unnecessarily contains the function declaration of 2 methods which makes the code more complex. Also in future if we want to add some new notifier for example, Microsoft Teams notifier or XMatters notifier we need to add new method firstly in the interface and then the newly added method in the interface would lead to addition of more unnecessary code in all the concrete classes.

A better way to design the above application is using separate interface for each type of notifier so that any newly addition can be made easily without touching the existing code.

Now each concrete class can implement their respective interface and thus enabling loose coupling between interfaces and classes.

Dependency Inversion Principle

DIP encourages you to depend on abstractions (interfaces or abstract classes) rather than concrete implementations, and it promotes the idea of inverting the direction of dependencies. Dependency Inversion Principle (DIP) states:

  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.

To better understand the principle lets take an example of Report Generation application. The app generates report in PDF & CSV formats from the input file.

A initial design could look like

In this design, the ReportGeneratorclass depends directly on the concrete implementations of PDFReport and CSVReport. But, if we want to add more report formats, we’ll need to modify the ReportGenerator class, doing which we’ll be violating the Open-Closed Principle.

To adhere to the Dependency Inversion Principle, we can introduce an abstraction (interface or abstract class) that represents the common behavior of report generators. Then, have concrete implementations of this abstraction.

In this improved design, we introduce the Report interface, and both PDFReport and CSVReport implement it. The ReportGenerator class depends on the Report interface rather than concrete implementations. You can easily add new report formats by creating classes that implement the Report interface, without modifying the ReportGenerator class. The design makes code more extensible, easy to re-use and easy to debug.

Conclusion

By applying SRP, OCP, LSP, ISP, and DIP, we can create code that is more resilient to change, easier to understand, and simpler to maintain. These principles help build software that stands the test of time, adapts to evolving requirements, and minimizes the risk of introducing bugs when making changes.

So, the next time you embark on a software development journey, remember the SOLID principles and let them guide you towards writing code that is not just functional but also elegant and robust.

For any doubts in the above explanation feel free to reach me at — ankitpandeycu@gmail.com

Share this blog with your peers if you find it useful.

Hope it helps!

AnkitCode99 here….

--

--