Guide to The SOLID Principles In Java

Overview

If you don’t have much time, this table captures the core of the SOLID principles:

PrincipleDescription
Single Responsibility Principle (SRP)Clients should not be forced to depend on interfaces they do not use. This principle promotes the idea of small, focused interfaces instead of large, monolithic interfaces. Clients should only be exposed to the methods they require, reducing the impact of changes and increasing reusability. One example is having an interface called Bird and a method fly then creating a Duck class implementing that interface. Obviously, Duck cannot fly. A better design is to create Flyable and Unflyable interfaces extending Bird and make Duck implement Unflyable
Open/Closed Principle (OCP)A class should have only one reason to change. That means a class should have only one responsibility or job and not be responsible for multiple unrelated tasks. The purpose is to achieve high cohesion and maintainability.
Liskov Substitution Principle (LSP)Subtypes must be substitutable for their base types. It ensures that objects of a superclass can be replaced with objects of its subclasses without affecting the correctness of the program.
Interface Segregation Principle (ISP)Clients should not be forced to depend on interfaces they do not use. This principle promotes the idea of small, focused interfaces instead of large, monolithic interfaces. Clients should only be exposed to the methods they require, reducing the impact of changes and increasing reusability. One example is having an interface called Bird and a method fly then creating a Duck class implementing that interface. Obviously Duck cannot fly. A better design is to create Flyable and Unflyable interfaces extending Bird and make Duck implement Unflyable
Dependency Inversion Principle (DIP)High-level modules should not depend on low-level modules. Both should depend on abstractions. It promotes loose coupling and decoupling between modules by introducing abstractions (interfaces or abstract classes) that define the contract between them. Dependencies should be based on abstractions rather than concrete implementations.

SOLID principles violations

Before learning how to do it right, we need to know how to do it wrong.

Here are the violations of each principle:

Single Responsibility violation

Consider this code:

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

    public void calculateSalary() {
        // Code to calculate the salary
    }

    public void saveToDatabase() {
        // Code to save the employee details to the database
    }

    public void sendNotification() {
        // Code to send a notification to the employee
    }
}

This class has more than one reason to change.

If you change the database, probably saveToDatabase need to change.

If you want to change the notification, probably sendNotification need to change.

Ideally, this Employee class need to do just one thing: Represent the employee’s information.

Open/Close Violation

Consider this class:

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

    public double calculateSalary() {
        if (type.equals("FullTime")) {
            // Calculation logic for full-time employees
        } else if (type.equals("PartTime")) {
            // Calculation logic for part-time employees
        } else if (type.equals("Contractor")) {
            // Calculation logic for contractors
        }
        // ...
    }
}

This class has been evolving to support multiple types of employees. The calculateSalary method is a classic example of the open/close principle.

Open/Close Violation’s Fix

Here is one possible fix to conform to the open/close principle:

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

    public abstract double calculateSalary();
}

public class FullTimeEmployee extends Employee {
    // Implementation of calculateSalary() for full-time employees
}

public class PartTimeEmployee extends Employee {
    // Implementation of calculateSalary() for part-time employees
}

public class ContractorEmployee extends Employee {
    // Implementation of calculateSalary() for contractors
}

Liskov Substitution Principle’s Violation

Your code violates the LSP when you declare a function that accepts a base class but cannot handle a sub-type of that base class. Let’s consider this example:

interface Bird {
    void fly();
}

class Penguin implements Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
    
    void swim() {
        System.out.println("I can swim!");
    }
}

class TestBird {
  
 
  public void watchBirdFly(Bird bird) {
    bird.fly();
  }
  
  
  public static void main(String[] args) {
  	var penguin = new Penguin();
    watchBirdFly(penguin);//Throws an exception
  }
}

In this code, your watchBirdFly declares that it would accept any instance of the Bird class. However, it cannot handle a Penguin (which is a subtype of the Bird class).

Liskov Substitution Principle’s Violation’s Fix

The issue above was caused by using a *fat* interface. Instead of letting Penguin extend Bird, you need to create interfaces that represent the nature of birds more closely. For example, in this case, I would create a FlyingBird interface and a SwimmingBird interface that extend the Bird interface.

interface Bird {
}
interface FlyingBird {
  void fly();
}

interface SwimmingBird {
 void swim(); 
}

class Penguin implements SwimmingBird {

	@Override    
    void swim() {
        System.out.println("I can swim!");
    }
}

class TestBird {
  
 
  public void watchBirdFly(FlyingBird bird) {
    bird.fly();
  }
  public void watchBirdSwim(SwimmingBird bird) {
    bird.swim();
  }
  
  public static void main(String[] args) {
  	var penguin = new Penguin();
    watchBirdSwim(penguin);//Works fine
  }
}

Interface Segregation Principle’s Violation

An Interface Segregation Principle (ISP) violation happens when a class is forced to implement an interface that includes methods it does not use or need. This will lead to bloated code, increased complexity, and a higher chance of bugs due to the implementation of irrelevant methods.

For example, you are implementing a Device interface.

interface MultiFunctionDevice {
    void print();
    void scan();
    void fax();
}

class SimplePrinter implements MultiFunctionDevice {
    public void print() {
        // Printing logic
        System.out.println("Print document");
    }

    public void scan() {
        // Not supported, but still has to be implemented
        throw new UnsupportedOperationException("Scan not supported");
    }

    public void fax() {
        // Not supported, but still has to be implemented
        throw new UnsupportedOperationException("Fax not supported");
    }
}

As you can see, in this case, the printer is forced to implement the methods of fax and scan, which aren’t its function.

The solution for this violation would be to separate the MultiFunctionDevice into multiple interfaces:

interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

interface Fax {
    void fax();
}

class SimplePrinter implements Printer {
    public void print() {
        // Printing logic
        System.out.println("Print document");
    }
}

class MultiFunctionPrinter implements Printer, Scanner, Fax {
    public void print() {
        // Print logic
        System.out.println("Print document");
    }

    public void scan() {
        // Scan logic
        System.out.println("Scan document");
    }

    public void fax() {
        // Fax logic
        System.out.println("Fax document");
    }
}

Now, the SimplePrinter can implement only the function it supports. For a more complex device, all three functions are implemented.

Dependency Inversion Principle’s violation

Dependency Inversion Principle (DIP) says that high-level modules should not depend on low-level modules; both should depend on abstractions. In addition, abstractions should not depend on details; details should depend on abstractions. The purpose of this principle is to reduce the coupling between software modules to make systems easier to maintain and extend.

To understand this principle, we need to clarify that the level mentioned here is the level of abstraction. The high-level module is more generalized, and more abstract and the low-level modules are more specific, and closer to the implementation.

Take the following example:

class Lamp {
    public void turnOn() {
        System.out.println("Lamp turned on");
    }

    public void turnOff() {
        System.out.println("Lamp turned off");
    }
}

class Button {
    private Lamp lamp;

    public Button(Lamp lamp) {
        this.lamp = lamp;
    }

    public void toggle() {
        // Some toggle logic
        // This directly couples the Button to a Lamp, violating DIP
        lamp.turnOn();
        // ...
        lamp.turnOff();
    }
}

Here, the high level module (Button) depends directly on the low level module Lamp. However, a Lamp is not the only module that the button can action. We can switch on and off a fan or a laptop, for example. Thus the Button module should depend on the abstraction of the switch on/off function.

This code fixes the violation:

// Abstraction that both high-level and low-level modules depend on
interface Switchable {
    void turnOn();
    void turnOff();
}

// Low-level module implementation
class Lamp implements Switchable {
    public void turnOn() {
        System.out.println("Lamp turned on");
    }

    public void turnOff() {
        System.out.println("Lamp turned off");
    }
}

// High-level module
class Button {
    private Switchable device; // Depends on abstraction, not on concrete Lamp

    public Button(Switchable device) {
        this.device = device;
    }

    public void toggle() {
        // Toggle logic
        // Can turn on/off any device, not just a lamp
        device.turnOn();
        // ...
        device.turnOff();
    }
}

// Usage
Switchable myLamp = new Lamp();
Button button

Here, by defining an interface called Switchable, the Button module can act on any low-level module that implements this interface, not just the Lamp module.

Conclusion

In this post, I introduced you to the SOLID principles. I also gave examples of the violations of each principle and how to fix them.

Leave a Comment