SOLID Principles

The five fundamental principles of object-oriented design for writing maintainable and scalable code.

best-practices
solidoopdesign-patternsarchitecturebest-practices

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

PrincipleSummary
Single ResponsibilityOne class, one job
Open/ClosedExtend, don't modify
Liskov SubstitutionSubtypes must be interchangeable
Interface SegregationSmall, focused interfaces
Dependency InversionDepend 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