SE Notes
Five object-oriented design principles for maintainable software.
SOLID is an acronym for five object-oriented design principles introduced by Robert C. Martin (Uncle Bob) that, when applied together, produce software that is easier to maintain, extend, and test. These principles are not abstract theory—they address the specific, practical problems that make code rigid (hard to change), fragile (changes break unexpected things), immobile (hard to reuse), and viscous (easier to hack than to do correctly). Understanding and applying SOLID is essential for any professional software developer.
S — Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
Every class should have exactly one responsibility—one axis of change. If a class has multiple responsibilities, changes to one responsibility risk breaking the other. The class becomes fragile because it serves multiple masters.
// Violating SRP - Employee class has multiple responsibilities
class Employee {
public double calculatePay() { ... } // Payroll responsibility
public void saveToDatabase() { ... } // Persistence responsibility
public String generateReport() { ... } // Reporting responsibility
}
// If the report format changes, why would we modify the Employee class?
// Following SRP - each class has one responsibility
class PayCalculator {
public double calculatePay(Employee emp) { ... }
}
class EmployeeRepository {
public void save(Employee emp) { ... }
}
class EmployeeReportGenerator {
public String generate(Employee emp) { ... }
}O — Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification."
You should be able to add new behavior without modifying existing code. When requirements change, you extend the system by adding new code rather than changing code that already works (and has been tested).
# Violating OCP - must modify this function for every new shape
def calculate_area(shape):
if shape.type == "circle":
return 3.14 * shape.radius ** 2
elif shape.type == "rectangle":
return shape.width * shape.height
elif shape.type == "triangle": # Adding triangle requires modifying existing code!
return 0.5 * shape.base * shape.height
# Following OCP - extend by adding new classes, never modifying existing ones
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
class Circle(Shape):
def area(self): return 3.14 * self.radius ** 2
class Rectangle(Shape):
def area(self): return self.width * self.height
class Triangle(Shape): # Added without modifying any existing code!
def area(self): return 0.5 * self.base * self.heightL — Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types without altering the correctness of the program."
If class B extends class A, you should be able to use B anywhere A is expected without surprise. Subclasses must honor the contract established by their parent—including preconditions, postconditions, and invariants.
# Violating LSP - Square "is-a" Rectangle but breaks Rectangle's contract
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # Surprise! Setting width also changes height
def set_height(self, h):
self.width = h # Surprise! Setting height also changes width
# Code expecting a Rectangle breaks with a Square:
def test_area(rect):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # Fails for Square! (area is 16)The solution is to recognize that despite geometry, Square and Rectangle have different behavioral contracts and should not share an inheritance relationship in code.
I — Interface Segregation Principle (ISP)
"Clients should not be forced to depend on interfaces they do not use."
Large, monolithic interfaces force implementing classes to provide methods they do not need. Split large interfaces into smaller, focused ones so that clients depend only on the methods they actually use.
// Violating ISP - one fat interface forces unnecessary implementations
interface Worker {
void work();
void eat();
void sleep();
}
// A Robot implements Worker but cannot eat() or sleep()!
// Following ISP - segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class Human implements Workable, Eatable, Sleepable { ... }
class Robot implements Workable { ... } // Only implements what it needsD — Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Business logic (high-level) should not directly depend on infrastructure details (low-level). Instead, both depend on interfaces (abstractions), and concrete implementations are injected.
# Violating DIP - high-level OrderService depends directly on low-level MySQLDatabase
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # Tightly coupled to MySQL!
def save_order(self, order):
self.db.mysql_insert(order) # Cannot swap database
# Following DIP - depend on abstraction
class OrderRepository(ABC): # Abstraction
@abstractmethod
def save(self, order): pass
class MySQLOrderRepository(OrderRepository): # Low-level detail
def save(self, order): ...
class OrderService: # High-level, depends on abstraction
def __init__(self, repository: OrderRepository):
self.repository = repository # Injected dependency
def save_order(self, order):
self.repository.save(order) # Works with any implementationSOLID in Practice
SOLID principles work together synergistically. SRP ensures each class has focused responsibility. OCP ensures new requirements are handled by extension. LSP ensures substitutability in class hierarchies. ISP keeps interfaces lean. DIP ensures flexible dependency management. Together, they produce systems where adding features means adding code (not modifying it), bugs are localized to specific modules, and testing is straightforward because dependencies can be mocked through interfaces.
When Not to Apply SOLID
SOLID introduces abstraction, and abstraction has costs: more files, more indirection, more interfaces. For simple scripts, prototypes, or throwaway code, strict SOLID adherence is over-engineering. Apply SOLID proportionally to the system's expected lifetime and maintenance burden. Production code maintained for years benefits enormously from SOLID. A one-off data migration script does not.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for SOLID Principles — Software Engineering.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Software Engineering topic.
Search Terms
software-engineering, software engineering, software, engineering, design, solid, principles, solid principles — software engineering
Related Software Engineering Topics