tool / 51

SOLID Principles

The five object-oriented design principles, with what to avoid and what to do instead.

All local
5/5
S — Single Responsibility
A class should have one reason to change.

Mixing concerns means a change to one feature can break unrelated ones.

avoid
class User {
  save() { db.write(this); }
  sendWelcomeEmail() { mailer.send(this.email); }
  generateReportPdf() { pdf.render(this); }
}
prefer
class User { save() { db.write(this); } }
class WelcomeMailer { send(user) { mailer.send(user.email); } }
class UserReport { generate(user) { pdf.render(user); } }
O — Open / Closed
Open for extension, closed for modification.

You should be able to add new behavior without editing existing tested code.

avoid
function area(shape) {
  if (shape.type === "circle") return Math.PI * shape.r ** 2;
  if (shape.type === "square") return shape.s ** 2;
  // adding a new shape means editing this function
}
prefer
class Circle { area() { return Math.PI * this.r ** 2; } }
class Square { area() { return this.s ** 2; } }
// new shapes plug in without changing existing code
L — Liskov Substitution
Subtypes must be substitutable for their base types.

If callers depend on the parent contract, replacing it with a child shouldn't break anything.

avoid
class Bird { fly() {} }
class Penguin extends Bird {
  fly() { throw new Error("can't fly"); }
}
prefer
class Bird {}
class FlyingBird extends Bird { fly() {} }
class Penguin extends Bird { swim() {} }
I — Interface Segregation
Don't force clients to depend on methods they don't use.

Fat interfaces couple unrelated consumers and make every change risky.

avoid
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}
class Robot implements Worker {
  work() {}
  eat() { /* no-op */ }
  sleep() { /* no-op */ }
}
prefer
interface Workable { work(): void; }
interface Feedable { eat(): void; }
class Robot implements Workable { work() {} }
class Human implements Workable, Feedable { work() {} eat() {} }
D — Dependency Inversion
Depend on abstractions, not concretions.

High-level modules shouldn't be glued to low-level ones — both should depend on a contract.

avoid
class OrderService {
  constructor() {
    this.db = new MySqlDatabase(); // hard-coded
  }
}
prefer
class OrderService {
  constructor(private db: Database) {} // any DB that satisfies the contract
}
const svc = new OrderService(new MySqlDatabase());