Handbooks / OOP & Design Patterns / Chapter 2

SOLID Principles

7 pages · ~5 min✓ Reviewed

Builds on The Four Pillars of OOP. Next up: Creational Patterns.

OOP & Design PatternsPage 1

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

SmellPrinciple
Giant classSRP
Growing if/else on typeOCP
Subclass that throwsLSP
Unused methodsISP
new of concrete depsDIP

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, new deps DIP
  • Overkill when? Stable code, one type, no second reason to change
OOP & Design PatternsPage 2

Single Responsibility and Open/Closed

1. Single Responsibility

  • One reason to change = one actor owns the class
  • Bad: Invoice has calculateTotal(), saveToDb(), printPdf()
  • Tax, DB and layout changes all edit one class
ClassOwnsChanges with
Invoicedata, totalstax rules
InvoiceRepositorypersistenceDB
InvoicePrinteroutputlayout

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: Shape interface with area(); 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
OOP & Design PatternsPage 3

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

  • Square setters change both sides, so a Rectangle caller gets the wrong area
  • Penguin.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

ModelBaseImplementers
BirdsFlyer.fly()Sparrow; Penguin is only a Bird
ShapesShape.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
OOP & Design PatternsPage 4

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: Worker has work() and eat()
  • Robot must no-op or throw in eat()
  • Fix: split into Workable and Eatable

2. Dependency Inversion

  • DIP: high-level modules depend on abstractions
  • Both high and low level depend on the interface
  • Violation: OrderService runs self.db = MySQLDatabase()
  • Cannot test or swap it
  • Fix: Database interface, 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

TermWhat it is
DIPprinciple
Dependency injectiontechnique
IoC containertool

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
OOP & Design PatternsPage 5

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 reuseCompose
Linkis-a by conveniencehas-a, delegates
LSPoverrides break callersnothing to override
Fixindependent Shape types

3. Spot in Review

SmellPrincipleFix
Fat Manager / Util classSRPsplit by actor
new Concrete() in constructorDIPinject 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.

OOP & Design PatternsPage 6

SOLID Cheat Sheet

1. Smell and Fix

RuleSmellFix
SRPDB + logic + output in one classSplit responsibilities
OCPif/elif on typePolymorphism or Strategy
LSPOverride throws or changes semanticsComposition, smaller interfaces
ISPEmpty or throwing methodsSplit the interface
DIPnew Concrete() in a serviceConstructor 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
OOP & Design PatternsPage 7

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 a Flyer interface 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 Database interface and inject it through the constructor