Design Patterns

SOLID Principles

The SOLID principles are five design principles that help developers write maintainable, scalable, and testable object-oriented code.


1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

Example (Violating SRP):

This class handles both employee data and file operations.

class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public void saveToFile() {
        // Code to save employee data to a file (bad practice)
    }
}

Why is this bad?

• If we change how we store employees (e.g., from files to a database), we must modify this class.

Example (Following SRP):

Separate the responsibilities:

class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

class EmployeeFileManager {
    public void saveToFile(Employee employee) {
        // Code to save employee data
    }
}

Now:

• Employee handles only employee data.

• EmployeeFileManager handles file storage.

2. Open/Closed Principle (OCP)

Classes should be open for extension, but closed for modification.

Example (Violating OCP):

class AreaCalculator {
    public double calculate(Object shape) {
        if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.getRadius() * c.getRadius();
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.getWidth() * r.getHeight();
        }
        return 0;
    }
}

• Adding a new shape requires modifying AreaCalculator, violating OCP.

Example (Following OCP):

Use polymorphism so new shapes can be added without modifying existing code.

interface Shape {
    double calculateArea();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) { this.radius = radius; }

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

class Rectangle implements Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

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

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

Now, adding a new shape like Triangle does not modify AreaCalculator.

3. Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without breaking behavior.

Bad Example (Violating LSP):

class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

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

Penguin breaks expectations because it overrides fly() incorrectly.

Example (Following LSP):

Separate flying and non-flying birds.

interface Bird { }

interface FlyingBird extends Bird {
    void fly();
}

class Sparrow implements FlyingBird {
    public void fly() {
        System.out.println("Flying...");
    }
}

class Penguin implements Bird {
    public void swim() {
        System.out.println("Swimming...");
    }
}

Now, Penguin does not break expectations because it doesn’t pretend to fly.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Example (Violating ISP):

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

class Robot implements Worker {
    public void work() {
        System.out.println("Working...");
    }

    public void eat() {
        throw new UnsupportedOperationException("Robots don’t eat!");
    }
}

• The Robot class is forced to implement eat(), which is meaningless.

Example (Following ISP):

Split interfaces into smaller, specific ones.

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

class Robot implements Worker {
    public void work() {
        System.out.println("Working...");
    }

    public void eat() {
        throw new UnsupportedOperationException("Robots don’t eat!");
    }
}

Now:

• Human implements both Workable and Eatable.

• Robot only implements Workable.

5. Dependency Inversion Principle (DIP)

Depend on abstractions, not concrete implementations.

Example (Violating DIP):

class Keyboard { }

class Computer {
    private Keyboard keyboard;

    public Computer() {
        this.keyboard = new Keyboard(); // Direct dependency
    }
}

• The Computer class is tightly coupled to Keyboard, making changes difficult.

Example (Following DIP):

Use dependency injection.

interface Keyboard { }

class MechanicalKeyboard implements Keyboard { }

class Computer {
    private Keyboard keyboard;

    public Computer(Keyboard keyboard) { // Inject dependency
        this.keyboard = keyboard;
    }
}

Now:

• Computer depends on an interface (Keyboard), allowing different keyboard types.

Summary Table

PrincipleDescriptionFix
SRPOne reason to changeSeparate concerns
OCPOpen for extension, closed for modificationUse polymorphism
LSPSubclasses should not break base class behaviorFollow correct hierarchy
ISPAvoid forcing classes to implement unused methodsSplit interfaces
DIPDepend on abstractions, not concrete classesUse dependency injection

Leave a Reply

Your email address will not be published. Required fields are marked *