Labs ICT
โญ Pro Login

SOLID & OO Principles

Master SOLID, DRY, KISS, and other core object-oriented design principles.

The SOLID Principles

The SOLID principles are five design guidelines that, when followed, lead to software that is easier to maintain, extend, and understand. They were popularized by Robert C. Martin (Uncle Bob) and form the backbone of good OO design.

S โ€” Single Responsibility Principle (SRP)
O โ€” Open/Closed Principle (OCP)
L โ€” Liskov Substitution Principle (LSP)
I โ€” Interface Segregation Principle (ISP)
D โ€” Dependency Inversion Principle (DIP)

S โ€” Single Responsibility Principle

A class should have only one reason to change. In other words, each class should be responsible for exactly one thing. When a class does too many things, changes to one responsibility risk breaking another.

// Violates SRP โ€” does too many things
class Employee {
  calculatePay() { /* ... */ }
  saveToDatabase() { /* ... */ }
  generateReport() { /* ... */ }
  sendEmail() { /* ... */ }
}

// Follows SRP โ€” each class has one responsibility
class PayCalculator {
  calculatePay(employee) { /* ... */ }
}

class EmployeeRepository {
  save(employee) { /* ... */ }
  findById(id) { /* ... */ }
}

class ReportGenerator {
  generate(employee) { /* ... */ }
}

class EmailService {
  sendWelcomeEmail(employee) { /* ... */ }
}

O โ€” Open/Closed Principle

Classes should be open for extension but closed for modification. You should be able to add new behavior without changing existing, tested code. This is typically achieved through inheritance, composition, or interfaces.

// Violates OCP โ€” must modify the class to add new shapes
class AreaCalculator {
  calculate(shape) {
    if (shape.type === "circle") {
      return Math.PI * shape.radius * shape.radius;
    } else if (shape.type === "rectangle") {
      return shape.width * shape.height;
    }
    // Adding a triangle means modifying this method...
  }
}

// Follows OCP โ€” new shapes extend behavior without modifying existing code
class Shape {
  area() { throw new Error("Must implement area()"); }
}

class Circle extends Shape {
  constructor(radius) { super(); this.radius = radius; }
  area() { return Math.PI * this.radius * this.radius; }
}

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

class Triangle extends Shape {
  constructor(base, height) { super(); this.base = base; this.height = height; }
  area() { return 0.5 * this.base * this.height; }
}

function calculateArea(shape) {
  return shape.area(); // Works for ANY shape โ€” no modification needed
}

L โ€” Liskov Substitution Principle

Subtypes must be substitutable for their base types without altering the correctness of the program. If class B extends class A, you should be able to use B anywhere A is expected without surprises.

// Violates LSP โ€” Square breaks Rectangle's behavior
class Rectangle {
  constructor(w, h) { this.width = w; this.height = h; }
  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; } // forces height too
  setHeight(h) { this.width = h; this.height = h; } // forces width too
}

function printArea(rect) {
  rect.setWidth(5);
  rect.setHeight(4);
  console.log(`Expected 20, got ${rect.area()}`);
}

printArea(new Rectangle()); // Expected 20, got 20 โœ“
printArea(new Square());    // Expected 20, got 16 โœ— (broken!)

I โ€” Interface Segregation Principle

No client should be forced to depend on methods it does not use. It's better to have many small, specific interfaces than one large, general-purpose interface.

// Violates ISP โ€” one fat interface
class Worker {
  work() {}
  eat() {}
  sleep() {}
}

// Follows ISP โ€” small, focused interfaces
class Workable {
  work() {}
}

class Feedable {
  eat() {}
}

class Sleepable {
  sleep() {}
}

class HumanWorker extends Workable extends Feedable extends Sleepable {}
class RobotWorker extends Workable {} // Robots don't eat or sleep

D โ€” Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details โ€” details should depend on abstractions.

// Violates DIP โ€” high-level depends on low-level
class MySQLDatabase {
  save(data) { /* MySQL-specific code */ }
}

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

// Follows DIP โ€” depends on abstraction
class Database {
  save(data) { throw new Error("Implement save()"); }
}

class MySQLDatabase extends Database {
  save(data) { /* MySQL-specific code */ }
}

class PostgreSQLDatabase extends Database {
  save(data) { /* PostgreSQL-specific code */ }
}

class UserService {
  constructor(database) {
    this.db = database; // depends on abstraction, not detail
  }
}

// Usage โ€” easy to swap implementations
const service = new UserService(new PostgreSQLDatabase());

Other Important Principles

Beyond SOLID, several other principles guide good OO design:

DRY (Don't Repeat Yourself) โ€” Every piece of knowledge should have a single, unambiguous representation. Duplication leads to bugs when you update one copy but forget the other.

KISS (Keep It Simple, Stupid) โ€” Simplicity should be a key goal. Avoid over-engineering. The simplest solution that works is usually the best.

YAGNI (You Aren't Gonna Need It) โ€” Don't build functionality until it's actually needed. Premature abstraction is the root of much complexity.

Composition Over Inheritance โ€” Prefer object composition over class inheritance. Inheritance creates tight coupling; composition is more flexible.

๐Ÿงช Quick Quiz

Which SOLID principle states that a class should have only one reason to change?