SE

SOLID principles

Must-know concept1.5 hIntermediate

SRP, OCP, LSP, ISP, DIP — the backbone of clean code.

SOLID is five rules of thumb for keeping object-oriented code changeable. Applied mechanically they over-engineer; applied when you feel the pain they describe, they pay back tenfold. The trick is recognising the pain.

The big idea

Each letter describes one thing that makes code easy to change:

S — Single Responsibility
A class should have one reason to change.
O — Open/Closed
Open to extension, closed to modification.
L — Liskov Substitution
Subtypes must be drop-in replacements for their base type.
I — Interface Segregation
Many small interfaces beat one fat one.
D — Dependency Inversion
Depend on abstractions, not concretions.

S — Single Responsibility

A class that parses, formats, validates, persists, and emails a report has five reasons to change. Each split into its own class can change independently.

Do
class ReportParser   { parse(raw: string): Report { /* … */ } }
class ReportPersister { save(r: Report): Promise<void> { /* … */ } }
class ReportEmailer  { email(r: Report): Promise<void> { /* … */ } }

One reason to change each.

Don't
class ReportService {
  parse() { /* … */ }
  validate() { /* … */ }
  saveToDb() { /* … */ }
  formatForPdf() { /* … */ }
  sendEmail() { /* … */ }
}

Five reasons to change. Five places to break a test.

O — Open/Closed

Adding a new payment method shouldn't require editing the existing pay() function. Make it accept a strategy or a new subclass instead.

interface Payment { charge(cents: number): Promise<void>; }
 
class StripeCard implements Payment { /* … */ }
class AchBank   implements Payment { /* … */ }
 
// Adding `Bitcoin` means *adding* a new class — not editing existing ones.
class Bitcoin implements Payment { /* … */ }

Strategy and Decorator (see Behavioral patterns) are the mechanical ways to satisfy O/C.

L — Liskov Substitution

If Square extends Rectangle, anywhere you accept a Rectangle you must accept a Square. The classic violation: setting width on a Square also changes its height, breaking callers who set width and height independently. Subtypes must honour the contract of the base, not just the type signature.

I — Interface Segregation

A class shouldn't be forced to implement methods it doesn't need. One fat interface that mixes concerns becomes a forced lie for anyone implementing it.

Do
interface Readable<T> { read(id: number): Promise<T | null>; }
interface Writable<T> { write(value: T): Promise<void>; }
 
class ReadOnlyCache<T> implements Readable<T> { /* no write */ }
Don't
interface Repository<T> {
  read(id: number): Promise<T | null>;
  write(value: T): Promise<void>;
  delete(id: number): Promise<void>;
  search(q: string): Promise<T[]>;
  bulkLoad(items: T[]): Promise<void>;
}
// every implementation either lies or throws on half the methods

D — Dependency Inversion

High-level modules shouldn't depend on low-level modules; both should depend on abstractions. In practice: depend on an interface, inject the implementation.

// good: handler depends on an interface
class OrderHandler {
  constructor(private readonly repo: OrderRepository) {}
  async place(items: Item[]) {
    return this.repo.save({ items, status: 'new' });
  }
}
 
// caller decides what 'repo' is — Pg, Fake, in-memory — at the edge of the app
new OrderHandler(new PgOrderRepository(pool));

DI containers automate this — but the principle applies even without one. Pass collaborators in; don't construct them inside.

In practice

You'll feel SOLID violations as friction:

Tests are hard to write
You can't mock the dependency → DI is missing.
One bug fix breaks three other features
SRP violated; multiple reasons to change.
Adding a feature edits a long switch
O/C violated; replace with polymorphism.
Subclass throws `NotImplementedError`
LSP or ISP violated.

The principles are five different ways of saying "make change cheap." When change is cheap, everything else gets easier.

Key takeaways

  • SOLID is about making change cheap — apply it when you feel the pain.
  • SRP: one class, one reason to change.
  • OCP: extend by adding, not editing.
  • LSP: subclasses must honour the base contract, not just its type.
  • ISP: split fat interfaces; let implementations opt into capabilities.
  • DIP: depend on interfaces and inject implementations.

Checkpoint questions

Use these to test whether the lesson is clear enough to explain without rereading.

  1. 1What is one concrete sign that a class has more than one responsibility?
  2. 2How can you extend behavior without editing existing code?
  3. 3What kind of subclass breaks Liskov Substitution?
  4. 4How does depending on interfaces make code easier to test?

References

External resources for going deeper after the lesson above.