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
Ten letters PascalCase.
Discussion
Loading…