Mastering SOLID Principles ✅

Ali Samir - Jun 26 - - Dev Community

The SOLID principles are design principles in object-oriented programming that help developers create more understandable, flexible, and maintainable software.

Let's dive into each principle and see how they can be applied using JavaScript.


📌 1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

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

class UserService {
  createUser(user) {
    // logic to create user
  }

  getUser(id) {
    // logic to get user
  }
}

class UserNotificationService {
  sendWelcomeEmail(user) {
    // logic to send email
  }
}

const user = new User('John Doe', 'john.doe@example.com');
const userService = new UserService();
userService.createUser(user);

const notificationService = new UserNotificationService();
notificationService.sendWelcomeEmail(user);
Enter fullscreen mode Exit fullscreen mode

Here, User handles the user data, UserService handles user-related operations, and UserNotificationService handles notifications. Each class has a single responsibility.


📌 2. Open/Closed Principle (OCP)

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

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

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

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

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

const shapes = [new Rectangle(4, 5), new Circle(3)];

const totalArea = shapes.reduce((sum, shape) => sum + shape.area(), 0);
console.log(totalArea);
Enter fullscreen mode Exit fullscreen mode

In this example, each shape class's area method (like Rectangle and Circle) can be extended without modifying the existing code of the shape classes. This allows for adding new shapes in the future without changing the existing ones.


📌 3. Liskov Substitution Principle (LSP)

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

class Bird {
  fly() {
    console.log('I can fly');
  }
}

class Duck extends Bird {}

class Ostrich extends Bird {
  fly() {
    throw new Error('I cannot fly');
  }
}

function makeBirdFly(bird) {
  bird.fly();
}

const duck = new Duck();
makeBirdFly(duck); // Works fine

const ostrich = new Ostrich();
makeBirdFly(ostrich); // Throws error
Enter fullscreen mode Exit fullscreen mode

In this example, Ostrich violates LSP because it changes the expected behavior of the fly method. To comply with LSP, we should ensure that subclasses do not change the behavior expected by the base class.


📌 4. Interface Segregation Principle (ISP)

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

class Printer {
  print() {
    console.log('Printing document');
  }
}

class Scanner {
  scan() {
    console.log('Scanning document');
  }
}

class MultiFunctionPrinter {
  print() {
    console.log('Printing document');
  }

  scan() {
    console.log('Scanning document');
  }
}

const printer = new Printer();
printer.print();

const scanner = new Scanner();
scanner.scan();

const multiFunctionPrinter = new MultiFunctionPrinter();
multiFunctionPrinter.print();
multiFunctionPrinter.scan();
Enter fullscreen mode Exit fullscreen mode

Here, Printer and Scanner classes provide specific functionalities without forcing clients to implement methods they don't need. The MultiFunctionPrinter can use both functionalities, adhering to the ISP.


📌 5. Dependency Inversion Principle (DIP)

Definition: 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.

class NotificationService {
  constructor(sender) {
    this.sender = sender;
  }

  sendNotification(message) {
    this.sender.send(message);
  }
}

class EmailSender {
  send(message) {
    console.log(`Sending email: ${message}`);
  }
}

class SMSSender {
  send(message) {
    console.log(`Sending SMS: ${message}`);
  }
}

const emailSender = new EmailSender();
const notificationService = new NotificationService(emailSender);
notificationService.sendNotification('Hello via Email');

const smsSender = new SMSSender();
const notificationServiceWithSMS = new NotificationService(smsSender);
notificationServiceWithSMS.sendNotification('Hello via SMS');
Enter fullscreen mode Exit fullscreen mode

In this example, the NotificationService depends on an abstraction (sender), allowing it to work with any sender implementation (like EmailSender or SMSSender). This adheres to DIP by making the high-level module (NotificationService) depend on abstractions rather than concrete implementations.



Conclusion ✅

By adhering to the SOLID principles, you can design JavaScript applications that are more robust, maintainable, and scalable.

These principles help to ensure that your codebase remains clean and flexible, making it easier to manage and extend as your application grows.

Applying these principles consistently can significantly improve the quality of your software.


Happy Coding! 🔥

LinkedIn, X (Twitter), Telegram, YouTube, Discord, Facebook, Instagram

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .