SOLID Design Principle Explained with Java

Introduction

SOLID design principle is one of the most popular set of design principles in object-oriented software development. It’s a mnemonic acronym for the following five design principles:

  • S – Single Responsiblity Principle
  • O – Open Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

Now I will explain in details on each of the above design principle with example.

Single Responsibility Principle

The most important features of single responsibility principle are given below:

  • A class should have one and only one reason to change, meaning that a class should do only one job.
  • It is one of the basic principles most developers apply to build robust and maintainable software.
  • The most important benefit you get is it makes your software easier to implement and prevents unexpected side-effects of future changes.

Let’s take an example, let’s say you have below shape classes – Circle and Square and you want to sum all of the calculated area for each shape.

The code for Circle class is given below:

public public class Circle {
	private double radius;
	public Circle(double radius) {
		this.radius = radius;
	}
	public double getRadius() {
		return radius;
	}
}

The code for Square class is given below:

public class Square {
	private double length;
	public Square(double length) {
		this.length = length;
	}
	public double getLength() {
		return length;
	}
}

Now you move on by creating the AreaCalculator class and then write up your logic to sum up the areas for all provided shapes.

public class AreaCalculator {
	private Object[] shape;
	public AreaCalculator(Object[] shape) {
		this.shape = shape;
	}
	public double calculateTotalArea() {
		double sum = 0;
		for (int i = 0; i < shape.length; i++) {
			if (shape[i] instanceof Circle) {
				Circle circle = (Circle) shape[i];
				double radius = circle.getRadius();
				sum += 3.14 * radius * radius;
			} else if (shape[i] instanceof Square) {
				Square square = (Square) shape[i];
				double length = square.getLength();
				sum += length * length;
			}
		}
		return sum;
	}
	public void display() {
		System.out.println("Calculated Total Area: " + calculateTotalArea());
	}
}

To use the AreaCalculator class, you simply instantiate the class and pass in an array of shapes, and display the output of the total calculated area.

Now you create the below class SolidTest to test the calculated total area for the given shapes.

public class SolidTest {
	public static void main(String[] args) {
		Circle circle = new Circle(5.0d);
		Square square = new Square(5);
		Object[] arr = new Object[] { circle, square };
		AreaCalculator areaCalculator = new AreaCalculator(arr);
		areaCalculator.display();
	}
}

Now look at the class AreaCalculator, it does multiple responsibilities – summing up all areas for shapes as well as displaying total areas.

Now the problem with the display() method is that the AreaCalculator handles the logic to display the data.

Therefore,

What if the user wanted to display the data as JSON format or something else?

All of that logic would be handled by the AreaCalculator class, this is what SRP (Single Responsibility Principle) complains against; the AreaCalculator class should only sum the areas for the given shapes, it should not care whether the user wants to display in JSON or in HTML format.

Therefore there is a reason to break down the class AreaCalculator and it should do only one job – calculating areas.

To fix this problem, you can create an AreaCalculatorOutputter class and use this to handle whatever logic you need to handle how the sum areas of all given shapes are displayed.

public class AreaCalculatorOutputter {
	private double totalArea;
	public AreaCalculatorOutputter(double totalArea) {
		this.totalArea = totalArea;
	}
	public String json() {
		return totalArea + " in json format";
	}
	public String html() {
		return totalArea + " in html format";
	}
	public String xml() {
		return totalArea + " in xml format";
	}
}

Remove the display() method from AreaCalculator class. The updated AreaCalculator class is shown below:

public class AreaCalculator {
	private Object[] shape;
	public AreaCalculator(Object[] shape) {
		this.shape = shape;
	}
	public double calculateTotalArea() {
		double sum = 0;
		for (int i = 0; i < shape.length; i++) {
			if (shape[i] instanceof Circle) {
				Circle circle = (Circle) shape[i];
				double radius = circle.getRadius();
				sum += 3.14 * radius * radius;
			} else if (shape[i] instanceof Square) {
				Square square = (Square) shape[i];
				double length = square.getLength();
				sum += length * length;
			}
		}
		return sum;
	}
}

Now updated SolidTest class looks like below:

public class SolidTest {
	public static void main(String[] args) {
		Circle circle = new Circle(5.0d);
		Square square = new Square(5);
		Object[] arr = new Object[] { circle, square };
		AreaCalculator areaCalculator = new AreaCalculator(arr);
		double totalArea = areaCalculator.calculateTotalArea();
		AreaCalculatorOutputter areaCalculatorOutputter = new AreaCalculatorOutputter(totalArea);
		System.out.println(areaCalculatorOutputter.json());
		System.out.println(areaCalculatorOutputter.html());
		System.out.println(areaCalculatorOutputter.xml());
	}
}

Now, whatever logic you need to display the data to the user is now handled by the AreaCalculatorOutputter class.

With this solution, you have some classes but each class with a single responsibility, so you get a low coupling and a high cohesion.

Open Closed Principle

The Open Closed Principle states that:

Software entities, classes, methods etc. should be open for extension but closed for modification.

To put this more concretely, you should write a class that does what it needs to flawlessly and not assuming that people should come in and change its source code later. Its source code is closed for modification, but modification to the entities behavior can be achieved by extending its features, for instance, by inheriting from it and overriding or extending certain behaviors.

In the previous section’s example, let’s take a look at the AreaCalculator class, especially it’s calculateTotalArea() method.

If you wanted the calculateTotalArea() method to be able to sum areas of more shapes, you would have to add more if/else blocks.

As new types of shapes come to calculate area, the AreaCalculator class will be more confusing and fragile to changes.

Therefore the AreaCalculator class is not closed for modification and that goes against the Open-closed principle.

A way you can make the shape as abstract class or interface and remove the logic to calculate the area of each shape out of the calculateTotalArea() method and attach it to the shape’s class.

You can create below Shape interface or if you want you may also create abstract class:

public interface Shape {
	double area();
}

Now updated Circle class is given below:

public class Circle implements Shape {
	private double radius;
	public Circle(double radius) {
		this.radius = radius;
	}
	@Override
	public double area() {
		return 3.14 * radius * radius;
	}
}

Similarly, updated Square class is given below:

public class Square implements Shape {
	private double length;
	public Square(double length) {
		this.length = length;
	}
	@Override
	public double area() {
		return length * length;
	}
}

Now look at the below AreaCalculator class:

public class AreaCalculator {
	private Shape[] shapes;
	public AreaCalculator(Shape[] shapes) {
		this.shapes = shapes;
	}
	public double calculateTotalArea() {
		double sum = 0;
		for (int i = 0; i < shapes.length; i++) {
			sum += shapes[i].area();
		}
		return sum;
	}
}

Now the AreaCalculator class remains intact when we add a new shape type. The existing code is not modified.

So if you want to add more types of shapes you just have to create a class for that shape.

Now calculate the total area of all shapes:

public class SolidTest {
	public static void main(String[] args) {
		Circle circle = new Circle(5.0d);
		Square square = new Square(5);
		Shape[] arr = new Shape[] { circle, square };
		AreaCalculator areaCalculator = new AreaCalculator(arr);
		double totalArea = areaCalculator.calculateTotalArea();
		AreaCalculatorOutputter areaCalculatorOutputter = new AreaCalculatorOutputter(totalArea);
		System.out.println(areaCalculatorOutputter.json());
		System.out.println(areaCalculatorOutputter.html());
		System.out.println(areaCalculatorOutputter.xml());
	}
}

Now when you run the above main class, you will see below output:

103.5 in json format
103.5 in html format
103.5 in xml format

Liskov Substitution Principle

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

All this stating is that every subclass/derived class should be substitutable for their base/parent class.

It ensures that derived classes do not affect the behavior of the classes they are inheriting from. Or put another way, that any derived class can take the place of its parent class.

Let’s consider the below example.

You have a below Person class:

public class Person {
    void walkInsideRoom() {
        System.out.println("Walk inside the room");
    }
    void walkOutsideRoom() {
        System.out.println("Walk outside the room");
    }
}

Now, a prisoner is obviously a person. So logically, a sub-class can be created:

public class Prisoner extends Person {
    void walkOutsideRoom() {
        throw new RuntimeException("Cannot walk outside the room");
    }
}

Also obviously, this leads to trouble, since a prisoner is not free to move an arbitrary distance in any direction, yet the contract of the Person class states that a Person can.

Thus, the class Person could better be named FreePerson. If that were the case, then the idea that class Prisoner extends FreePerson is clearly wrong.

As per the inheritance hierarchy the Person object can point to any one of its child objects and you do not expect any unusual behavior.

But when walkOutsideRoom() method of the Prisoner object is invoked it leads to below error because our Prisoner object does walkOutsideRoom() the room but they are not allowed to.

So Liskov principle says the parent should easily replace the child object.

Therefore to implement Liskov you need to create two interfaces – one is for walk inside and other for walk outside as shown below.

The below interface allows walking inside

public interface InsideRoom {
    void walk();
}

The below interface allows walking outside:

public interface OutsideRoom {
    void walk();
}

Now the Prisoner class will only implement InsideRoom interface as he/she is not allowed to go outside the room.

public class Prisoner implements InsideRoom {
    @Override
    public void walkInside() {
        System.out.println("Walk inside the room");
    }
}

While the Person class will implement both InsideRoom interface as well as OutsideRoom as he/she is free to walk anywhere.

public class Person implements InsideRoom, OutsideRoom {
    @Override
    public void walkInside() {
        System.out.println("Walk inside the room");
    }
    @Override
    public void walkOutside() {
        System.out.println("Walk outside the room");
    }
}

Therefore from the above situation you got to know that this strongly suggests that inheritance should never be used when the sub-class restricts the freedom implicit in the base class, but should only be used when the sub-class adds extra detail to the concept represented by the base class.

Interface Segregation Principle

Clients should never be forced to implement interfaces that they don’t use.

In other words, make fine grained interfaces that are client specific.

In our previous example while I was discussing about SRP and OCP, then I used Shape interface to give the solution.

Now let’s say new clients come up with a demand that the client wants to add new method that will calculate volume for shapes in addition to area calculation.

So you enthusiastically update the Shape by adding a new method called volume.

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

Any shape you create must implement the volume method, but you know that squares or circles are flat shapes and they do not have volumes, so this interface would force the Square or Circle class to implement a method that has no use.

It goes against ISP (Interface Segregation Principle) because you are forcing them to use the volume method, instead you could create another interface called SolidShape that has the volume contract and solid shapes like cubes, spheres etc. can implement this interface:

public interface SolidShape {
    double volume();
}

Now let’s say a solid shape Cude implements SolidShape interface:

public class Cube implements SolidShape {
    private double side;
    public Cube(double side) {
        this.side = side;
    }
    @Override
    public double volume() {
        return side * side * side;
    }
}

Another solid shape Sphere implements SolidShape interface:

public class Sphere implements SolidShape {
    private double radius;
    public Sphere(double radius) {
        this.radius = radius;
    }
    @Override
    public double volume() {
        return (4 / 3) * 3.14 * radius * radius * radius;
    }
}

Now if you want to calculate area as well volume of a specific shape, such as, Cuboid then you can create class that implements both Shape and SolidShape interfaces:

public class Cuboid implements Shape, SolidShape {
    private double height;
    private double width;
    private double length;
    public Cuboid(double height, double width, double length) {
        this.height = height;
        this.width = width;
       this.length = length;
    }
    @Override
    public double volume() {
        return height * width * length;
    }
    @Override
    public double area() {
        return 2 * (width * length + width * height + length * height);
    }
}

Therefore, by breaking down interfaces, you favor Composition instead of Inheritance, and Decoupling over Coupling.

You favor composition by separating by roles(responsibilities) and decoupling by not coupling derivative classes with unneeded responsibilities inside a monolith.

Dependency Inversion Principle

Entities must depend on abstractions not on concretions.

It means that the high level module must not depend on the low level module, but they should depend on abstractions.

Let’s say you have a system that handles authentication through external services such as Google, Facebook, Twitter etc. You would have a class for each service: GoogleAuthenticationService, GitHubAuthenticationService, etc.

To be able to make use of all the services, you have two possibilities: either write a piece of code that adapts each service to the authentication process, or define an abstraction of the authentication services.

The first possibility is a dirty solution that will potentially introduce technical debt in the future as shown in the below example; in case a new authentication service is to be integrated to the system, you will need to change the code, which as a result violates the OCP.

public class GoogleAuthenticationService {
}
public class Authenticator {
    private GoogleAuthenticationService authenticationService;
    public Authenticator(GoogleAuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }
}

First the GoogleAuthenticationService is the low level module while the Authenticator is high level, but according to the definition of D in S.O.L.I.D. which states that “depend on Abstraction but not on Concretions”, this snippet above violates this principle as the Authenticator class is being forced to depend on the GoogleAuthenticationService class.

Later if you were to change the authentication service, you would also have to edit the Authenticator class and thus violates Open Close Principle.

The Authenticator class should not care what authentication service your application uses, to fix this again you need to “code to an interface”, since high level and low level modules should depend on abstraction.

This second possibility is much cleaner, it allows for future addition of services, and changes can be done to each service without changing the integration logic.

By defining a AuthenticationService interface and implementing it in each service, you would then be able to use Dependency Injection in our authentication logic.

public interface AuthenticationService {
    boolean isAuthenticated(Object user);
}

The interface has a isAuthenticated() method and the GoogleAuthenticationService class implements this interface, also instead of directly type-hinting GoogleAuthenticationService class in the constructor of the Authenticator, you instead type-hint the interface and no matter the type of authentication service your application uses, the Authenticator class can easily authenticate the user without any problems and OCP is not violated.

public class GoogleAuthenticationService implements AuthenticationService {
    @Override
    public boolean isAuthenticated(Object user) {
        // return true if user is authenticated
        return false;
    }
}

Now the updated Authenticator class should look like this:

public class Authenticator {
    private AuthenticationService authenticationService;
    public Authenticator(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }
}

According to the little snippet above, you can now see that both the high level and low level modules depend on abstraction.

By depending on higher-level abstractions, you can easily change one instance with another instance in order to change the behavior.

Dependency Inversion increases the reusability and flexibility of our code.

That’s all about SOLID design principle.

Leave a Reply

Your email address will not be published. Required fields are marked *