SOLID Principles in Object-Oriented Programming

Lesson 0019 — 45 min read

The SOLID Principles are five fundamental object-oriented design guidelines introduced by Robert C. Martin ("Uncle Bob"). Adhering to these principles produces software that is modular, flexible, scalable, easy to test, and resistant to unexpected bugs when requirements change.

Overview of SOLID

1. Single Responsibility Principle (SRP)

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

A class should focus exclusively on a single responsibility or business domain. Mixing unrelated concerns (such as salary calculations and report printing inside an Employee class) makes updating code risky and hard to test.

class Employee {
    private String name;
    private double baseSalary;
    public Employee(String name, double salary) {
        this.name = name;
        this.baseSalary = salary;
    }
    public double getBaseSalary() { return baseSalary; }
}

// Responsibility 1: Calculate Salary
class SalaryCalculator {
    public double calculateSalary(Employee emp) {
        return emp.getBaseSalary() * 1.2; // Add tax/bonus logic
    }
}

// Responsibility 2: Generate Reports
class ReportGenerator {
    public void generateReport(Employee emp) {
        System.out.println("Generating report for employee...");
    }
}
#include <iostream>
#include <string>
using namespace std;

class Employee {
public:
    string name;
    double baseSalary;
    Employee(string n, double s) : name(n), baseSalary(s) {}
};

// Responsibility 1: Calculate Salary
class SalaryCalculator {
public:
    double calculateSalary(const Employee& emp) {
        return emp.baseSalary * 1.2;
    }
};

// Responsibility 2: Generate Reports
class ReportGenerator {
public:
    void generateReport(const Employee& emp) {
        cout << "Generating report for " << emp.name << "\n";
    }
};
class Employee:
    def __init__(self, name: str, base_salary: float):
        self.name = name
        self.base_salary = base_salary

# Responsibility 1: Calculate Salary
class SalaryCalculator:
    def calculate_salary(self, emp: Employee) -> float:
        return emp.base_salary * 1.2

# Responsibility 2: Generate Reports
class ReportGenerator:
    def generate_report(self, emp: Employee) -> None:
        print(f"Generating report for {emp.name}")

2. Open/Closed Principle (OCP)

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

You should be able to add new features without modifying existing source code. This is achieved by programming to abstract interfaces and using polymorphism.

abstract class Shape {
    abstract double calculateArea();
}

class Rectangle extends Shape {
    private double length, breadth;
    public Rectangle(double l, double b) { length = l; breadth = b; }

    @Override
    double calculateArea() { return length * breadth; }
}

class Circle extends Shape {
    private double radius;
    public Circle(double r) { radius = r; }

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

// Adding Triangle extends Shape without modifying Shape, Rectangle, or Circle!
#include <iostream>
#include <cmath>
using namespace std;

class Shape {
public:
    virtual double calculateArea() = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
    double length, breadth;
public:
    Rectangle(double l, double b) : length(l), breadth(b) {}
    double calculateArea() override { return length * breadth; }
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double calculateArea() override { return M_PI * radius * radius; }
};
import math
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def calculate_area((self) -> float:
        pass

class Rectangle(Shape):
    def __init__(self, length: float, breadth: float):
        self.length = length
        self.breadth = breadth

    def calculate_area(self) -> float:
        return self.length * self.breadth

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def calculate_area(self) -> float:
        return math.pi * self.radius * self.radius

3. Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types without altering correctness."

A derived class must fulfill all expectations set by the base class. If a subclass (e.g., Penguin) throws exceptions for base class methods (e.g., fly() in Bird), LSP is violated. Refactor into dedicated capability interfaces.

interface FlyingBird {
    void fly();
}

class Sparrow implements FlyingBird {
    @Override
    public void fly() { System.out.println("Sparrow flying high!"); }
}

// Penguin does NOT implement FlyingBird — LSP preserved!
class Penguin {
    public void swim() { System.out.println("Penguin swimming!"); }
}
#include <iostream>
using namespace std;

class FlyingBird {
public:
    virtual void fly() = 0;
    virtual ~FlyingBird() = default;
};

class Sparrow : public FlyingBird {
public:
    void fly() override { cout << "Sparrow flying high!\n"; }
};

// Penguin does not inherit FlyingBird — no broken fly() expectation!
class Penguin {
public:
    void swim() { cout << "Penguin swimming!\n"; }
};
from abc import ABC, abstractmethod

class FlyingBird(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass

class Sparrow(FlyingBird):
    def fly(self) -> None:
        print("Sparrow flying high!")

class Penguin:
    def swim(self) -> None:
        print("Penguin swimming!")

4. Interface Segregation Principle (ISP)

"A class should not be forced to implement interfaces it does not use."

Avoid large, bloated interfaces. Instead, break interfaces into small, cohesive contracts so implementing classes are only aware of methods relevant to them.

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

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

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

// Robot only implements Workable — no dummy eat() method required!
class Robot implements Workable {
    @Override
    public void work() { System.out.println("Robot working"); }
}
#include <iostream>
using namespace std;

class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

class Eatable {
public:
    virtual void eat() = 0;
    virtual ~Eatable() = default;
};

class Human : public Workable, public Eatable {
public:
    void work() override { cout << "Human working\n"; }
    void eat() override { cout << "Human eating\n"; }
};

class Robot : public Workable {
public:
    void work() override { cout << "Robot working\n"; }
};
from abc import ABC, abstractmethod

class Workable(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None:
        pass

class Human(Workable, Eatable):
    def work(self) -> None:
        print("Human working")

    def eat(self) -> None:
        print("Human eating")

class Robot(Workable):
    def work(self) -> None:
        print("Robot working")

5. Dependency Inversion Principle (DIP)

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

Instead of hardcoding concrete implementations inside high-level classes (e.g. Computer instantiating a concrete WiredKeyboard), inject interface references (Keyboard) to allow swapping implementations seamlessly.

interface Keyboard {
    void connect();
}

class WiredKeyboard implements Keyboard {
    @Override
    public void connect() { System.out.println("Connected via wire."); }
}

class WirelessKeyboard implements Keyboard {
    @Override
    public void connect() { System.out.println("Connected via Bluetooth."); }
}

class Computer {
    private Keyboard keyboard; // Depends on abstraction

    // Dependency Injection
    public Computer(Keyboard keyboard) {
        this.keyboard = keyboard;
    }
}
#include <iostream>
using namespace std;

class Keyboard {
public:
    virtual void connect() = 0;
    virtual ~Keyboard() = default;
};

class WiredKeyboard : public Keyboard {
public:
    void connect() override { cout << "Connected via wire.\n"; }
};

class WirelessKeyboard : public Keyboard {
public:
    void connect() override { cout << "Connected via Bluetooth.\n"; }
};

class Computer {
private:
    Keyboard* keyboard; // Depends on interface pointer
public:
    Computer(Keyboard* k) : keyboard(k) {}
};
from abc import ABC, abstractmethod

class Keyboard(ABC):
    @abstractmethod
    def connect(self) -> None:
        pass

class WiredKeyboard(Keyboard):
    def connect(self) -> None:
        print("Connected via wire.")

class WirelessKeyboard(Keyboard):
    def connect(self) -> None:
        print("Connected via Bluetooth.")

class Computer:
    def __init__(self, keyboard: Keyboard):
        self.__keyboard = keyboard # Depends on abstraction

Summary Reference Table

Principle Core Goal Anti-Pattern Violation Refactored Solution
SRP (Single Responsibility) One reason to change per class Class handling salary, report printing, & DB save Separate SalaryCalculator & ReportGenerator
OCP (Open/Closed) Extend features without editing source Adding shapes via if/else type checks Inherit abstract Shape class polymorphically
LSP (Liskov Substitution) Subtypes substitutable for base types Penguin extends Bird & throws in fly() Segregate FlyingBird interface from non-flying birds
ISP (Interface Segregation) No forced unused interface methods Monolithic Worker forcing Robot to implement eat() Split into Workable & Eatable interfaces
DIP (Dependency Inversion) Depend on abstractions, not concrete types Computer directly instantiates WiredKeyboard Inject Keyboard interface reference into Computer

Which SOLID principle is violated if a Penguin subclass extends Bird and throws an UnsupportedOperationException in fly()?

How does Dependency Inversion Principle (DIP) recommend decoupling high-level modules from low-level modules?

Notes

SOLID Principles :-

Purpose ⇒ 5 design guidelines by Robert C. Martin ("Uncle Bob") for scalable, maintainable software

S — Single Responsibility (SRP) :-

Rule ⇒ One reason to change per class ; isolate distinct business responsibilities

Example ⇒ Separate SalaryCalculator from ReportGenerator

O — Open/Closed (OCP) :-

Rule ⇒ Open for extension, closed for modification ; use polymorphism/interfaces

Example ⇒ Abstract Shape allows adding Triangle without editing Rectangle

L — Liskov Substitution (LSP) :-

Rule ⇒ Subclasses must be transparently substitutable for base types

Example ⇒ Don't inherit Penguin from Bird.fly() ; use FlyingBird interface

I — Interface Segregation (ISP) :-

Rule ⇒ Don't force classes to implement unused interface methods

Example ⇒ Split monolithic Worker into Workable and Eatable

D — Dependency Inversion (DIP) :-

Rule ⇒ Depend on abstractions, not concrete implementations ; use dependency injection

Example ⇒ Computer depends on Keyboard interface, not WiredKeyboard

Primary source: Oracle Java Tutorials — Object-Oriented Design & cppreference — Abstract Classes & Polymorphism. Ask me anything that's unclear.

← Prev: File Handling Next: Object Lifecycle →

Questions? Ask your agent — you can follow up on any concept, quiz answer, or how this applies to a specific coding problem. Review the Java glossary or syntax quick reference for a compressed overview.