SOLID Principles

Reading Time: 5 minutes

SOLID is stand for Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.

It is refers to five design principles in object-oriented programming, designed to reduce code and improve the value, class, function, and maintainability of software. The SOLID principles help the user develop minimized coupled code. If code is tightly coupled, a group of classes are dependent on one another. This should be avoided for better maintainability and readability.

PrincipleDescription
Single Responsibility PrincipleEach class should be responsible for a single part or functionality of the system.
Open-Closed PrincipleSoftware components should be open for extension, but not for modification.
Liskov Substitution PrincipleObjects of a superclass (super type) should be replaceable with objects of its subclasses (sub type) without breaking the system.
Interface Segregation PrincipleNo client should be forced to depend on methods that it does not use.
Dependency Inversion PrincipleHigh-level modules should not depend on low-level modules, both should depend on abstractions.

SOLID Design Principles

1.Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) states that there should never be more than one reason for a class to change. This means that every class, or similar structure, in your code should have only one job to do.

Everything in the class should be related to that single purpose. It does not mean that your classes should only contain one method or property.

public class Employee {
    public void printDetails() {}
    public double calculateValue() {}
    public void addEmployee() {}
}

The Employee class has three separate responsibilities: reporting, calculation, and adding employee. By applying SRP, we can separate the above class into three classes with separate responsibilities.

2.Open Closed Principle

Open closed principle, entities (e.g., classes, modules, functions) should remain open for extension, but they should stay closed for modification. To be precise, according to this principle, a class should be written in such a manner that it performs its job without the assumption that people in the future will simply come and change it. Hence, the class should remain closed for modification, but it should have the option to get extended. Ways of extending the class include:

  • Inheriting from the class
  • Overwriting the required behaviors from the class
  • Extending certain behaviors of the class

Consider the below method of the class AnimalDietCalculations:

public class AnimalDietCalculations {
    public double calculateDiet(Animal animal) {
        if (animal instanceof Elephant) {
            return animal.getValue() * 40;
        if (animal instanceof Lion) {
            return animal.getValue() * 15;
    }
}

Suppose we now want to add another subclass called Cow. We would have to modify the above class by adding another if statement, which goes against the Open-Closed Principle.
A better approach would be for the subclasses Elephant and Lion to override the calculateDiet method:

public class Animal {
    public double calculateDiet() {...}
}
public class Lion extends Animal {
    public double calculateDiet() {
        return this.getValue() * 15;
}
public class Elephent extends Animal{
    public double calculateDiet() {
        return this.getValue() *40;
}

Adding another Animal type is as simple as making another sub-type and extending from the Animal class.

3. Liskov substitution principle

Liskov substitution, which is the most complex of the five principles. Bu in simply we can put, if class A is a subtype of class B, we should be able to replace with without disrupting the behavior of our program. This avoids misusing inheritance. It helps us conform to the “is-a” relationship. We can also say that subclasses must fulfill a contract defined by the base class.

class Rectangle
{
public double length;
public double width;
//setter & getter

public double getArea() {
        return length * width;
}

}

class Square extends Rectangle{
{
    public void setBreadth(double width) {
        super.setWidth(width);
        super.setLength(width);
    }

    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }
}

//-- Main class
public class LSPDemo {

    public static void calculateArea(Rectangle rectangle,double width,double length) {
        System.out.println("Calculate Area of "+rectangle.getClass().getSimpleName());
        rectangle.setWidth(width);
        rectangle.setLength(length);
        System.out.println(String.format("Calculate Area  %s of %s",     rectangle.getArea(),rectangle.getClass().getSimpleName()));
    }

    public static void main(String[] args) {
        System.out.println("====================================");
        // An instance of Rectangle is passed
        Rectangle rectangle=new Rectangle();
        calculateArea(rectangle,2,3);

        // An instance of Square-1 is passed
        rectangle = new Square();
        calculateArea(rectangle, 2, 3);

        // An instance of Square-2 is passed
        rectangle = new Square();
        calculateArea(rectangle, 3, 2);

        System.out.println("====================================");
    }
}

OutPut:
====================================
Calculate Area of Rectangle
Calculate Area  6.0 of Rectangle

Calculate Area of Square
Calculate Area  9.0 of Square

Calculate Area of Square
Calculate Area  4.0 of Square
====================================

The above classes do not obey LSP because you cannot replace the Rectangle base class with its derived class Square. The Square class has extra constraints, i.e., the height and width must be the same. Therefore, substituting Rectangle with Square class may result in unexpected behavior.

Improvising the above code to fulfill the LSP requirements.

abstract class Shape {
    public abstract double getArea();
}

class Rectangle extends Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double getArea() {
        return length * width;
    }
}

class Square extends Shape {
    double length;

    public Square(int length) {
        this.length = length;
    }

    @Override
    public double getArea() {
        return length * length;
    }
}

// -- Main class
public class LSPDemo {

    public static void calculateArea(Shape shape) {
        System.out.println(String.format("Calculate Area  %s of %s", shape.getArea(), shape.getClass().getSimpleName()));
    }

    public static void main(String[] args) {
        System.out.println("====================================");
        // An instance of Rectangle is assigned
        Shape shape = new Rectangle(2, 3);
        calculateArea(shape);

        // An instance of Square is assigned
        shape = new Square(3);
        calculateArea(shape);
        System.out.println("====================================");
    }
}

OutPut::
====================================
Calculate Area  6.0 of Rectangle
Calculate Area  9.0 of Square
====================================

4.Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that clients should not be forced to depend upon interface members they do not use. The ISP guides us to create multiple, smaller, cohesive interfaces.

“Robert C. Martin describes it as clients should not be forced to implement unnecessary methods which they will not use.”

Suppose there’s an interface for Vehicle and a Bike class:

public interface Vehicle {
    public void drive();
    public void stop();
    public void openDoors();
}
public class Bike implements Vehicle {
    // Can be implemented
    public void drive() {...}
    public void stop() {...}
    public void refuel() {...}
    
    // Can not be implemented
    public void openDoors() {...}
}

As you can see, it does not make sense for a Bike class to implement the openDoors() method as a bike does not have any doors! To fix this, ISP proposes that the interfaces be broken down into multiple, small cohesive interfaces so that no class is forced to implement any interface, and therefore methods, that it does not need.

5.Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) states that high-level modules should not depend upon low-level modules; they should depend on abstractions. According to this, dependency inversion principle, entities should depend only on abstractions but not on concretions.This helps keep coupling low and makes our design easier to change. DIP also allows us to test things in isolation.

Consider the example below. We have a Car class that depends on the concrete Engine class; therefore, it is not obeying DIP.

public class Car {
    private Engine engine;
    public Car(Engine e) {
        engine = e;
    }
    public void start() {
        engine.start();
    }
}

class Engine {
   public void start() {...}
}

The code will work, for now, but what if we wanted to add another engine type, let’s say a diesel engine? This will require refactoring the Car class.
However, we can solve this by introducing a layer of abstraction. Create Engine as interface.

interface class Engine {
   public void start() {...}

Now we can connect any type of Engine that implements the Engine interface to the Car class:

public class PetrolEngine implements Engine {
   public void start() {...}
}
public class DieselEngine implements Engine {
   public void start() {...}
}

Written by 

Abid Khan is a Lead Consultant at Knoldus Inc., postgraduate (MCA), and having 5+ years of experience in JavaSE, JavaEE, ORM framework, Spring, Spring-boot, RESTful Web Services, Kafka, MQTT, Rabbitmq, Docker, Redis, MySQL, Maven, GIT, etc. He is a well-developed professional with a prolific track record of designing, testing, and monitoring software as well as upgrading the existing programs.