1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
// ❌ Bad - multiple responsibilities
class User {
saveToDatabase() { /* ... */ }
sendEmail() { /* ... */ }
generateReport() { /* ... */ }
}
// ✅ Good - single responsibility
class User {
constructor(public name: string, public email: string) {}
}
class UserRepository {
save(user: User) { /* ... */ }
}
class EmailService {
send(to: string, message: string) { /* ... */ }
}
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
// ❌ Bad - modifying existing code for new shapes
class AreaCalculator {
calculate(shape: any) {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
// Need to modify this for every new shape
}
}
// ✅ Good - extend without modifying
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area() { return Math.PI * this.radius ** 2; }
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area() { return this.width * this.height; }
}
// New shapes don't require modifying existing code
class Triangle implements Shape {
constructor(private base: number, private height: number) {}
area() { return 0.5 * this.base * this.height; }
}
3. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
// ❌ Bad - Square violates LSP for Rectangle
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w: number) { this.width = this.height = w; } // Unexpected behavior
setHeight(h: number) { this.width = this.height = h; }
}
// ✅ Good - use composition or separate abstractions
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area() { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
area() { return this.side ** 2; }
}
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use.
// ❌ Bad - fat interface forces unnecessary implementations
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class Robot implements Worker {
work() { /* ... */ }
eat() { throw new Error('Robots do not eat'); } // Forced to implement
sleep() { throw new Error('Robots do not sleep'); }
}
// ✅ Good - segregated interfaces
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class Human implements Workable, Eatable, Sleepable {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}
class Robot implements Workable {
work() { /* ... */ }
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// ❌ Bad - high-level depends on low-level
class MySQLDatabase {
save(data: string) { /* MySQL specific */ }
}
class UserService {
private db = new MySQLDatabase(); // Direct dependency
saveUser(user: string) {
this.db.save(user);
}
}
// ✅ Good - depend on abstractions
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string) { /* MySQL specific */ }
}
class MongoDatabase implements Database {
save(data: string) { /* MongoDB specific */ }
}
class UserService {
constructor(private db: Database) {} // Injected dependency
saveUser(user: string) {
this.db.save(user);
}
}
// Easy to swap implementations
const service = new UserService(new MongoDatabase());
Quick Reference
| Principle | Summary |
|---|---|
| Single Responsibility | One class, one job |
| Open/Closed | Extend, don't modify |
| Liskov Substitution | Subtypes must be interchangeable |
| Interface Segregation | Small, focused interfaces |
| Dependency Inversion | Depend on abstractions |
Benefits of SOLID
✓ Easier to maintain and refactor
✓ More testable code (easier mocking)
✓ Reduced coupling between components
✓ Better code reusability
✓ Simpler to extend functionality
✓ Clearer separation of concerns