SOLID principles in Java
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 Engine
— PetrolEngine
, 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!