SOLID Principles
Five heuristics for object-oriented design that keep code easy to change: single responsibility, open/closed, Liskov, interface segregation, dependency inversion.
Level: advanced
What is it?
SOLID is a set of five design principles popularized by Robert C. Martin. S: a class should have one reason to change. O: open for extension, closed for modification. L: subtypes must be substitutable for their base types. I: prefer several small interfaces to one wide one. D: depend on abstractions, not on concrete details.
Why does it matter?
They are a vocabulary for talking about why a change was painful. Each principle names a kind of coupling — and each can be over-applied, so they are heuristics to weigh, not laws to follow blindly.
Mental model
The five rules a good landlord follows: each room has one purpose (S), you add rooms without rebuilding the house (O), any tenant who fits the lease can swap in (L), the lease lists only what you use (I), and the building depends on the utility contract, not on one provider (D).
How it works
- Single Responsibility: group things that change for the same reason and separate what changes for different reasons.
- Open/Closed: add new behavior by adding new code (a new subclass or strategy), not by editing tested code.
- Liskov Substitution: a subtype must honor the contract of its base type, including preconditions, postconditions, and invariants.
- Interface Segregation: no client should depend on methods it does not use. Many small interfaces beat one big one.
- Dependency Inversion: high-level policy depends on an abstraction, and low-level details implement it. Wiring is done at the edge (dependency injection).
- Apply them where change actually happens. Speculative abstraction is its own cost.
Steps
- Notice a change that was painful
- Name the coupling behind it
- Pick the principle that addresses it
- Refactor toward an abstraction
- Stop when the pain is gone
Code example: Open/closed: add a discount without editing checkout
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply(self, price): ...
class NoDiscount(Discount):
def apply(self, price):
return price
class PercentOff(Discount):
def __init__(self, pct):
self.pct = pct
def apply(self, price):
return price * (1 - self.pct / 100)
def checkout(price, discount: Discount): # closed for change, open for new Discounts
return discount.apply(price)
print(checkout(200, PercentOff(25))) # 150.0Real-world usage
- Code reviews: naming the coupling a change exposed
- Designing modules and service boundaries
- Deciding when to split a growing class
Common mistakes
- Splitting every class into many tiny ones until no one can follow the flow.
- Adding interfaces and factories 'in case' something changes.
- Treating the principles as a checklist instead of a diagnosis of a real problem.
Interview questions
Explain the Single Responsibility Principle.
A class should have one reason to change — one actor or concern that drives modifications. If a report class changes for formatting and for data access, split them.
What is the Dependency Inversion Principle?
High-level modules should not depend on low-level modules; both should depend on abstractions. It lets you swap a database or service by implementing an interface, and it enables testing with fakes.
Give an example of violating Liskov substitution.
A Square subclass of Rectangle that forces width and height to be equal. Code that sets width and height separately and then checks the area breaks when it receives a Square.
| Letter | Principle | A quick smell test |
|---|---|---|
| S | Single Responsibility | Does this class change for more than one reason? |
| O | Open/Closed | Do I edit old code every time I add a case? |
| L | Liskov Substitution | Does any subclass throw or ignore what the base promises? |
| I | Interface Segregation | Do implementers leave methods empty or throw? |
| D | Dependency Inversion | Does business logic construct database or network classes directly? |