SOLID for dummies with examples.

Amarjit Jha
11 min readSep 6, 2023

--

Let’s get the definition out of the way. They may help recall the idea during implementation. Each letter of SOLID acronym represents a design principle as defined below :

S = Single responsibility principle (SRP)
O = Open/Closed principle (OCP)
L = Liskov substitution principle (LSP)
I = Interface segregation principle (ISP)
D = Dependency inversion principle (DIP)

The idea is that these principles, when applied together, will result in systems that are easier to maintain and extendable over time.

TL;DR — Multiple examples are used to emphasise upon each principle.

This states that every class should do only one thing. Should have only a single responsibility. Refer to some of the examples below — which violates this principle and that is something that should be avoided.

1.This employee class below handles both employee information and file operations, which is a violation of the principle.

public class Employee {
private String name;
private String employeeId;
private double salary;

// Constructor, getters, and setters for employee information

public void saveToFile(String fileName) {
// Code to serialize employee data to a file
}

public Employee loadFromFile(String fileName) {
// Code to deserialize employee data from a file
}
}

Instead the Employee class could be refactored as below.

// Employee class responsible for employee information
public class Employee {
private String name;
private String employeeId;
private double salary;

// Constructor, getters, and setters for employee information
// Public private methods
}

// EmployeeFileHandler class responsible for file operations
public class EmployeeFileHandler {
public void saveToFile(Employee employee, String fileName) {
// Code to serialize employee data to a file
}
public Employee loadFromFile(String fileName) {
// Code to deserialize employee data from a file
}
}

Employee — class made responsible to only manage employee information

EmployeeFileHandler — new class created to move the file handling I/O operations related to employee.

2.This order class below violates the principle because it combines two responsibilities: Processing the order and sending email notifications.

public class Order {
private int orderId;
private Date orderDate;
private List<Item> items;
private double totalAmount;

// Constructor, getters, and setters for order information

public void processOrder() {
// Code to process the order
}

public void sendConfirmationEmail() {
// Code to send an email confirmation to the customer
}
}

Instead it could be refactored as below :

// Order class responsible for order information
public class Order {
private int orderId;
private Date orderDate;
private List<Item> items;
private double totalAmount;

// Constructor, getters, and setters for order information
public void processOrder() {
// Code to process the order
}
}

// EmailService class responsible for sending email notifications
public class EmailService {
public void sendConfirmationEmail(Customer customer, Order order) {
// Code to send an email confirmation to the customer
}
}

Order — class made responsible to only manage the order information and order related methods.

EmailService — new class created to handle the responsibility of sending the email notifications.

The OCP suggests that software entities (such as classes, modules, or functions) should be open for extension but closed for modification. In other words, you should be able to add new functionality to a system without altering existing code.

1.This following class ShapeCalculator that calculates the area of different shapes violates the OCP because it is not open for extension without modifying its existing code.

If you want to add area calculation for a new shape (e.g., a circle), you would need to modify the ShapeCalculator class, which is not ideal.

public class ShapeCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.getWidth() * rectangle.getHeight();
} else if (shape instanceof Triangle) {
Triangle triangle = (Triangle) shape;
return 0.5 * triangle.getBase() * triangle.getHeight();
}
return 0;
}
}

Instead we can use an interface or an abstract class to define a common contract for shapes, and then create separate classes for each shape that implements this contract. Additionally, we can use polymorphism to calculate the area of any shape without modifying the existing code.

// Define a common interface for shapes
public interface Shape {
double calculateArea();
}

// Rectangle class implementing the Shape interface
public class Rectangle implements Shape {
private double width;
private double height;

// Constructor, getters, and setters

@Override
public double calculateArea() {
return width * height;
}
}

// Circle class implementing the Shape interface
public class Circle implements Shape {
private double radius;

// Constructor, getters, and setters

@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}

// Triangle class implementing the Shape interface
public class Triangle implements Shape {
private double base;
private double height;

// Constructor, getters, and setters

@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
  1. We define a common Shape interface with a calculateArea method.
  2. Each shape class (e.g., Rectangle, Circle, Triangle) implements this interface and provides its own implementation of the calculateArea method.
  3. The ShapeCalculator class can now work with any class that implements the Shape interface, making it open for extension. If you want to add a new shape, you can create a new class that implements the Shape interface without modifying the existing code in the ShapeCalculator class.

Lets look at the second example which adopts a slightly different pattern to make it even better.

2.The following DiscountCalculator class which calculates discount for various products based on their type violates the OCP because adding a new product type of changing discount calculation requires modifying existing code, which is not open for extension.

public class DiscountCalculator {
public double calculateDiscount(Product product) {
double discount = 0;
switch (product.getType()) {
case "Book":
discount = product.getPrice() * 0.1;
break;
case "Electronics":
discount = product.getPrice() * 0.05;
break;
case "Clothing":
discount = product.getPrice() * 0.2;
break;
}
return discount;
}
}

Instead in combination to previous example pattern, we can use a Strategy Pattern by defining a common interface for discount calculation and creating separate classes for each product type that implements this interface.

// Define a common interface for discount calculators
public interface DiscountStrategy {
double calculateDiscount(double price);
}

// Create separate classes for each product type
// implementing the DiscountStrategy interface
public class BookDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double price) {
return price * 0.1;
}
}

public class ElectronicsDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double price) {
return price * 0.05;
}
}

public class ClothingDiscount implements DiscountStrategy {
@Override
public double calculateDiscount(double price) {
return price * 0.2;
}
}
// Modify the DiscountCalculator to use the strategy pattern
public class DiscountCalculator {
private DiscountStrategy discountStrategy;

public DiscountCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}

public double calculateDiscount(double price) {
return discountStrategy.calculateDiscount(price);
}
}

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

In other words, a subclass should be able to substitute for its superclass in any context without any problems. This means that the subclass must behave in a way that is consistent with the behaviour of the superclass.

This principle probably is the most difficult to get. It is very important to pay attention to words used such as the ones made bold in the above paragraph.

It should be consistent with the behaviour of the superclass. Semantics should not change.

Let see examples.

1.Observe this simple classic example.

class Animal {
public void walk() {
System.out.println("I am walking");
}
}

class Dog extends Animal {
@Override
public void walk() {
System.out.println("I am walking and barking");
}
}

Technically nothing wrong with the way Animal class is extended and walk method is overridden in the Dog class.

But if you notice it has changed the behaviour of the walk method. See the program below :

public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
animal.walk(); // Prints "I am walking and barking"
}
}

In general we understand that all animals can walk but not all animals can bark. The Animal class was defined with this idea. So in the ideal scenario we expect that when we check walk method of the animal instances it should perform the task walking nothing else.

But the moment Animal object is substituted with Dog instance, the animal instance prints that it is barking along with walking, which violates the LSP here.

To fix the problem, Dog class should have separated the barking task as below :

class Dog extends Animal {
@Override
public void walk() {
System.out.println("I am walking");
}

public void bark() {
System.out.println("Woof!");
}
}

Remember, not to confuse this with the output of the method. Understand the semantics here. The behaviour should not change.

2.Let see the next example of LSP. See the program below :

interface Calculator {
public int add(int a, int b);
}

class SimpleCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a + b; // Conforms to LSP
}
}

class AdvancedCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a * b; // Violates LSP
}
}

public class Main {
public static void main(String[] args) {
Calculator a_calculator = new SimpleCalculator();
int sum = a_calculator.add(5, 10); // Output is summation of 2 nums

Calculator b_calculator = new AdvancedCalculator();
int mul = b_calculator.add(5, 10); // Violates LSP : Multiplies numbers

System.out.println( sum + " : " + mul );
}
}

Observe when the calculator instance a_calculator replaced with instance of SimpleCalculator it conforms to LSP but violates when replaced with instance of AdvancedCalculator because the add method of advance calculator has completely different behaviour. The idea was for the method to perform addition, instead it does multiplication.

3.Consider the third and final example. One of the example which was used above in OCP, which is repurposed for this example.

This program below conforms to LSP as the sub-classes Rectangle, Circle, and Triangle all implement the calculateArea() method in a way that is consistent with the behavior of the method in the Shape interface.

// Define a common interface for shapes
interface Shape {
double calculateArea();
}

// Rectangle class implementing the Shape interface
class Rectangle implements Shape {
private double width;
private double height;

public Rectangle( double _width, double _height) {
width = _width;
height = _height;
}

@Override
public double calculateArea() {
return width * height;
}
}

// Circle class implementing the Shape interface
class Circle implements Shape {
private double radius;

public Circle( double _radius) {
radius = _radius;
}

@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}

// Triangle class implementing the Shape interface
class Triangle implements Shape {
private double base;
private double height;

public Triangle( double _base, double _height) {
base = _base;
height = _height;
}

@Override
public double calculateArea() {
return 0.5 * base * height;
}
}

public class Main {
public static void main(String[] args) {

Rectangle rect = new Rectangle( 10, 10);
System.out.println(rect.calculateArea());

Circle circle = new Circle(1);
System.out.println(circle.calculateArea());

Triangle tngle = new Triangle( 100, 100);
System.out.println(tngle.calculateArea());


Shape shape = rect;
System.out.println(shape.calculateArea());


}
}

Shape instance can be substituted with any other instances and the program with stay consistent.

No client should be forced to depend on methods it does not use.

In other words, an interface should not expose methods that are not needed by all of its clients. This principle helps to keep code decoupled and easier to maintain.

1.See the program below which defines an interfaceEmployee with 3 method

public interface Employee {
void work();
void eatLunch();
void takeBreak();
}

public class Developer implements Employee {
@Override
public void work() {
// Developer-specific work implementation
}

@Override
public void eatLunch() {
// Developer-specific lunch break implementation
}

@Override
public void takeBreak() {
// Developer-specific break implementation
}
}

In this version, the Developer class implements the entire Employee interface, which includes methods for tasks that may not be relevant to all types of employees (e.g., managers may not be entitled to eatLunch() or takeBreak() ( pun intended 🤣 ).

This violates the Interface Segregation Principle because it forces all classes implementing Employee to provide implementations for methods that they may not need.

Instead we can optimise this as follows :

// Define smaller interfaces based on employee tasks
public interface Workable {
void work();
}

public interface Lunchable {
void eatLunch();
}

public interface Breakable {
void takeBreak();
}

// DEVELOPER class
public class Developer implements Workable, Lunchable, Breakable {
@Override
public void work() {
// Worker-specific work implementation
}

@Override
public void eatLunch() {
// Worker-specific lunch break implementation
}

@Override
public void takeBreak() {
// Worker-specific break implementation
}
}

// MANAGER class
public class Manager implements Workable {
@Override
public void work() {
// Manager-specific work implementation
}
}

Notice the interface Employee broken down into smaller interfaces Workable, Lunchable and Breakable.

The classes Developer and Manager implements only the interfaces that are relevant to its role. Which conforms to ISP.

Moving on to final principle :

This principle states that High-level modules should not depend on low-level modules. Both should depend on abstractions. It promotes the use of interfaces or abstract classes to decouple components and make the system more flexible and easy to maintain.

1.Lets understand with example. See the violation of the principle and the corrected

The Switch class below directly creates an instance of the the LightBulb class within the constructor. This violates the DIP because the high-level module eg. Switch should not depend on low-level module eg. LightBulb.

public class LightBulb {
public void turnOn() {
// Code to turn on the light bulb
}

public void turnOff() {
// Code to turn off the light bulb
}
}

public class Switch {
private LightBulb bulb;

public Switch() {
this.bulb = new LightBulb(); // Violation of DIP
}

public void press() {
if (bulb.isOn()) {
bulb.turnOff();
} else {
bulb.turnOn();
}
}
}

Instead both should depend on abstraction. See the fix below :

// Define an abstraction for the LightBulb
public interface Bulb {
void turnOn();
void turnOff();
boolean isOn();
}

public class LightBulb implements Bulb {
private boolean isOn = false;

@Override
public void turnOn() {
isOn = true;
}

@Override
public void turnOff() {
isOn = false;
}

@Override
public boolean isOn() {
return isOn;
}
}

public class Switch {
private Bulb bulb;

public Switch(Bulb bulb) {
this.bulb = bulb; // Adheres to DIP
}

public void press() {
if (bulb.isOn()) {
bulb.turnOff();
} else {
bulb.turnOn();
}
}
}

An interface called Bulb introduced for an abstraction and Switch class is made to depend on the Bulb abstraction through its constructor injection. This design conforms to DIP.

If you have made it here, probably you were hooked. Don’t forget to give a clap before you go and I would love to respond to any question/comment or concern that you may have.

Cheers.
Amarjit.

--

--