Pramitha Mihiranga Jayasooriya

Pramitha Mihiranga Jayasooriya

5 min read

Art of Clean Code: Understanding the SOLID Principles in Java

JavaSOLIDClean CodeObject-Oriented Programming

Introduction

In software engineering, the SOLID principles are a set of five design principles that help developers write code that is easy to maintain and extend. These principles were introduced by Robert C. Martin and are essential for anyone looking to improve their coding skills, especially in object-oriented programming.
In this article, we break down each principle and provide Java code examples to illustrate how to adhere to these principles and the consequences of violating them.

S - Single Responsibility Principle (SRP)

Principle: A class should have one, and only one, reason to change.
Meaning: A class should have only one job or responsibility. If a class assumes more than one responsibility, it becomes more complex and harder to maintain.

Violation of SRP

public class User {
    private String name;

    public void saveUserToDatabase() {
        // Code to save user to a database
    }
}
Here the
User
class has two responsibilities: - Holding user data - Saving the user to a database

Correct Approach

public class User {
    private String name;
    // User related methods
}

public class UserRepository {
    public void save(User user) {
        // Code to save user to a database
    }
}
Responsibilities are separated: -
User
handles user data -
UserRepository
handles persistence

O - Open/Closed Principle (OCP)

Principle: Software entities should be open for extension but closed for modification.
Meaning: Classes should allow behavior to be extended without modifying existing code.

Violation of OCP

public class AreaCalculator {
    public double calculateRectangleArea(Rectangle r) {
        return r.length * r.width;
    }
}

public class Rectangle {
    public double length;
    public double width;
}
Adding another shape would require modifying
AreaCalculator
.

Correct Approach

public interface Shape {
    double calculateArea();
}

public class Rectangle implements Shape {
    private double length;
    private double width;

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

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}
Now new shapes can be added without modifying existing classes.

L - Liskov Substitution Principle (LSP)

Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting correctness.

Violation of LSP

public class Bird {
    public void fly() {
        // Flying logic
    }
}

public class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostrich can't fly");
    }
}
Replacing
Bird
with
Ostrich
causes runtime errors.

Correct Approach

public abstract class Bird {
}

public class FlyingBird extends Bird {
    public void fly() {
        // Flying logic
    }
}

public class Ostrich extends Bird {
    // Ostrich specific logic
}
This design avoids incorrect assumptions about behavior.

I - Interface Segregation Principle (ISP)

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

Violation of ISP

public interface Worker {
    void work();
    void eat();
}

public class HumanWorker implements Worker {
    public void work() {
        // working
    }

    public void eat() {
        // eating
    }
}

public class RobotWorker implements Worker {
    public void work() {
        // working
    }

    public void eat() {
        // irrelevant for robots
    }
}
RobotWorker
is forced to implement a method it does not need.

Correct Approach

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class HumanWorker implements Workable, Eatable {
    public void work() {
        // working
    }

    public void eat() {
        // eating
    }
}

public class RobotWorker implements Workable {
    public void work() {
        // working
    }
}
Each class only implements relevant behavior.

D - Dependency Inversion Principle (DIP)

Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Violation of DIP

public class LightBulb {
    public void turnOn() {
        // Turn on the light
    }

    public void turnOff() {
        // Turn off the light
    }
}

public class ElectricPowerSwitch {
    private LightBulb lightBulb;

    public ElectricPowerSwitch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
    }

    public void press() {
        // logic to use the light bulb
    }
}
The switch depends directly on a concrete class.

Correct Approach

public interface Switchable {
    void turnOn();
    void turnOff();
}

public class LightBulb implements Switchable {
    public void turnOn() {
        // logic to turn on
    }

    public void turnOff() {
        // logic to turn off
    }
}

public class ElectricPowerSwitch {
    private Switchable device;

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

    public void press() {
        // logic to use the device
    }
}
Now the switch depends on an abstraction.

Conclusion

The SOLID principles are foundational for building maintainable and scalable software systems. By applying these principles, developers can design systems that are easier to extend, test, and refactor.
The examples above demonstrate how adhering to SOLID improves software design and helps avoid common pitfalls.

✍️ Written by Pramitha Jayasooriya