# SOLID Principles Demystified: A Developer's Roadmap to Clean Code

In the ever-evolving landscape of software development, writing code that is not only functional but also readable, maintainable and adaptable is the main goal. This is where SOLID principles come into play—a set of five design principles that serve as a guiding light for developers striving to create robust and scalable software.

SOLID is an acronym that stands for five fundamental principles of object-oriented programming and design, introduced by **Robert C. Martin** and widely adopted across the software development industry. The 5 SOLID principles are:

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

Let us delve into the heart of SOLID principles and understand each of them for their significance in building a clean readable, maintainable and adaptable code.

## Single Responsibility

The **Single Responsibility Principle** states that a class should always have only one responsibility and there should only be one reason to modify it.  
At the core, the principle says that each class should deal with a specific concern and only one concern so that if there is any situation to modify the code you know where exactly it has to be done, minimizing the risk of unintended consequences.

**Bad Implementation:**

```java
public class Product {
    private int productId;
    private String productName;
    private double productPrice;

    // Getters and Setters

    // *** Business Logic ***
    public void saveProduct(){ /*buisness logic here*/ }
    public void getProductById(){/*buisness logic here*/ }

    // *** Persistence Logic ***
    public void saveProductToDatabase(Product product){/*persistence logic here*/}
    public void fetchProductById(int id){/*persistence logic here*/}
}
```

**Issue:** In the above example, the Product class is dealing with multiple aspects(entity definition, business logic and persistence logic). This code can further get complex and there are multiple reasons to modify the class.

**Good Implementation:**

```java
// Create a Product class that defines product attributes.
public class Product {
    private int productId;
    private String productName;
    private double productPrice;

    // Getters and Setters
}
/*---------------------------------------------------------------------------
Later, create a service class that performs a business logic.*/
public class ProductService {
    // Business Logic
    public void saveProduct(){ /* ... */ }
    public void getProductById(){/* ... */ }
}
/*---------------------------------------------------------------------------
And, have a Dao class that interacts with the database (persistence logic).*/
public class ProductDao {
    // Persistence Logic
    public void saveProductToDatabase(Product product){/* ... */}
    public void fetchProductById(int id){/* ... */}
}
```

The above example contains three different classes dealing with multiple aspects of the same resource (Product). The `Product` class defines the entity, the `ProductService` class deals with the business logic and `ProductDao` deals with the persistence logic.  
Here each class has its reason and only reason for getting modified.

The Single Responsibility Principle stands as the most widely followed guideline among all SOLID principles. By advocating for a single responsibility per class, it promotes code that is not only more readable and maintainable but also highly adaptable to diverse projects.

## Open/Closed

The **Open/Closed Principle** states that "a class should be open for Extension but closed for modification". In other words, once a class is written and tested, it should not be altered to accommodate new features. Instead, it should be open to accepting new functionality through extension, without requiring changes to its existing codebase.

**Bad Implementation:**

```java
public class Payment {

   public void doPay(String paymentType){
       if(paymentType.equals("CreditCard")){
           // logic to perform Credit Card Payment
       }else if(paymentType.equals("DebitCard")) {
           // logic to perform Debit Card Payment
       }
   }
}
```

**Issue:** In the above example, the Payment class performs a payment in two ways by Credit Card and Debit Card. If in future we need to add another payment mode, we may have to alter the existing codebase. As the Open/Closed principle states the existing codebase should never be altered, so how do we resolve this issue? Let us look into the good Implementation.

**Good Implementation:**

```java
public interface Payment {
   public void doPay(String paymentType);
}
```

Since we have introduced an interface **Payment,** it can later be implemented by desired implementing classes allowing different payment modes such as **Credit Card, Debit Card,** etc.,

```java
public class CreditCardPayment implements Payment{
    public void doPay(String paymentType) {
        // logic to perform Credit Card Payment
    }
}

public class DebitCardPayment implements Payment{
    public void doPay(String paymentType) {
        // logic to perform Debit Card Payment
    }
}
```

Later in future if there has to be a new payment method introduced, it can be achieved by implementing the Payment Interface and we need not have to alter the existing code base. for example, if we need to add a **UPI Payment** mode to the list the Payment modes we can just implement the Payment interface with UPI Payment logic.

```java
public class UpiPayment implements Payment{
    public void doPay(String paymentType) {
        // logic to perform UPI Payment
    }
}
```

In the real world, we can take the **List interface** and its implementing classes **ArrayList, Vector** and **LinkedList** from the **Collection Framework** as an example.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700060294087/5a1a98a9-532b-45ec-b165-cc95e940e0b0.jpeg align="center")

Here in future, it is possible to easily create a new List implementing class with a new way of storing and processing the elements without the need of altering the existing codebase.

## Liskov Substitution

The **Liskov Substitution Principle** states that the objects of parent class should replaceable by the objects of child classes without affecting the correctness of the program.  
In other words, if we have a parent class A and child class B, we should be able to replace of object of class A with the object of class B.

**Bad Implementation**

```java
public interface Car{
    public void move();
    public void color();
    public void fuel();
}

public class HondaCar implements Car{
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
     @Override
    public void fuel(){ /* implementation */}
}

public class TeslaCar implements Car{
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
     @Override
    public void fuel(){
         // does not support
    }
    // requires a different method to support the propultion type
    public void battery(){ /* implemention */ }
}
```

In the above given example the `HondaCar` class object can completely replace the `Car` type with same behaviours, but `TeslaCar` class object cannot completely replace the behaviours of `Car` type. This is because the TeslaCar doesn't support `fuel()` and has an additional behaviour `battery()`.  
When we try to assign the object of TeslaCar to the reference of Car (`Car car = new TeslaCar()`), TeslaCar loses its battery() behaviour. In other words, the battery method cannot be accessed with the reference of the Car Type.

**Good Implementation**

```java
public interface Car{
    public void move();
    public void color();
}

public class HondaCar implements Car{
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
}

public class TeslaCar implements Car{
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
}
```

The above example perfectly adheres to the **Liskov Substitution Principle,** both the TeslaCar object and the HondaCar object can be assigned to the Car reference without losing any behaviours of any classes.

But, few behaviours are subjective to TeslaCar and HondaCar such as `fuel()` and `battery()`. These behaviours can be mandatory as well. These issues can further be solved using the next principle of the SOLID principles i.e., the **Interface Segregation principle.**

## Interface Segregation

The **Interface Segregation Principle** is the fourth principle of the SOLID principles, it states that " An Interface should only have those methods that apply to all its child classes, A child class should never be forced to implement the method that does not apply to its behaviour".

Considering the previous example taken for the Liskov Substitution Principle, we can see that the interface has only those methods that apply to all its child classes. So hereby we are not forcing our child classes (HondaCar & TeslaCar) to implement unnecessary methods. But further, HondaCar and TeslaCar have their subjective behaviours right? So how do we achieve having them?

**Solution:**

```java
public interface Car{
    public void move();
    public void color();
}

public interface FuelEngineCar{
    public void fuel();
}

public interface BatteryCar{
    public void battery();
}
// --------------------------------------------------------------------------
// implementing classes

public class HondaCar implements Car, FuelEngineCar {
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
    @Override
    public void fuel(){ /* implementation */}
}

public class TeslaCar implements Car, BatteryCar {
    @Override
    public void move(){ /* implementation */}
     @Override
    public void color(){ /* implementation */}
     @Override
    public void battery(){ /* implementation */}
}
```

In the above program, we have created two different interfaces `FuelEngineCar` and `BatteryCar` both have specific methods for marking specific behaviours. so by implementing `FuelEngineCar` interface we can have a car with `fuel()` method and by implementing `BatteryCar` interface we can have a car with the `battery()` method.

This is how we can segregate the unrelated behavioural methods into different interfaces and achieve different behaviours.

In real-time, we can again consider the **List interface** and its implementing classes **ArrayList, Vector** and **LinkedList** of collection framework as an example.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700121624635/0360f6f4-7629-4089-9eed-be1aefb131f3.jpeg align="center")

Here the LinkedList class implements both List and Deque marking the behaviour of both the interfaces together.

## Dependency Inversion

The **Dependency Inversion Principle** states that "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".

In a classic scenario where high-level modules directly depend on low-level modules. If changes are made to low-level modules, it can have a cascading impact on higher-level modules, leading to fragility in the system. The Dependency Inversion Principle addresses this issue by introducing abstractions, typically in the form of interfaces or abstract classes, that act as mediators between high-level and low-level components.

```java
// Abstraction
interface SwitchableDevice {
    void turnOn();
    void turnOff();
}
```

Here we have an Interface `SwitchableDevice`, having methods such as `turnOn()` and `turnOff()`. This interface can be used by a high-level module without worrying about the implementation.

```java

// High-level module depending on abstractions
class Switch {
    private SwitchableDevice device;

    public Switch(SwitchableDevice device) {
        this.device = device;
    }
    public void press() {
        device.turnOn();
    }
}
```

The above `Switch` class represents a high-level module, where it is not directly dependent on the low-level module (classes with concrete methods), rather it depends on an abstracted method. Therefore any changes to the implementing class of `SwitchableDevice` will not affect the code on `Switch` class.

```java
// Low-level module
class LightBulb implements SwitchableDevice {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: ON");
    }
    @Override
    public void turnOff() {
        System.out.println("LightBulb: OFF");
    }
}

// Another low-level module
class Fan implements SwitchableDevice {
    @Override
    public void turnOn() {
        System.out.println("Fan: ON");
    }
    @Override
    public void turnOff() {
        System.out.println("Fan: OFF");
    }
}
```

Here we have two low-level modules, `LightBulb` and `Fan` implementing the `SwitchableDevice` interface. So now we can either assign an object of `LightBulb` of `Fan` to the `SwitchableDevice` reference present in the `Switch` class (high-level module).  
This promotes the code reusability.

```java

public class Test {
    public static void main(String[] args) {
        SwitchableDevice light = new LightBulb();
        SwitchableDevice fan = new Fan();

        Switch lightSwitch = new Switch(light);
        Switch fanSwitch = new Switch(fan);

        lightSwitch.press(); // Output: LightBulb: ON
        fanSwitch.press();   // Output: Fan: ON
    }
}
```

In the real world, the **Dependency Inversion Principle** is again one of the widely followed principles among the SOLID principles.  
**Examples:**

1. **Java-JDBC-Connector**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700128692716/85ceae40-8322-4b8d-88a2-d5ff8411696e.jpeg align="center")
    
2. **Java-JPA-Hibernate**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700128704373/9d466014-3f44-46e3-881b-b9632bafc7fc.jpeg align="center")
    

## Conclusion

In wrapping up our exploration of the SOLID principles, think of them as the superheroes of software design, each playing a crucial role in crafting code that's not just functional but also flexible and easy to manage.

These principles create a league of extraordinary coding practices. They may sound fancy, but at the heart of it, they're simple tools helping us build software that's powerful, adaptable, and ready for whatever challenges come our way. So, let's put on our coding capes and continue crafting our software! If there are any queries feel free to leave a comment.
