SOLID Principles: The Key to Writing Clean &High-Quality Code

Sumonta Saha Mridul
Nerd For Tech
Published in
11 min readMar 18, 2023

--

S — O — L— I — D

The SOLID principles are a set of five software design principles that aim to help developers create software systems that are easier to maintain and extend over time. The principles were introduced by Robert C. Martin (also known as “Uncle Bob”) and are considered to be the foundation of good object-oriented design.

The five SOLID principles are:

  1. Single Responsibility Principle (SRP): A class should have only one reason to change.
  2. Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
  3. Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use.
  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.

By following these principles, developers can create code that is easier to test, maintain, and extend. This is important because software systems often need to be updated and changed over time, and if the codebase is not well-designed, these changes can become difficult, time-consuming, and error-prone.

In summary, the SOLID principles provide guidelines for creating software that is flexible, maintainable, and easy to extend, which ultimately leads to more robust and reliable software systems.

Single Responsibility Principal (SRP)

This principle states that “a class should have only one reason to change” which means every class should have a single responsibility or single job or single purpose or a single functionality.

Let’s understand this principal with an example. Imagine there is a class called BankService which performs following operations.

1. Login

2. Deposit

3. Withdraw

BankService before using Single Responsibility Principle (SRP)

public class BankService {

public void login(String userName , String password){
// authenticate user
}

public long deposit(long amount, String accountNo) {
//deposit amount
return 0;
}

public long withDraw(long amount, String accountNo) {
//withdraw amount
return 0;
}
}

Have you imagined the scenario? Here the class can be changed for login, deposit and withdraw. It doesn’t follow single responsibility principle because this class has to many responsible or task to perform.

To achieve the goal of the single responsibility principle, we should implement a separate class that performs a single functionality only.

Now the BankService class is divided into 3 different class to achieve the goal of Single Responsibility Principle

1. BankServiceLogin

2. BankServiceWithdraw

3. BankServiceDeposit

public class BankServiceLogin{
public void login(String userName , String password){
// authenticate user
}
}
public class BankServiceWithdraw {
public long withDraw(long amount, String accountNo) {
//withdraw amount
return 0;
}
}
public class BankServiceDeposit {
public long deposit(long amount, String accountNo) {
//deposit amount
return 0;
}
}
When you don’t follow the Single Responsibility Principal

Benefits of Single Responsibility Principle

It makes your code easier to understand and maintain, as each class has a clear and specific purpose.

It reduces the complexity and coupling of your code, as each class has fewer dependencies and responsibilities.

It increases the reusability and testability of your code, as each class can be used and tested independently.

It facilitates agile development and refactoring, as each class can be changed with minimal impact on other classes.

Open closed Principle (OCP)

This principle states that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification” which means you should be able to extend a class behavior, without modifying it.

Open — Closed Principal

Suppose I have a class e named “payment.java”. Open closed principal says that if new implementations come in, I can extend “payment.java” class into another class but I cannot modify or change the content in “payment.java” class.

For example, you have created a payment service. Payment Service takes payment with bKash and Creditcard.

PaymentService before using Open Closed Principle (OCP)

public class PaymentService {

public void bkashPayment(String id , int amount , int password){
// proceed bkash Payment
}
public void cardPayment(String id , int amount , int password){
// proceed atm card Payment
}
}

Now if you want to introduce Nagad payment in your payment service then you need to modify source code in Payment Service, right?

Here What OCP says, it opens for Extension but close for modification hence it's not recommended to modify Payment Service for each payment feature, it will violate OCP.

So, to overcome this you need to design your code in such a way that everyone can reuse your feature by just extending it and if they need any customization, they can extend it and add their feature on top of it like an abstraction.

You can design your payment service something like below.

// Payment Service Class Following OCP 
public interface PaymentService {
public void payment(String Id, int amount, int password);
}

// Bkash payment Implementation
// Creating new bkash class and extending payment service without modifying it
public class bkash implements PaymentService{

@Override
public void payment(String Id, int amount, int password) {
// proceed bkash Payment
}
}

// Credit Card payment Implementation
// Creating new CreditCard class and extending payment service without modifying it
public class CreditCard implements PaymentService{
@Override
public void payment(String Id, int amount, int password) {
// proceed atm card Payment
}
}

// New implementation
// Nagad payment Implementation
// Creating new nagad class and extending payment service without modifying it
public class Nagad implements PaymentService{
@Override
public void payment(String Id, int amount, int password) {
// proceed nagad Payment
}
}

Here, you can see, I have introduced nagad payment into my payment service without modifying the payment service class which satisfy the OCP (open for Extension but close for modification).

Benefits of Open Closed Principle

It prevents unintentional behaviors of existing components, as you don’t need to change them when adding new features.

It prevents unintentional behaviors of existing components, as you don’t need to change them when adding new features.

It increases the maintainability and flexibility of your code, as you can easily extend it with new functionalities without affecting the existing ones.

Liskov substitution Principle (LSP)

This principle states that Derived or child classes must be substitutable for their base or parent classes”. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.

The Liskov Substitution Principle (LSP): “Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.” Thus, the objects of the subclasses should behave in the same way as the objects of the superclass.

This principle ensures that inheritance (one of the OOP principles) is used correctly. If an override method does nothing or just throws an exception, then you’re probably violating the LSP.

Let’s understand LSP with an example.

Suppose you have parent class Bird which have two methods fly() and walk()

public class Bird {

public void fly() {
System.out.println("I'm flying");
}

public void walk() {
System.out.println("I'm walking");
}
}

Now you have a class name Dove which is child class of Bird . As Dove can fulfill both fly() and walk() , Dove is complete substitute of Bird class, both can be replaced without any interrupt. So, this doesn’t break the LSP principal.

 public class Dove extends Bird{
public void fly() {
System.out.println("I'm flying");
}

public void walk() {
System.out.println("I'm walking");
}
}

Now you have a class named Penguin which is child class of Bird. As penguin cannot satisfy both fly() and walk() because penguin can’t fly, it is not complete substitute of Bird class.

public class Penguin extends Bird{
public void fly() {
throw new IllegalArgumentException("Can't Fly");
}

public void walk() {
System.out.println("I'm walking");
}
}

In this inheritance, as technically penguin is a bird, penguins do not fly. Thus, the “fly” method is not applicable to all types of birds, violating the LSP principle.

Solution of this problem so that it follows LSP principal.

public class Bird {

public void walk(){
System.out.println("I'm walking");
}
}

public class FlyingBird extends Bird{

public void fly(){
System.out.println("I'm flying");
}
}

public class Dove extends FlyingBird{}{
// dove can fly and walk
}

public class Penguin extends Bird{
// penguin can't fly but can walk
}

Here , the Dove can both walk(from the Bird class) and fly (from FlyingBird class) and the Penguin can only walk (from the Bird class).

Interface Segregation Principle (ISP)

This principle is the first principle that applies to Interfaces instead of classes in SOLID and it is similar to the concept of single responsibility principle. It states that “Do not force any client to implement an interface which is irrelevant to them.” In other words, you should separate interfaces into smaller, more focused ones that are easier to implement and change.

Suppose there is an interface named A and also a class named B.
Again suppose, Interface A has four methods — c(), d(), e(), f(). Now according to the ISP principle, if class B implements interface A, interface A should have only those methods that will be implemented by class B. Now assume that the d() method from interface A, can’t be implemented in a B class and thus it violates the ISP principle. ISP says, as d() will not be implemented by class B, it is irrelevant for class B. So, interface A should not hold method d().

Let’s consider I have an interface named SocialMedia who supported all social media activity for user to entertain them and have 3 methods.

public interface SocialMedia {

public void chatWithFriend();

public void publishPost(Object post);

public void sendPhoto();
}

SocialMedia interface can have multiple implementations like Facebook, WhatsApp, Instagram and Twitter etc.

Now let’s assume Facebook want to users these features or functionalities.

public class Facebook implements SocialMedia{
@Override
public void publishPost(Object post) {
// facebook supports this
}

@Override
public void chatWithFriend() {
// facebook supports this
}

@Override
public void sendPhoto() {
// facebook supports this
}
}

Here, all the above-mentioned features/methods available in Facebook. Here we can consider Facebook completely implements interface SocialMedia. So, this doesn’t break the ISP principal.

But what if WhatsApp wants to use these features or functionalities. Let’s see.

public class Whatsapp implements SocialMedia{
@Override
public void publishPost(Object post) {
// whatsapp doesn't support this
}

@Override
public void chatWithFriend() {
// whatsapp supports this
}

@Override
public void sendPhoto() {
// whatsapp supports this
}
}

Here, all the above-mentioned features/methods are not available in WhatsApp. WhatsApp doesn’t support publishpost() functionality. So, WhatsApp doesn’t completely implement interface SocialMedia . As a result, it doesn’t follow ISP.

How to overcome this issue so that WhatsApp and overall code follow ISP.

  1. SocialMedia interface should have functionalities which supported by both Facebook and WhatsApp like( chatWithFriend() , sendPhotos() )
public interface SocialMedia {

public void chatWithFriend();

public void sendPhoto();
}

2. Create another interface name “PostManager” which handle posting functionalities like publishpost().

public interface PostManager {
public abstract void publishPost(Object post);
}

3. As Facebook has functionalities of both SocialMedia and PostManager. It will implement both interface SocialMedia and PostManager. So, this doesn’t break the ISP.

public class Facebook implements SocialMedia,PostManager{
@Override
public void publishPost(Object post) {

}

@Override
public void chatWithFriend() {

}

@Override
public void sendPhoto() {

}
}

4. As WhatsApp has only the functionalities of SocialMedia now. It will only implement SocialMedia and will not implement PostManager. Now WhatsApp completely implements SocialMedia. So, this doesn’t break the ISP.

public class Whatsapp implements SocialMedia{
@Override
public void chatWithFriend() {

}

@Override
public void sendPhoto() {

}
}

Liskov substitution Principle (LSP) vs Interface Segregation Principle (ISP)

These two principles may seem similar at first glance, they actually address different concerns:

LSP ensures that subtypes can be used in place of their base types without altering the correctness of the program. In other words, it ensures that inheritance is used correctly, and that derived classes don’t break the behavior expected from the base class.

ISP ensures that interfaces are designed in a way that is specific and relevant to the needs of the clients that use them. It focuses on avoiding bloated and unnecessary interfaces by splitting them into smaller, more focused ones that can be used independently.

To summarize, LSP is concerned with inheritance and polymorphism, while ISP is concerned with interfaces and their usage. While they are both important principles for designing maintainable and extensible software, they address different issues and should be applied in different contexts.

Dependency Inversion Principle (DIP)

The principle states that we must use abstraction (abstract classes and interfaces) instead of concrete implementations. High-level modules should not depend on the low-level module, but both should depend on the abstraction.

Suppose, you go to a local store to buy something, and you decide to pay for it by using your card. So, when you give your card to the clerk for making the payment, the clerk doesn’t bother to check what kind of card you have given.

Even if you have given a debit card or credit card it doesn’t even matter; they will simply swipe it. this is what the abstraction between clerk and you to relay on Card processing.

Let assume you have ShoppingMall class and it only takes debit card payment

public class DebitCard{
public void doTransaction(int amount){
System.out.println("Done with DebitCard");
}
}
public class ShoppingMall {

private DebitCard debitCard;

public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}

public void doPayment(Object order, int amount){
debitCard.doTransaction(amount);
}

public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}

Here, ShoppingMall class is dependent on DebitCard. Now, the shopping mall wants to introduce CreditCard payment. As ShoppingMall class is tightly coupled with DebitCard, you cannot apply credit card payment in ShoppingMall.

Now one solution is to remove Debitcard from constructor and inject CreditCard. which not good approach to write code or wrong design of code.
To follow DIP, we need to design our application in such a way so that high-level modules should not depend on the low-level module, but both should depend on the abstraction. You need to create a design in which my shopping mall payment system should accept any type of ATM Card (it shouldn’t care whether it is debit or credit card).

To simplify this designing principle, i am creating a interface called Bankcards

public interface BankCard {
public void doTransaction(int amount);
}

Now both DebitCard and CreditCard will use this BankCard as abstraction.

public class DebitCard implements BankCard{

public void doTransaction(int amount){
System.out.println("Done with DebitCard");
}
}
public class CreditCard implements BankCard{

public void doTransaction(int amount){
System.out.println("Done with CreditCard");
}
}

Now you need to redesign ShoppingMall implementation

public class ShoppingMall {

private BankCard bankCard;

public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}

public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}

public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}

ShoppingMall class (high-level module) was dependent on DebitCard (low-level module). As it violated the DIP, we created BankCard interface and thus lessened the dependency on DebitCard.
Now ShoppingMall class depends on BankCard which can implement several low-level modules and thus doesn’t violate the DIP.

Now if you observe shopping mall is loosely coupled with BankCard , any type of card processes the payment without any impact on shopping mall which satisfies the Dependency Inversion Principle (DIP).

--

--

Sumonta Saha Mridul
Nerd For Tech

Strategic thinker with a passion😍 for Software Engineering💻 and a creative Photographer📸 https://sumonta056.github.io/