A practical walkthrough of all five SOLID principles with before/after JavaScript code examples that you can apply immediately.
A class should have only one reason to change.
// Bad — UserService does too much
class UserService {
createUser(data) { /* ... */ }
sendWelcomeEmail(user) { /* ... */ } // should be EmailService
logUserCreation(user) { /* ... */ } // should be Logger
}
// Good
class UserService {
constructor(emailService, logger) {
this.emailService = emailService;
this.logger = logger;
}
async createUser(data) {
const user = await db.users.create(data);
await this.emailService.sendWelcome(user);
this.logger.info('user_created', { userId: user.id });
return user;
}
}
Open for extension, closed for modification.
// Bad — add new discount type = modify existing function
function calculateDiscount(order, type) {
if (type === 'percentage') return order.total * 0.1;
if (type === 'fixed') return 10;
// Must edit this every time
}
// Good — add new type without changing existing code
const discountStrategies = {
percentage: (order) => order.total * 0.1,
fixed: () => 10,
vip: (order) => order.total * 0.2,
};
function calculateDiscount(order, type) {
return discountStrategies[type]?.(order) ?? 0;
}
Subtypes must be substitutable for their base types.
// Bad — Square breaks Rectangle's contract
class Rectangle {
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w) { this.width = w; this.height = w; } // violates LSP
}
// Good — separate classes, no broken inheritance
class Shape {
area() { throw new Error('Not implemented'); }
}
class Rectangle extends Shape {
constructor(w, h) { super(); this.w = w; this.h = h; }
area() { return this.w * this.h; }
}
class Square extends Shape {
constructor(s) { super(); this.s = s; }
area() { return this.s * this.s; }
}
No client should be forced to depend on methods it does not use.
// Bad — all plugins must implement everything
class Plugin {
onInstall() {}
onUninstall() {}
onUpdate() {}
onRender() {} // not all plugins render!
}
// Good — small focused interfaces (mixins)
const Installable = (Base) => class extends Base {
onInstall() {}
onUninstall() {}
};
const Renderable = (Base) => class extends Base {
onRender() {}
};
class MyPlugin extends Installable(Renderable(class {})) {}
Depend on abstractions, not concretions.
// Bad — UserService tightly coupled to MySQL
import { MySQLDatabase } from './mysql';
class UserService {
constructor() { this.db = new MySQLDatabase(); }
getUser(id) { return this.db.query(`SELECT * FROM users WHERE id = ${id}`); }
}
// Good — depends on an abstraction (interface contract)
class UserService {
constructor(userRepository) { // inject any repo: MySQL, Postgres, in-memory
this.repo = userRepository;
}
getUser(id) { return this.repo.findById(id); }
}
SOLID principles are most valuable when applied to code that changes often. Don't over-engineer — apply them where complexity and change frequency justify it.