Handbooks / OOP & Design Patterns / Chapter 2
SOLID Principles
7 pages · ~5 min✓ Reviewed
Builds on The Four Pillars of OOP. Next up: Creational Patterns.
SOLID Mental Model
1. What SOLID Is
- SOLID = five class-design rules for easy-to-change code
- SRP, OCP, LSP, ISP, DIP
- Core goal: one requirement change touches one place
- Not a ripple across many classes
2. Smell to Principle
| Smell | Principle |
|---|---|
| Giant class | SRP |
| Growing if/else on type | OCP |
| Subclass that throws | LSP |
| Unused methods | ISP |
new of concrete deps | DIP |
3. Judgment
- Guidelines for managing change, not laws
- Apply when a real change pressure appears
Remember
No change pressure, no refactor. Do not pre-split for imagined futures.
4. Interview Point
- Why SOLID? Keeps change local and cheap
- Smell to principle? Giant class SRP, type if/else OCP, throwing subclass LSP, unused methods ISP,
newdeps DIP - Overkill when? Stable code, one type, no second reason to change
Single Responsibility and Open/Closed
1. Single Responsibility
- One reason to change = one actor owns the class
- Bad:
InvoicehascalculateTotal(),saveToDb(),printPdf() - Tax, DB and layout changes all edit one class
| Class | Owns | Changes with |
|---|---|---|
Invoice | data, totals | tax rules |
InvoiceRepository | persistence | DB |
InvoicePrinter | output | layout |
2. Open/Closed
- Open to extension, closed to modification
- Add behavior by adding code, not editing tested code
- Bad:
area(shape)uses if/elif on type; every new shape edits it - Fix:
Shapeinterface witharea(); each shape implements it
python
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): ... class Circle(Shape): def area(self): return 3.14 * self.r ** 2
new shape = new class, no edits
Premature OCP
Wait for the third variant (rule of three).
3. Interview Point
- Reason to change? One actor's requirement: tax, DB, layout
- OCP vs SRP? OCP extends without editing; SRP gives one owner
- OCP pattern? Polymorphism / Strategy
- Premature when? One type, no new types expected
Liskov Substitution Principle
1. The Rule
- LSP: a subtype works wherever its base type is expected
- No stronger preconditions (what callers must supply)
- No weaker postconditions (what callers get back)
- Keep every base invariant
2. Violations
Squaresetters change both sides, so aRectanglecaller gets the wrong areaPenguin.fly()throws: any caller holding a Penguin as a Bird crashes
python
r = Square() # extends Rect r.set_width(5) r.set_height(4) r.area() # caller expects 20, gets 16
3. Fix: Model Behavior
| Model | Base | Implementers |
|---|---|---|
| Birds | Flyer.fly() | Sparrow; Penguin is only a Bird |
| Shapes | Shape.area() | Rectangle, Square (independent) |
4. Interview Point
- Detect a break? An override throws, or a base caller's expectation fails
- Why Square fails? It breaks Rect's invariant: width and height are independent
- Precondition? What callers must supply. Postcondition: what the method guarantees
Interface Segregation and Dependency Inversion
1. Interface Segregation
- ISP: no client forced to depend on methods it does not use
- Prefer many small role interfaces
- Violation:
Workerhaswork()andeat() Robotmust no-op or throw ineat()- Fix: split into
WorkableandEatable
2. Dependency Inversion
- DIP: high-level modules depend on abstractions
- Both high and low level depend on the interface
- Violation:
OrderServicerunsself.db = MySQLDatabase() - Cannot test or swap it
- Fix:
Databaseinterface, injected via constructor
python
class OrderService: def __init__(self, db: Database): self.db = db svc = OrderService(FakeDatabase())
tests pass a fake
3. Terms Not to Mix Up
| Term | What it is |
|---|---|
| DIP | principle |
| Dependency injection | technique |
| IoC container | tool |
4. Interview Point
- ISP vs SRP? ISP: client's view of an interface; SRP: one reason to change
- DIP vs DI? Principle vs the technique that applies it
- DIP and unit tests? Inject a fake, no real DB
Common SOLID Mistakes
1. Over-Applying SOLID
- Over-splitting: one-method classes hide the flow; split only on a real second reason to change
- One interface per class, one implementation, no test seam: noise, not DIP
- Type switch in one place, no new types expected: leave it (rule of three)
2. Inheritance vs Composition
| Inherit for reuse | Compose | |
|---|---|---|
| Link | is-a by convenience | has-a, delegates |
| LSP | overrides break callers | nothing to override |
| Fix | independent Shape types |
3. Spot in Review
| Smell | Principle | Fix |
|---|---|---|
Fat Manager / Util class | SRP | split by actor |
new Concrete() in constructor | DIP | inject interface |
- Over-apply SOLID? Yes: no change pressure means only extra indirection
- Composition vs inheritance for LSP? Composition has no override to break
- Spot SRP issues? Manager/Util names, edits from several teams
Rule
Split on real change pressure, not on principle names.
SOLID Cheat Sheet
1. Smell and Fix
| Rule | Smell | Fix |
|---|---|---|
| SRP | DB + logic + output in one class | Split responsibilities |
| OCP | if/elif on type | Polymorphism or Strategy |
| LSP | Override throws or changes semantics | Composition, smaller interfaces |
| ISP | Empty or throwing methods | Split the interface |
| DIP | new Concrete() in a service | Constructor injection |
2. Rules to Remember
- SRP: one reason to change = one actor
- OCP: extend by adding code, not editing
- LSP: keep the base contract
- ISP: small role interfaces
- DIP is the principle, DI the technique
python
class OrderService: def __init__(self, db: Database): self.db = db # injected
DIP fix: inject, never MySQLDatabase()
Don't over-apply
Split only on a real second reason to change; one-impl interfaces are noise.
3. Interview Point
- Recite: giant class, if/elif, throwing override, empty method,
new - Hardest? LSP, its contract is implicit
- Most overlap? SRP and ISP, both split by role
Check yourself
A Square extends Rectangle. Its setWidth also sets height, and setHeight also sets width. After r.setWidth(5); r.setHeight(4) on a Square, what is r.area()?
- 16: the second call resets both sides to 4
- The caller expects 20, so Square is not substitutable: an LSP break
Penguin extends Bird and its fly() throws. A loop calls fly() on every Bird. What breaks, and what is the fix?
- The loop crashes on the Penguin: an LSP break
- Move
fly()into aFlyerinterface that only flyers implement
OrderService does self.db = MySQLDatabase() in its constructor. Why can't you unit test it, and what fixes it?
- It creates a concrete dependency, so no fake can be swapped in: a DIP break
- Define a
Databaseinterface and inject it through the constructor