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.
"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}")
"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
"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!")
"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")
"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
| 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?
Purpose ⇒ 5 design guidelines by Robert C. Martin ("Uncle Bob") for scalable, maintainable software
Rule ⇒ One reason to change per class ; isolate distinct business responsibilities
Example ⇒ Separate SalaryCalculator from ReportGenerator
Rule ⇒ Open for extension, closed for modification ; use polymorphism/interfaces
Example ⇒ Abstract Shape allows adding Triangle without editing
Rectangle
Rule ⇒ Subclasses must be transparently substitutable for base types
Example ⇒ Don't inherit Penguin from Bird.fly() ; use
FlyingBird interface
Rule ⇒ Don't force classes to implement unused interface methods
Example ⇒ Split monolithic Worker into Workable and
Eatable
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.
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.