SOLID principles are object-oriented design concepts relevant to software development. SOLID is an acronym for five other class-design principles: Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.
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 should be replaceable with objects of its subclasses 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 is a structured design approach that ensures your software is modular and easy to maintain, understand, debug, and refactor. Following SOLID also helps save time and effort in both development and maintenance. SOLID prevents your code from becoming rigid and fragile, which helps you build long-lasting software.
Every class in Java should have a single job to do. To be precise, there should only be one reason to change a class. Here’s an example of a Java class that does not follow the single responsibility principle (SRP):
public class Vehicle {
public void printDetails() {}
public double calculateValue() {}
public void addVehicleToDB() {}
}
The Vehicle
class has three separate responsibilities: reporting, calculation, and database. By applying SRP, we can separate the above class into three classes with separate responsibilities.
Consider the following code example that fulfills the SRP. The Vehicle
class is only responsible for printing the details of the Vehicle:
public class Vehicle {private String make;private String model;// Constructorpublic Vehicle(String make, String model) {this.make = make;this.model = model;}// Getterspublic String getMake() {return make;}public String getModel() {return model;}// Print vehicle detailspublic void printDetails() {System.out.println("Make: " + make);System.out.println("Model: " + model);}public static void main(String[] args) {// Create a sample vehicleVehicle firstCar = new Vehicle("Toyota", "Camry");// Print detailsfirstCar.printDetails();}}
Software entities (e.g., classes, modules, functions) should be open for an extension, but closed for modification.
Consider the below method of the class VehicleCalculations
:
public class VehicleCalculations {
public double calculateValue(Vehicle v) {
if (v instanceof Car) {
return v.getValue() * 0.8;
if (v instanceof Bike) {
return v.getValue() * 0.5;
}
}
Suppose we now want to add another subclass called Truck
. 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 Car
and Truck
to override the calculateValue
method:
// Make the Vehicle class as parent for Car and Truck subclassesclass Vehicle {private double value;public Vehicle(double value) {this.value = value;}public double getValue() {return value;}// Calculate vehicle value (base implementation)public double calculateValue() {return value; // No depreciation by default}}// Subclass Carclass Car extends Vehicle {public Car(double value) {super(value);}// Override calculateValue for cars (80% depreciation)@Overridepublic double calculateValue() {return super.calculateValue() * 0.8; // Apply 80% depreciation}}// Subclass Truckclass Truck extends Vehicle {public Truck(double value) {super(value);}// Override calculateValue for trucks (90% depreciation)@Overridepublic double calculateValue() {return super.calculateValue() * 0.9; // Apply 90% depreciation}}public class Main {public static void main(String[] args) {Car myCar = new Car(25000.0);Truck myTruck = new Truck(35000.0);System.out.println("Car Value: $" + myCar.calculateValue());System.out.println("Truck Value: $" + myTruck.calculateValue());}}
Adding another Vehicle
type is as simple as making another subclass and extending from the Vehicle
class.
The Liskov Substitution Principle (LSP) applies to inheritance hierarchies such that derived classes must be completely substitutable for their base classes.
Consider a typical example of a Square
derived class and Rectangle
base class:
public class Rectangle {
private double height;
private double width;
public void setHeight(double h) { height = h; }
public void setWidht(double w) { width = w; }
...
}
public class Square extends Rectangle {
public void setHeight(double h) {
super.setHeight(h);
super.setWidth(h);
}
public void setWidth(double w) {
super.setHeight(w);
super.setWidth(w);
}
}
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 the Rectangle
with the Square
class may result in unexpected behavior.
The solution to the above example is to create a separate abstract Shape
class and then have the Square
and Rectangle
classes inherit from it. Consider the following code examples:
abstract class Shape {public abstract double getArea();}class Rectangle extends Shape {private double height;private double width;public Rectangle(double height, double width) {this.height = height;this.width = width;}@Overridepublic double getArea() {return height * width;}}class Square extends Shape {private double side;public Square(double side) {this.side = side;}@Overridepublic double getArea() {return side * side;}}public class Main {public static void main(String[] args) {Shape rectangle = new Rectangle(7.0, 5.0);Shape square = new Square(3.0);System.out.println("Rectangle Area: " + rectangle.getArea());System.out.println("Square Area: " + square.getArea());}}
The Interface Segregation Principle (ISP) states that clients should not be forced to depend upon interface members they do not use. In other words, do not force any client to implement an interface that is irrelevant to them.
Suppose there’s an interface for vehicle and a Bike
class:
public interface Vehicle {
public void drive();
public void stop();
public void refuel();
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.
The Dependency Inversion Principle (DIP) states that we should depend on abstractions (interfaces and abstract classes) instead of concrete implementations (classes). The abstractions should not depend on details; instead, the details should depend on abstractions.
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();
}
}
public 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. Instead of Car
depending directly on Engine
, let’s add an interface:
public interface Engine {
public void start();
}
Now we can connect any type of Engine
that implements the Engine interface to the Car
class:
public class Car {
private Engine engine;
public Car(Engine e) {
engine = e;
}
public void start() {
engine.start();
}
}
public class PetrolEngine implements Engine {
public void start() {...}
}
public class DieselEngine implements Engine {
public void start() {...}
}
Consider the following code examples that follow the dependency inversion principle:
// Abstraction for Engineinterface Engine {void start();}// Class for PetrolEngineclass PetrolEngine implements Engine {public void start() {System.out.println("Petrol engine started.");}}// Class for PetrolEngineclass DieselEngine implements Engine {public void start() {System.out.println("Diesel engine started.");}}// Car class depends on Engine abstractionclass Car {private Engine engine;public Car(Engine e) {this.engine = e;}public void startCar() {engine.start();}}public class Main {public static void main(String[] args) {Engine petrolEngine = new PetrolEngine();Engine dieselEngine = new DieselEngine();Car petrolCar = new Car(petrolEngine);Car dieselCar = new Car(dieselEngine);petrolCar.startCar();dieselCar.startCar();}}