Skip to main content

Command Palette

Search for a command to run...

SOLID Principles Explained: Write Better Object-Oriented Code

Learn: SOLID Principles Explained: Write Better Object-Oriented Code

Updated
5 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

SOLID Principles: A Comprehensive Guide with Real-World Examples

The SOLID principles are five fundamental design principles in object-oriented programming that help developers create more maintainable, flexible, and scalable software systems. Introduced by Robert C. Martin (Uncle Bob), these principles have become cornerstones of modern software development.

1. Single Responsibility Principle (SRP)

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

Violation Example

public class Employee {
    private String name;
    private double salary;

    public void calculatePay() {
        // Calculate employee payment
    }

    public void saveToDatabase() {
        // Database persistence logic
    }

    public void generateReport() {
        // Generate employee report
    }

    public void sendEmail() {
        // Send notification email
    }
}

This class violates SRP because it handles payment calculation, database operations, reporting, and email notifications—four different responsibilities.

Refactored Solution

public class Employee {
    private String name;
    private double salary;

    // Getters and setters only
}

public class PayrollCalculator {
    public double calculatePay(Employee employee) {
        // Payment calculation logic
    }
}

public class EmployeeRepository {
    public void save(Employee employee) {
        // Database persistence logic
    }
}

public class EmployeeReportGenerator {
    public Report generate(Employee employee) {
        // Report generation logic
    }
}

public class EmailService {
    public void sendNotification(Employee employee, String message) {
        // Email sending logic
    }
}

Now each class has a single, well-defined responsibility, making the code easier to maintain and test.

2. Open/Closed Principle (OCP)

Definition: 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

public class DiscountCalculator {
    public double calculateDiscount(String customerType, double amount) {
        if (customerType.equals("Regular")) {
            return amount * 0.05;
        } else if (customerType.equals("Premium")) {
            return amount * 0.10;
        } else if (customerType.equals("VIP")) {
            return amount * 0.20;
        }
        return 0;
    }
}

Adding a new customer type requires modifying the existing method, violating OCP.

Refactored Solution

public interface DiscountStrategy {
    double calculateDiscount(double amount);
}

public class RegularCustomerDiscount implements DiscountStrategy {
    public double calculateDiscount(double amount) {
        return amount * 0.05;
    }
}

public class PremiumCustomerDiscount implements DiscountStrategy {
    public double calculateDiscount(double amount) {
        return amount * 0.10;
    }
}

public class VIPCustomerDiscount implements DiscountStrategy {
    public double calculateDiscount(double amount) {
        return amount * 0.20;
    }
}

public class DiscountCalculator {
    private DiscountStrategy strategy;

    public DiscountCalculator(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculate(double amount) {
        return strategy.calculateDiscount(amount);
    }
}

Now you can add new discount types by creating new classes without modifying existing code.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subtypes must be substitutable for their base types.

Violation Example

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; // Violates LSP
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height; // Violates LSP
    }
}

// This breaks when using Square
public void testRectangle(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(4);
    assert rect.getArea() == 20; // Fails for Square!
}

Refactored Solution

public interface Shape {
    int getArea();
}

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

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

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

public class Square implements Shape {
    private int side;

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

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

Now both shapes implement the same interface without inheritance issues.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they don't use. Many specific interfaces are better than one general-purpose interface.

Violation Example

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

public class HumanWorker implements Worker {
    public void work() { /* implementation */ }
    public void eat() { /* implementation */ }
    public void sleep() { /* implementation */ }
    public void attendMeeting() { /* implementation */ }
}

public class RobotWorker implements Worker {
    public void work() { /* implementation */ }
    public void eat() { /* Robot doesn't eat! */ }
    public void sleep() { /* Robot doesn't sleep! */ }
    public void attendMeeting() { /* implementation */ }
}

Refactored Solution

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public interface MeetingAttendable {
    void attendMeeting();
}

public class HumanWorker implements Workable, Eatable, Sleepable, MeetingAttendable {
    public void work() { /* implementation */ }
    public void eat() { /* implementation */ }
    public void sleep() { /* implementation */ }
    public void attendMeeting() { /* implementation */ }
}

public class RobotWorker implements Workable, MeetingAttendable {
    public void work() { /* implementation */ }
    public void attendMeeting() { /* implementation */ }
}

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

Violation Example

public class MySQLDatabase {
    public void save(String data) {
        // MySQL specific save logic
    }
}

public class UserService {
    private MySQLDatabase database;

    public UserService() {
        this.database = new MySQLDatabase(); // Tight coupling
    }

    public void saveUser(String userData) {
        database.save(userData);
    }
}

Refactored Solution

public interface Database {
    void save(String data);
}

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

public class MongoDBDatabase implements Database {
    public void save(String data) {
        // MongoDB specific implementation
    }
}

public class UserService {
    private Database database;

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

    public void saveUser(String userData) {
        database.save(userData);
    }
}

// Usage
Database db = new MySQLDatabase();
UserService service = new UserService(db);

Conclusion

The SOLID principles work together to create robust, maintainable software architectures. SRP ensures focused classes, OCP enables extension without modification, LSP guarantees proper inheritance, ISP prevents bloated interfaces, and DIP decouples dependencies. Applying these principles requires practice and judgment—not every situation demands strict adherence, but understanding them helps make informed design decisions that lead to cleaner, more professional code.