SOLID principles in Java

Md Ariful Islam Rana
6 min readJul 27, 2022

--

SOLID through Java Examples

SOLID is design principle in object oriented programming. It consists of five key concepts. SOLID acronyms for -

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

We will explore these concepts by examples in Java. In every concept, at first, we will see the examples that violates the SOLID principle, then we will remove the faulty code and make it compatible with these principles.

Single Responsibility Principle

A class should have one, and only one, reason to change.

Consider the following class.

public class Employee {
private String name;
private Date dob;
private int baseSalary;

public Employee(String name, Date dob, int baseSalary) {
this.name = name;
this.dob = dob;
this.baseSalary = baseSalary;
}

public String getEmpInfo() {
return "name - " + name + " , dob - " + dob.toString();
}

public int calculateNetSalary() {
int tax = (int) (baseSalary * 0.25);//calculate in otherway
return baseSalary - tax;
}
}

This class violates SRP as it performs two separate jobs. To meet the SRP, we can modify Employee class into two different classes, Employee — which will be responsible to give employee details whereas EmployeeSalaryCalculator — will calculate salary related information. Now, both class has only one job and only one reason to change.

public class Employee {
private String name;
private Date dob;

public Employee(String name, Date dob) {
this.name = name;
this.dob = dob;
}

public String getEmpInfo() {
return "name - " + name + " , dob - " + dob.toString();
}
}

public class EmployeeSalaryCalculator {
private int baseSalary;

public EmployeeSalaryCalculator(int baseSalary) {
this.baseSalary = baseSalary;
}

public int calculateNetSalary() {
int tax = (int) (baseSalary * 0.25);//calculate in otherway
return baseSalary - tax;
}
}

Open-Closed Principle

Open for extension, closed for modification i.e able to extend a class behavior, without modifying it.

The SpeedCalculation class calculate allowed speed for different kind of Vehicle. Now, it calculates only for Car and Bus type Vehicle. But, later if we want to add MotorBike like — new Vehicle(120, “MotorBike”) — this way, then we have to change code in calculateAllowedSpeed method to calculate speed of MotorBike which goes against OCP concept.

public class SpeedCalculation {
public double calculateAllowedSpeed(Vehicle vehicle) {
if (vehicle.getType().equalsIgnoreCase("Car")) {
return vehicle.getMaxSpeed() * 0.8;
} else if (vehicle.getType().equalsIgnoreCase("Bus")) {
return vehicle.getMaxSpeed() * 0.6;
}

return 0.0;
}
}
public class Vehicle {
int maxSpeed;
String type;

public Vehicle(int maxSpeed, String type) {
this.maxSpeed = maxSpeed;
this.type = type;
}

public int getMaxSpeed() {
return this.maxSpeed;
}

public String getType() {
return this.type;
}
}

We can solve this by moving calculateAllowedSpeed to Vehicle class, remove SpeedCalculation class and add Car/Bus class which extends Vehicle and override default calculate method. In future, if we need to add MotorBike, we will simply create MotorBike class that will expends Vehicle class. Now, all class designs embrace OCP concept. Because to add new type of vehicle, we don’t need to change the class, we only need to extends it.

public class Vehicle {
int maxSpeed;
String type;

public Vehicle(int maxSpeed, String type) {
this.maxSpeed = maxSpeed;
this.type = type;
}

public int getMaxSpeed() {
return this.maxSpeed;
}

public String getType() {
return this.type;
}

public double calculateAllowedSpeed() {
return maxSpeed;
}
}

public class Car extends Vehicle {
public Car(int maxSpeed, String type) {
super(maxSpeed, type);
}
@Override
public double calculateAllowedSpeed() {
return super.maxSpeed * 0.8;
}
}

public class Bus extends Vehicle {
public Bus(int maxSpeed, String type) {
super(maxSpeed, type);
}
@Override
public double calculateAllowedSpeed() {
return super.maxSpeed * 0.8;
}
}

Liskov Substitution Principle

Derived/sub classes must be substitutable for their base/super classes.

The following code snippet shows Square-Rectangle problem.

public class Rectangle {
private int width;
private int height;

public void setWidth(int width) {
this.width = width;
}

public void setHeight(int height) {
this.height = height;
}

public int area() {
return this.width * this.height;
}
}

public class Square extends Rectangle {

@Override
public void setWidth(int width) {
super.width = width;
super.height = width;
}

@Override
public void setHeight(int height) {
super.width = height;
super.height = height;
}
}

Here, Square is a Rectangle, but it isn’t substitutable, because Rectangle has width and height, whereas Square has only length. Though we set width and height from Square, but sub-class is not substitutable with super class. Lets see the actual problem here-

Rectangle rectangle = new Square();
rectangle.setWidth(5);
rectangle.setHeight(10);

If we call rectangle.area() then as per rectangle behavior and LSP, it should return 50, however, it will return 100 as setHeight() set height and width both. So, the above example is not compatible with LSP.

To make it LSP compatible, we should create a new class, lets say, Quadrangle with an abstract method area() only, then this class can be implemented by both Square and Rectangle and implemented their own logic. Now, sub-class substitutable by super class.

public abstract class Quadrangle {
public abstract int area();
}
public class Rectangle extends Quadrangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}

@Override
public int area() {
return this.width * this.height;
}
}
public class Square extends Quadrangle {
private int length;

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

@Override
public int area() {
return this.length * this.length;
}
}

Interface Segregation Principle

Client should never be forced to implement an interface/method that it doesn’t use.

public interface Shape {
double area();
double volume();
}

public class Circle implements Shape {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

@Override
public double area() {
return 2 * 3.14 * radius;
}

@Override
public double volume() {
throw new UnsupportedOperationException();
}
}

public class Cube implements Shape {
private int edge;

public Cube(int edge) {
this.edge = edge;
}

@Override
public double area() {
return 6 * edge * edge;
}

@Override
public double volume() {
return edge * edge * edge;
}
}

This example shows that class Circle must implement volume method though Circle don’t have any volume. It’s clearly violation of ISP. Because it forced Circle to implement volume though it doesn’t need to implement. To make compatible with this design principle, we can separate Shape interface into two different interface like following.

public interface Shape {
double area();
}

public interface ThreeDShape {
double volume();
}

public class Circle implements Shape {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

@Override
public double area() {
return 2 * 3.14 * radius;
}
}

public class Cube implements Shape, 3DThreeDShape {
private int edge;

public Cube(int edge) {
this.edge = edge;
}

@Override
public double area() {
return 6 * edge * edge;
}

@Override
public double volume() {
return edge * edge * edge;
}
}

Now, Circle can only forced to implement that interface which it needs.

Dependency Inversion Principle

High-level module/class must not depend on the low-level module/class, but they should depend on abstractions.

First, we will check a class that depends on construction, later we will change it to depend on abstraction.

public class Car {
private PetrolEngine engine;

public Car(PetrolEngine engine) {
this.engine = engine;
}

public void start() {
this.engine.start();
}
}

public class PetrolEngine {
public void start() {
}
}

The Car class depends on low level PetrolEngine class — it depends on construction not abstraction. It violates DIP. To make it perfect, we can create new interface Engine and put that interface reference in Car class instead of PetrolEngine, now Car class only knows Engine, it would be any kind of EnginePetrolEngine, DieselEngine etc, so it can now work with other type Engine class. Check the changes —

public class Car {
private Engine engine;

public Car(Engine engine) {
this.engine = engine;
}

public void start() {
this.engine.start();
}
}

public interface Engine {
void start();
}

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

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

After the changes, high-level class Car is not depend on low-level PetrolEngine class anymore.

In this article, I tried to describe SOLID principle in a concise way. Thanks for reading!

--

--