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.
Principle | Description |
Single Responsibility Principle | Each class should be responsible for a single part or functionality of the system. |
Open-Closed Principle | Software components should be open for extension, but not for modification. |
Liskov Substitution Principle | Objects of a superclass (super type) should be replaceable with objects of its subclasses (sub type) without breaking the system. |
Interface Segregation Principle | No client should be forced to depend on methods that it does not use. |
Dependency Inversion Principle | High-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 B with A 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() {...}
}