SOLID Design Principles

The SOLID principles are a set of five design principles that help developers write cleaner, more maintainable code. These principles are widely regarded as the foundation for good object-oriented design. In this guide, we'll explore each of the SOLID principles with explanations and Java code examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Example:

// Before SRP: One class handles both user data and file management
class UserDataManager {
    public void saveUserData(User user) {
        // Code to save user data
    }

    public void writeToFile(User user) {
        // Code to write data to a file
    }
}

// After SRP: Separate the responsibilities into two classes
class UserDataManager {
    public void saveUserData(User user) {
        // Code to save user data
    }
}

class FileManager {
    public void writeToFile(User user) {
        // Code to write data to a file
    }
}

2. Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Example:

// Before OCP: Modifying existing code to add new features
class Rectangle {
    public double calculateArea() {
        return length * breadth;
    }
}

class Circle {
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

// After OCP: Use polymorphism to extend functionality without modifying existing code
interface Shape {
    double calculateArea();
}

class Rectangle implements Shape {
    public double calculateArea() {
        return length * breadth;
    }
}

class Circle implements Shape {
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class AreaCalculator {
    public double calculateShapeArea(Shape shape) {
        return shape.calculateArea();
    }
}

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Example:

// Before LSP: Subclass violates the behavior of the superclass
class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

// After LSP: Introduce a more appropriate hierarchy
class Bird {
    // Bird class doesn't assume that all birds can fly
}

class FlyingBird extends Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Penguin extends Bird {
    // Penguin doesn't have a fly method
}

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use. Instead, create specific interfaces for different client needs.

Example:

// Before ISP: A single interface with many methods
interface Worker {
    void work();
    void eat();
}

// After ISP: Split into smaller, more specific interfaces
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() {
        System.out.println("Robot is working");
    }
}

class Human implements Workable, Eatable {
    public void work() {
        System.out.println("Human is working");
    }

    public void eat() {
        System.out.println("Human is eating");
    }
}

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces), and abstractions should not depend on details.

Example:

// Before DIP: High-level module depends on low-level module
class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb is on");
    }
}

class Switch {
    private LightBulb bulb;

    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }

    public void operate() {
        bulb.turnOn();
    }
}

// After DIP: High-level module depends on an abstraction (interface)
interface Switchable {
    void turnOn();
}

class LightBulb implements Switchable {
    public void turnOn() {
        System.out.println("LightBulb is on");
    }
}

class Switch {
    private Switchable device;

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

    public void operate() {
        device.turnOn();
    }
}

Conclusion

The SOLID principles help developers create flexible, maintainable, and robust code. By following these principles, you can improve the design of your software and ensure that it remains scalable and easy to maintain as your project grows.