DEV Community

Cover image for 🛠️ SOLID Principles in JavaScript: Write Better Code with Examples
Alaa Samy
Alaa Samy

Posted on

🛠️ SOLID Principles in JavaScript: Write Better Code with Examples

SOLID principles are a set of design guidelines that help you create maintainable, flexible, and scalable code.

Let’s break them down with JavaScript examples:


1. Single Responsibility Principle (SRP)

A class/function should have only one reason to change.

Bad:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  saveToDatabase() {
    // Database logic here
  }

  sendEmail() {
    // Email logic here
  }
}

Enter fullscreen mode Exit fullscreen mode

Problem: The User class handles both data management and email logic.

Good:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  saveToDatabase(user) { /* DB logic */ }
}

class EmailService {
  sendEmail(user) { /* Email logic */ }
}
Enter fullscreen mode Exit fullscreen mode

Benefit: Each class has a single responsibility.


2. Open/Closed Principle (OCP)

Entities should be open for extension but closed for modification.

Bad:

class Logger {
  logToConsole(message) {
    console.log(message);
  }

  logToFile(message) {
    // Write to file
  }
}

// To add a new logger (e.g., HTTP), you must modify the Logger class.
Enter fullscreen mode Exit fullscreen mode

Good:

// Use a strategy pattern to extend behavior
class Logger {
  log(message, loggerStrategy) {
    loggerStrategy(message);
  }
}

// Define strategies (extensions)
const consoleStrategy = (msg) => console.log(msg);
const fileStrategy = (msg) => writeToFile(msg);
const httpStrategy = (msg) => fetch('/log', { body: msg }); // New logger added without changing Logger class

// Usage:
const logger = new Logger();
logger.log("Error!", httpStrategy); // No need to modify Logger
Enter fullscreen mode Exit fullscreen mode

Benefit: Extend functionality without altering existing code.


3. Liskov Substitution Principle (LSP)

Subclasses should replace their parent class without breaking functionality.

Bad:

class Rectangle {
  setWidth(w) { this.width = w }
  setHeight(h) { this.height = h }
}

class Square extends Rectangle {
  setSize(size) { // Violates LSP
    this.width = size;
    this.height = size;
  }
}

function resizeShape(shape) {
  shape.setWidth(10);
  shape.setHeight(5); // Breaks for Square
}
Enter fullscreen mode Exit fullscreen mode

Good:

class Shape {
  area() { /* Abstract */ }
}

class Rectangle extends Shape { /* Implement area */ }
class Square extends Shape { /* Implement area */ }
Enter fullscreen mode Exit fullscreen mode

Benefit: Subclasses don’t break parent class behavior.


4. Interface Segregation Principle (ISP)

Clients shouldn’t depend on interfaces they don’t use.

Bad:

class Worker {
  work() { /* ... */ }
  eat() { /* ... */ }
}

// Robot forced to implement eat()
class Robot extends Worker {
  eat() { throw Error("Robots don't eat!"); }
}
Enter fullscreen mode Exit fullscreen mode

Good:

class Workable {
  work() { /* Interface */ }
}

class Eatable {
  eat() { /* Interface */ }
}

class Human implements Workable, Eatable { /* ... */ }
class Robot implements Workable { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Benefit: Avoid bloated interfaces.


5. Dependency Inversion Principle (DIP)

Depend on abstractions (interfaces), not concretions (specific implementations).

Bad:

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

class UserService {
  constructor() {
    this.db = new MySQLDatabase(); // Tight coupling
  }
}
Enter fullscreen mode Exit fullscreen mode

Good:

class Database {
  save(data) { /* Abstract */ }
}

class MySQLDatabase extends Database { /* ... */ }
class MongoDB extends Database { /* ... */ }

class UserService {
  constructor(database) {
    this.db = database; // Injected dependency
  }
}
Enter fullscreen mode Exit fullscreen mode

Benefit: Decoupled, testable code.


Why SOLID Matters in JavaScript 🚀

  1. Easier Maintenance: Changes affect fewer components.

  2. Better Testability: Isolated logic is easier to test.

  3. Flexible Architecture: Adapt to new requirements without rewrites.

  4. Reusability: Components can be reused across projects.

Top comments (0)