🛠️ Cracking the Code: Master SOLID Principles in JavaScript with Real-World Examples 🚀

hamzakhan

Hamza Khan

Posted on October 19, 2024

🛠️ Cracking the Code: Master SOLID Principles in JavaScript with Real-World Examples 🚀

When writing clean, maintainable code, developers often look to SOLID principles as a guiding framework. These five principles help make code more readable, reusable, and easier to manage. In this post, we will break down each principle, from the basics to more advanced examples, to help you master SOLID principles in JavaScript.

What Are the SOLID Principles?

SOLID is an acronym representing five core principles of object-oriented programming, and they apply equally well in JavaScript, especially in class-based and object-oriented systems. Here's a quick breakdown:

  1. S - Single Responsibility Principle (SRP)
  2. O - Open/Closed Principle (OCP)
  3. L - Liskov Substitution Principle (LSP)
  4. I - Interface Segregation Principle (ISP)
  5. D - Dependency Inversion Principle (DIP)

Let’s dive deeper into each one and look at how we can apply these principles using JavaScript.

1. 🧩 Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change. It should have a single responsibility or job.

A common mistake is to have "God classes" that try to do everything. For example, a User class should only handle user-specific tasks, not things like email notifications.

Bad Example:

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

  sendWelcomeEmail() {
    // Sending email
    console.log(`Sending welcome email to ${this.email}`);
  }

  saveToDatabase() {
    // Save to database
    console.log(`${this.name} saved to the database.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Example:

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

class UserService {
  saveToDatabase(user) {
    console.log(`${user.name} saved to the database.`);
  }
}

class EmailService {
  sendWelcomeEmail(user) {
    console.log(`Sending welcome email to ${user.email}`);
  }
}

// Single responsibility for each service
const user = new User("Alice", "alice@example.com");
const userService = new UserService();
const emailService = new EmailService();

userService.saveToDatabase(user);
emailService.sendWelcomeEmail(user);
Enter fullscreen mode Exit fullscreen mode

By separating concerns, each class now handles just one job.

2. 🏗️ Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

You should be able to add new functionality to a class without altering its existing code. This principle encourages the use of inheritance or interfaces.

Bad Example:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class AreaCalculator {
  calculate(shape) {
    if (shape instanceof Rectangle) {
      return shape.area();
    }
    // More conditions for other shapes...
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Example (following OCP):

class Shape {
  area() {
    throw new Error("This method should be overridden!");
  }
}

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

  area() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class AreaCalculator {
  calculate(shape) {
    return shape.area();
  }
}

// No need to modify existing code when adding new shapes
const rectangle = new Rectangle(10, 20);
const circle = new Circle(5);

const calculator = new AreaCalculator();
console.log(calculator.calculate(rectangle)); // 200
console.log(calculator.calculate(circle));    // 78.54
Enter fullscreen mode Exit fullscreen mode

By making Shape an abstract class, we can extend new shapes without modifying existing code.

3. 🔄 Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

This means that you should be able to replace a parent class with its subclass without breaking the program. If a subclass can’t fulfill all promises of the parent class, it violates this principle.

Bad Example:

class Bird {
  fly() {
    console.log("Flying");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins can't fly!");
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Example (using LSP):

class Bird {
  move() {
    console.log("Moving");
  }
}

class FlyingBird extends Bird {
  fly() {
    console.log("Flying");
  }
}

class Penguin extends Bird {
  swim() {
    console.log("Swimming");
  }
}

const eagle = new FlyingBird();
const penguin = new Penguin();

eagle.fly();   // Works as expected
penguin.move(); // Penguins can still move without the expectation to fly
Enter fullscreen mode Exit fullscreen mode

By creating FlyingBird and Penguin separately, we respect LSP and avoid introducing issues.

4. đź“„ Interface Segregation Principle (ISP)

Definition: No client should be forced to depend on methods it does not use.

In JavaScript, while we don’t have traditional interfaces, we can still follow this principle by splitting large interfaces into more specific ones.

Bad Example:

class Animal {
  eat() {}
  fly() {}
  swim() {}
}

class Dog extends Animal {
  fly() {
    throw new Error("Dogs can't fly");
  }

  swim() {
    console.log("Dogs can swim");
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Example:

class Eater {
  eat() {
    console.log("Eating");
  }
}

class Swimmer {
  swim() {
    console.log("Swimming");
  }
}

class Flyer {
  fly() {
    console.log("Flying");
  }
}

class Dog extends Eater {}
class Duck extends Eater {
  swim() {
    console.log("Swimming");
  }

  fly() {
    console.log("Flying");
  }
}

const dog = new Dog();
const duck = new Duck();

dog.eat();  // Works
duck.fly(); // Works
duck.swim();// Works
Enter fullscreen mode Exit fullscreen mode

By creating separate, specific classes (Eater, Swimmer, Flyer), we don’t force classes like Dog to implement unused methods.

5. 🔧 Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

You should depend on abstractions (interfaces) rather than on concrete implementations.

Bad Example:

class Database {
  connect() {
    console.log("Connecting to the database...");
  }
}

class UserService {
  constructor() {
    this.db = new Database();
  }

  saveUser(user) {
    this.db.connect();
    console.log(`Saving user ${user}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Good Example (following DIP):

class Database {
  connect() {
    throw new Error("This method should be overridden!");
  }
}

class MySQLDatabase extends Database {
  connect() {
    console.log("Connecting to MySQL...");
  }
}

class UserService {
  constructor(database) {
    this.db = database;
  }

  saveUser(user) {
    this.db.connect();
    console.log(`Saving user ${user}`);
  }
}

const db = new MySQLDatabase();
const userService = new UserService(db);
userService.saveUser("Alice");
Enter fullscreen mode Exit fullscreen mode

By passing the Database dependency via the constructor, we decouple UserService from a specific database implementation.

🔥 Conclusion

Understanding and applying SOLID principles is essential to writing maintainable, scalable, and testable code. Whether you're working on a small project or a large-scale application, adhering to these principles in JavaScript ensures your code remains flexible, reduces bugs, and makes future development easier.

Are you applying SOLID principles in your current projects? If not, start with small steps—refactor classes with single responsibilities or abstract dependencies—and gradually make these principles part of your development workflow.


🧠 Key Takeaways:

  • SRP: Keep each class focused on one task.
  • OCP: Extend behavior without altering existing code.
  • LSP: Ensure subclasses can replace their parent without issues.
  • ISP: Don’t force classes to implement methods they don’t need.
  • DIP: Depend on abstractions, not concrete implementations.
đź’– đź’Ş đź™… đźš©
hamzakhan
Hamza Khan

Posted on October 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related