SOLID Principles in Java by Example

There are a lot of articles about the SOLID principles. But usually a different example for a particular principle is to be found. Instead, would it be nice to demonstrate all of them on a single code snippet?


We aren’t going to drive deep into the theory, as a lot was already written. We are interested mainly in code!

Consider a simple payroll component, an example very favored by Uncle Bob himself:

abstract class Employee {

    private final static Map<String, Employee> registry = new HashMap<>();

    protected final String personalId;
    protected final String firstName;
    protected final String lastName;

    /** constructor */

    public String fullName() {
        return String.format("%s %s", firstName, lastName);
    }

    public void register() {
        registry.put(personalId, this);
    }

    public boolean isRegistered() {
        return registry.containsKey(personalId);
    }
}

class Paycheck {

    private final Employee employee;

    /** constructor */

    public double amount() {
        if (employee instanceof Manager) {
            return 2000.0;
        }
        if (employee instanceof Developer) {
            return 1000.0;
        }
        return 0.0;
    }
}

Single Responsibility Principle (SRP)

The SRP is about cohesion, it says that a component (function, method, class, module) should have only one reason to change.

Our code breaks the SRP as any change in the persistence mechanism would require a change of the Employee code. For instance, a timestamp of the registration is persisted as well, etc.

abstract class Employee {
    /** ... */
    
    private final static Map<String, Employee> registry = new HashMap<>();
    
    public void register() {
        registry.put(personalId, this);
    }
    
    public boolean isRegistered() {
        return registry.containsKey(personalId);
    }
}

To fix that we introduce a new specialized class EmployeeRegistry and put the persistence functionality into it.

class EmployeeRegistry {

    private final static Map<String, Employee> map = new HashMap<>();

    public void register(Employee employee) {
        map.put(employee.personalId, employee);
    }

    public boolean isRegistered(Employee employee) {
        return map.containsKey(employee.personalId);
    }
}

Now, we just delegate the request:

abstract class Employee {
    /** ... */
    
    private final static EmployeeRegistry registry = new EmployeeRegistry();
    
    public void register() {
        registry.register(this);
    }
    
    public boolean isRegistered() {
        return registry.isRegistered(this);
    }
}

We can do even better with the Dependency Inversion Principle, stay tuned.

Open-Closed Principle (OCP)

The OCP states that software components should be open for extension, but closed for modification. It means that we should extend the system functionality by adding new components rather than modifying the existing ones.

With a new Employee subtype, calculating of the amout in the Paycheck must be modified:

public double amount() {
    if (employee instanceof Manager) {
        return 2000.0;
    }
    if (employee instanceof Developer) {
        return 1000.0;
    }
    return 0.0;
}

Better would be to create a method inside the Employee to return the salary for the paycheck:

abstract class Employee {
    /** ... */
    
    public abstract double salary();
}

class Manager extends Employee {
    /** ... */
    
    @Override
    public double salary() {
        return 2000.0;
    }
}

Now, the Paycheck could be simplified and will work with any additional Employee subclass.

class Paycheck {
    /** ... */
    
    public double amount() {
        return employee.salary();
    }
}

By the way, using instanceof violates the Liskov Substitution Principle, too. Avoid instanceof at any price!

Liskov Substitution Principle (LSP)

The LSP says that an object of type T should be replaceable with its subtypes S without affecting the correctness of the program P.

Although the most of possible violations of LSP are in strongly typed languages caught by the compiler (return types, proper parameter subtypes, etc.), there are still invariants and contracts to take care of.

From the definition, a volunteer has to salary; it actually makes no sense to talk about any salary:

class Volunteer extends Employee {
    /** ... */
    
    @Override
    public double salary() {
        throw new RuntimeException("No salary for volunteers!");
    }
}

But what happends to the Paycheck (P) when we try to calculate the amount for a volunteer (S)?

double amount = new Paycheck(
    new Volunteer("001", "John", "Smith")
).amount();

Unsurprisingly, an exception occurs. The exception breaks the contract we provided via the Employee to the Paycheck as not declared in the method signature. We have to fix it:

class Volunteer extends Employee {
    /** ... */
    
    @Override
    public double salary() {
        return 0.0;
    }
}

Interface Segregation Principle (ISP)

The ISP says that no client should be forced to depend on methods it does not use.

A volunteer is actually not a payed employee and implementing it like that violates a business invariant. We can fix it with the ISP:

interface PayedEmployee {

    double salary();
}

class Manager extends Employee implements PayedEmployee {
    /** ... */

    @Override
    public double salary() {
        return 2000.0;
    }
}

After the PayedEmployee was introduced, the method salary() has disappeared from the Volunteer and the Employee itself.

The Paycheck depends only on what it really needs now:

class Paycheck {

    private final PayedEmployee employee;

    /** ... */
}

Dependency Inversion Principle (DIP)

The DIP states that high level modules should not depend on low level modules; both should depend on abstractions and abstractions should not depend on details.

We have introduced the EmployeeRegistry to separate the persistence mechanism from the other code, but the class is still concrete and it's a hard dependency of the Employee.

What happends if a different implementation is needed? For instance, consider using a database instead of a Map.

Following the DIP we introduce an abstraction and let Employee depend on it:

interface EmployeeRegistry {

    void register(Employee employee);
    
    boolean isRegistered(Employee employee);
}

abstract class Employee {

    private final EmployeeRegistry registry;
    
    /** ... */
}

Notice that abstractions invert dependencies.

Conclusion

(Software) principles should never lead to dogma; they should provide a hint on unclear crossroads.

SOLID principles can help recognize a problem in your code, but applying them blindly will likely do more harm than good.

The example source code with SOLID commits (original, SRP, OCP, LSP, ISP, DIP) is on my Github.

Happy coding!