SOLID Principles

Five Principles of Object-Oriented Design

← Back to Index

What is SOLID?

SOLID is an acronym for five design principles that help developers create maintainable, flexible, and understandable object-oriented software.

The Five Principles
  • S - Single Responsibility Principle
  • O - Open/Closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

S - Single Responsibility Principle

"A class should have only one reason to change."

Each class should focus on doing one thing well. If a class has multiple responsibilities, changes to one aspect might break another.

Violation Example

// BAD: This class does too many things
public class UserManager {
    public void createUser(User user) {
        // Validate user
        if (user.getEmail() == null) {
            throw new ValidationException("Email required");
        }

        // Save to database
        Connection conn = DriverManager.getConnection(url);
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.executeUpdate();

        // Send email
        Properties props = new Properties();
        Session session = Session.getInstance(props);
        Transport.send(message);

        // Log the action
        FileWriter fw = new FileWriter("log.txt");
        fw.write("User created: " + user.getId());
    }
}

Correct Implementation

// GOOD: Each class has one responsibility
public class UserValidator {
    public void validate(User user) {
        if (user.getEmail() == null) {
            throw new ValidationException("Email required");
        }
    }
}

public class UserRepository {
    public void save(User user) {
        // Database operations only
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // Email operations only
    }
}

public class UserService {
    private final UserValidator validator;
    private final UserRepository repository;
    private final EmailService emailService;

    public void createUser(User user) {
        validator.validate(user);
        repository.save(user);
        emailService.sendWelcomeEmail(user);
    }
}

O - Open/Closed Principle

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

You should be able to add new functionality without changing existing code.

Violation Example

// BAD: Must modify this class to add new payment types
public class PaymentProcessor {
    public void processPayment(String type, double amount) {
        if (type.equals("credit_card")) {
            // Process credit card
        } else if (type.equals("paypal")) {
            // Process PayPal
        } else if (type.equals("crypto")) {
            // Adding new type requires modifying this class!
        }
    }
}

Correct Implementation

// GOOD: Open for extension via new implementations
public interface PaymentMethod {
    void process(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void process(double amount) {
        // Credit card logic
    }
}

public class PayPalPayment implements PaymentMethod {
    @Override
    public void process(double amount) {
        // PayPal logic
    }
}

// Adding crypto is just a new class - no modification needed!
public class CryptoPayment implements PaymentMethod {
    @Override
    public void process(double amount) {
        // Crypto logic
    }
}

// Processor is closed for modification
public class PaymentProcessor {
    public void processPayment(PaymentMethod method, double amount) {
        method.process(amount);
    }
}

L - Liskov Substitution Principle

"Objects of a superclass should be replaceable with objects of its subclasses without breaking the program."

Subclasses must be usable through the base class interface without the need to know the difference.

Violation Example

// BAD: Square violates LSP when used as Rectangle
public class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // Breaks expected behavior!
    }

    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height;  // Breaks expected behavior!
    }
}

// This test fails with Square!
void testRectangle(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.getArea() == 20;  // Fails for Square (returns 16)
}

Correct Implementation

// GOOD: Use interface instead of inheritance
public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private final int width;
    private final int height;

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

    @Override
    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private final int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

I - Interface Segregation Principle

"Clients should not be forced to depend on interfaces they don't use."

Create smaller, focused interfaces rather than large, general-purpose ones.

Violation Example

// BAD: Fat interface forces implementations to include unused methods
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// Robot doesn't eat or sleep!
public class Robot implements Worker {
    @Override
    public void work() {
        // Robot works
    }

    @Override
    public void eat() {
        // Forced to implement - throws exception or does nothing
        throw new UnsupportedOperationException();
    }

    @Override
    public void sleep() {
        throw new UnsupportedOperationException();
    }
}

Correct Implementation

// GOOD: Segregated interfaces
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

// Human implements all
public class Human implements Workable, Eatable, Sleepable {
    @Override
    public void work() { }

    @Override
    public void eat() { }

    @Override
    public void sleep() { }
}

// Robot only implements what it needs
public class Robot implements Workable {
    @Override
    public void work() {
        // Robot works
    }
}

D - Dependency Inversion Principle

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

Depend on interfaces rather than concrete implementations.

Violation Example

// BAD: High-level class depends on concrete low-level class
public class MySQLDatabase {
    public void save(String data) {
        // MySQL-specific code
    }
}

public class UserService {
    private MySQLDatabase database = new MySQLDatabase();  // Tight coupling!

    public void saveUser(User user) {
        database.save(user.toString());
        // Can't easily switch to PostgreSQL or test with mock
    }
}

Correct Implementation

// GOOD: Both depend on abstraction
public interface Database {
    void save(String data);
}

public class MySQLDatabase implements Database {
    @Override
    public void save(String data) {
        // MySQL implementation
    }
}

public class PostgreSQLDatabase implements Database {
    @Override
    public void save(String data) {
        // PostgreSQL implementation
    }
}

public class UserService {
    private final Database database;  // Depends on abstraction

    // Dependency injected
    public UserService(Database database) {
        this.database = database;
    }

    public void saveUser(User user) {
        database.save(user.toString());
    }
}

// Usage - can swap implementations easily
UserService service1 = new UserService(new MySQLDatabase());
UserService service2 = new UserService(new PostgreSQLDatabase());

// Testing - can use mock
Database mockDb = mock(Database.class);
UserService testService = new UserService(mockDb);

SOLID Summary

Quick Reference
Principle Key Idea Benefit
SRP One reason to change Easier maintenance
OCP Extend, don't modify Safer additions
LSP Substitutable subclasses Reliable polymorphism
ISP Small, focused interfaces Flexible implementations
DIP Depend on abstractions Testable, decoupled code