Lesson 07

SOLID Principles

Five principles for maintainable, testable, flexible software

Single Responsibility Open/Closed Liskov Substitution Interface Segregation Dependency Inversion

Why SOLID Matters

SOLID principles are the foundation of object-oriented design. They reduce coupling, increase cohesion, and make your codebase resilient to change.

S โ€” Single Responsibility Principle

"A class should have only one reason to change."

Each module or class should own exactly one piece of functionality. When a class does too much, a change in one concern ripples into unrelated areas.

Real-World Analogy

A restaurant chef cooks. A waiter serves. A cashier handles payments. If one person did all three, any change to the menu, service style, or payment system would affect the same person.

โŒ Bad โ€” Multiple Responsibilities

class UserService {
  createUser(data) { /* save to DB */ }
  sendWelcomeEmail(user) { /* send email */ }
  generateReport(users) { /* build PDF */ }
}

This class changes if the database schema changes, if the email template changes, or if the report format changes. Three reasons to change.

โœ… Good โ€” Single Responsibility Each

class UserRepository {
  create(data) { /* save to DB */ }
}

class EmailService {
  sendWelcome(user) { /* send email */ }
}

class ReportGenerator {
  generate(users) { /* build PDF */ }
}

Each class has one reason to change. They can be tested, deployed, and modified independently.

O โ€” Open/Closed Principle

"Software entities should be open for extension but closed for modification."

You should be able to add new behavior without changing existing, tested code. The strategy pattern is the classic tool for this.

Real-World Analogy

A power strip is closed (you do not rewire it) but open for extension (you plug in new devices).

โŒ Bad โ€” Modifying Existing Code

function calculateDiscount(type, amount) {
  if (type === 'regular') return amount * 0.05;
  if (type === 'premium') return amount * 0.10;
  if (type === 'vip') return amount * 0.20;  // added later
  // Every new type = modify this function
}

โœ… Good โ€” Strategy Pattern

const discountStrategies = {
  regular: (amount) => amount * 0.05,
  premium: (amount) => amount * 0.10,
};

function calculateDiscount(type, amount) {
  const strategy = discountStrategies[type];
  if (!strategy) throw new Error(`Unknown type: ${type}`);
  return strategy(amount);
}

// Adding VIP = just register, no modification:
discountStrategies.vip = (amount) => amount * 0.20;

L โ€” Liskov Substitution Principle

"Subtypes must be substitutable for their base types without altering program correctness."

If your code expects a Shape, any subclass of Shape must work without surprises. Violating this breaks polymorphism.

Real-World Analogy

If you rent a "car," any car should work โ€” sedan, SUV, electric. But if they gave you a shopping cart, the "car" contract is violated.

โŒ Bad โ€” Square/Rectangle Violation

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; }
  setHeight(h) { this.width = h; this.height = h; }
}

// Breaks expectations:
const shape = new Square();
shape.setWidth(5);
shape.setHeight(3);
shape.area(); // Returns 9, not 15!

โœ… Good โ€” Shared Interface, Separate Implementations

class Shape {
  area() { throw new Error('Not implemented'); }
}

class Rectangle extends Shape {
  constructor(w, h) { super(); this.width = w; this.height = h; }
  area() { return this.width * this.height; }
}

class Square extends Shape {
  constructor(side) { super(); this.side = side; }
  area() { return this.side * this.side; }
}

I โ€” Interface Segregation Principle

"Clients should not be forced to depend on interfaces they do not use."

Large, bloated interfaces force implementers to stub out methods they do not need. Split them into small, focused contracts.

Real-World Analogy

A universal remote with 80 buttons is worse than a simple remote with just the 5 you need. Most buttons go unused and add confusion.

โŒ Bad โ€” Fat Interface

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

// A Robot must implement eat() and sleep()? Nonsensical.
class Robot implements Worker {
  work() { /* ok */ }
  eat() { throw new Error('Robots don\'t eat'); }
  sleep() { throw new Error('Robots don\'t sleep'); }
  attendMeeting() { /* ok */ }
}

โœ… Good โ€” Segregated Interfaces

interface Workable {
  work(): void;
}
interface Feedable {
  eat(): void;
}
interface Restable {
  sleep(): void;
}

class HumanWorker implements Workable, Feedable, Restable {
  work() { /* ... */ }
  eat() { /* ... */ }
  sleep() { /* ... */ }
}

class Robot implements Workable {
  work() { /* ... */ }
  // No eat() or sleep() โ€” not needed
}

D โ€” Dependency Inversion Principle

"Depend on abstractions, not concretions."

High-level modules should not import low-level modules directly. Both should depend on abstractions (interfaces). This is the foundation of dependency injection.

Real-World Analogy

A lamp depends on a "power outlet" interface, not on a specific power plant. You can switch power sources without rewiring the lamp.

โŒ Bad โ€” Direct Dependency on Concretion

class OrderService {
  constructor() {
    this.db = new MySQLDatabase();      // tightly coupled
    this.mailer = new SendGridMailer(); // tightly coupled
  }
}

โœ… Good โ€” Dependency Injection

class OrderService {
  constructor(db, mailer) {
    this.db = db;       // any Database implementation
    this.mailer = mailer; // any Mailer implementation
  }
}

// Inject at composition root:
const service = new OrderService(
  new MySQLDatabase(),
  new SendGridMailer()
);

// In tests โ€” inject mocks:
const testService = new OrderService(
  new InMemoryDatabase(),
  new MockMailer()
);
โœ… The Payoff

With dependency inversion, swapping databases, email providers, or payment gateways is a configuration change โ€” not a rewrite. Tests become trivial because you inject mocks.

SOLID at a Glance

Principle One-Liner Violation Smell
SRPOne reason to changeGod classes, 500+ line files
OCPExtend, don't modifyGrowing if/switch chains
LSPSubtypes honor contractsOverrides that throw or no-op
ISPSmall, focused interfacesUnused method stubs
DIPDepend on abstractionsnew inside constructors
โš ๏ธ Don't Over-Apply

SOLID principles are guidelines, not laws. A 20-line script does not need dependency injection. Apply SOLID when the cost of rigidity exceeds the cost of abstraction.

๐Ÿง  Knowledge Check

Which SOLID principle states "depend on abstractions, not concretions"?