Home

Design Principles


Design principles provide guidelines to develop systems that are maintainable, flexible, reusable, and robust

SOLID Principles


A class should have only one reason to change, focusing on a single functionality.

Bad - one class doing two unrelated things:

class Invoice {
    public double calculateTotal() { /* ... */ }
    public void saveToDatabase() { /* ... */ }
}

Good - split responsibilities:

class Invoice {
    public double calculateTotal() { /* ... */ }
}

class InvoiceRepository {
    public void save(Invoice invoice) { /* ... */ }
}

Software entities should be open for extension but closed for modification.

Bad - must modify the class to add new shapes:

class AreaCalculator {
    public double area(Object shape) {
        if (shape instanceof Circle) { /* ... */ }
        else if (shape instanceof Square) { /* ... */ }
        return 0;
    }
}

Good - extend by adding new subclasses, no modification needed:

interface Shape {
    double area();
}

class Circle implements Shape {
    public double area() { /* ... */ return 0; }
}

class Square implements Shape {
    public double area() { /* ... */ return 0; }
}

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

Objects of a superclass should be replaceable with objects of subclasses without affecting the correctness of the program.

Bad - subclass breaks parent’s contract:

class Bird {
    public void fly() { /* ... */ }
}

class Penguin extends Bird {
    public void fly() {
        throw new UnsupportedOperationException();
    }
}

Good - separate behaviour so subtypes honour the contract:

class Bird { /* common bird behaviour */ }

class FlyingBird extends Bird {
    public void fly() { /* ... */ }
}

class Penguin extends Bird { /* no fly method */ }

Clients should not be forced to depend on interfaces they do not use. Favor many specific interfaces over a single general-purpose one.

Bad - one fat interface forces unused methods:

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

class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() {
        throw new UnsupportedOperationException();
    }
}

Good - split into focused interfaces:

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() { /* ... */ }
}

class Human implements Workable, Eatable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
}

Depend on abstractions, not concrete implementations. Higher-level modules should not depend on lower-level modules but rather on abstractions.

Bad - high-level class depends directly on a concrete class:

class MySQLDatabase {
    public void save(String data) { /* ... */ }
}

class UserService {
    private MySQLDatabase db = new MySQLDatabase();

    public void register(String user) {
        db.save(user);
    }
}

Good - depend on an abstraction, inject the implementation:

interface Database {
    void save(String data);
}

class MySQLDatabase implements Database {
    public void save(String data) { /* ... */ }
}

class UserService {
    private Database db;

    public UserService(Database db) {
        this.db = db;
    }

    public void register(String user) {
        db.save(user);
    }
}