iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

SOLID

SOLID in one page: five principles for code that is easy to read, easy to change, and hard to break.

Design patterns — SOLID

EXAMPLE
// ===== S: Single Responsibility Principle =====
// A class / module should have one reason to change.
// Bad: a UserManager that loads, validates, persists, emails, and reports.
// Good: separate UserRepository, UserValidator, UserEmailer, UserReportBuilder.

class UserRepository {
  async findById(id) { /* DB only */ }
  async save(user) { /* DB only */ }
}
class UserEmailer {
  async sendWelcome(user) { /* mail only */ }
}

// ===== O: Open / Closed Principle =====
// Open for extension, closed for modification.
// Add new behaviour by adding NEW code, not by editing existing classes.

// Bad: a Pricer with a switch on customer type, edited each time a new type lands.
// Good: a strategy / polymorphism.

class Pricer { price(order) { throw new Error('abstract'); } }
class RetailPricer    extends Pricer { price(o) { return o.subtotal; } }
class WholesalePricer extends Pricer { price(o) { return o.subtotal * 0.9; } }
class VipPricer       extends Pricer { price(o) { return o.subtotal * 0.8; } }

function quote(order, pricer) { return pricer.price(order); }

// New tier? Add a new class. Don't touch the old ones.

// ===== L: Liskov Substitution Principle =====
// Subtypes must be usable wherever their parent is used.

// Classic violation: Rectangle / Square.
class Rectangle {
  constructor(w, h) { this.w = w; this.h = h; }
  setWidth(w)  { this.w = w; }
  setHeight(h) { this.h = h; }
  area() { return this.w * this.h; }
}
class Square extends Rectangle {
  setWidth(w)  { this.w = w; this.h = w; }   // mutates h too
  setHeight(h) { this.w = h; this.h = h; }   // breaks Rectangle's contract
}
// A consumer that expects setWidth not to touch height breaks on Square.
// Fix: don't subclass Rectangle. Either model Shape -> Rectangle / Square OR
// make immutable value types.

// ===== I: Interface Segregation Principle =====
// Many small interfaces are better than one big one.

// Bad: an IUserAdmin with read, write, deactivate, audit, exportReports.
// Good: split into IUserReader, IUserWriter, IUserDeactivator, IAuditLog.

// Consumers depend only on the slice they need; mocks become tiny.

// ===== D: Dependency Inversion Principle =====
// Depend on abstractions, not concretions.

// Bad: OrderService knows about a PostgresOrderRepo directly.
// Good: OrderService depends on an OrderRepo INTERFACE; the Postgres impl is injected.

class OrderService {
  constructor(repo, clock) { this.repo = repo; this.clock = clock; }
  async placeOrder(order) {
    order.created_at = this.clock.now();
    return this.repo.save(order);
  }
}
// Tests inject a FakeOrderRepo and a FixedClock. No DB, no time, fast tests.

// ===== Pulling the principles together =====
// A small example domain:
//   PriceCalculator (S: only pricing logic)
//   uses Strategy via PricingPolicy interface (O + L)
//   takes a small TaxRate interface (I)
//   depends on it via constructor (D)

class PriceCalculator {
  constructor(policy, taxRate) { this.policy = policy; this.taxRate = taxRate; }
  total(order) {
    const subtotal = this.policy.price(order);
    return subtotal + subtotal * this.taxRate.for(order.region);
  }
}

// ===== Patterns to internalise =====
// - 'One reason to change' is your friend in code review
// - Strategy and polymorphism are the easy wins for OCP
// - LSP failures usually mean the inheritance was wrong; favour composition
// - ISP keeps mocks tiny; tiny mocks reveal real coupling
// - DI by constructor (not via singletons) keeps code testable

// ===== Pitfalls =====
// - Over-applying SRP: 500 micro-classes that no one can navigate
// - OCP at the cost of YAGNI: extending hypothetical futures, not real change drivers
// - Subclassing for code reuse instead of behaviour relationship -> LSP regrets
// - One-method-per-interface taken too far; group methods that ARE cohesive
// - Hand-rolled DI containers that obscure construction; favour explicit wiring at the edge

Why it matters

SOLID is a vocabulary for the design conversations you already have. Smaller responsibilities, extension via new code, subtypes that obey their contracts, narrow interfaces, abstractions injected at construction. Use it as a code-review aid, not a checklist; over-applying any of the five buys you ceremony, not maintainability.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// S Single Responsibility — one reason to change.
// O Open / Closed — open to extension, closed to modification.
// L Liskov Substitution — subtypes must honour the contract.
// I Interface Segregation — many small interfaces beat one fat one.
// D Dependency Inversion — depend on abstractions, inject implementations.
Try it Yourself »

Exercise

D in SOLID stands for…

Inversion

Test yourself

Q1. The S in SOLID stands for…
Q2. The D in SOLID stands for…
Q3. Liskov's rule says…

Discussion

Loading…