Mastering SOLID Principles: A Guide with JavaScript Examples

Samuel Okpe - Nov 4 - - Dev Community

Sam, an experienced developer, has been working with a codebase that seems to grow messier with each new feature. Each update introduces unexpected issues, making maintenance a headache. Seeking advice, Sam reaches out to a mentor, Alex, who introduces them to the SOLID principles — five guiding concepts for writing more robust, flexible code.

Think of these principles as a toolkit to help you design code that adapts easily to change,” Alex explains. With Sam’s curiosity piqued, they dive into each principle one by one.

Single Responsibility Principle (SRP): The Kitchen Gadget Problem

Alex starts with the Single Responsibility Principle. “Imagine if every time you rented a car, you also had to go through a training course on how to maintain it. It’s inefficient and unrealistic. In code, the Single Responsibility Principle ensures each class or function does just one job, reducing complexity and making each component easier to understand.”

Sam immediately sees the potential of applying this principle in their project, where some classes seem to carry multiple, unrelated duties.

// Before SRP: One class doing too much.
class OrderManager {
  createOrder(orderDetails) { /* create order */ }
  sendNotification(orderId) { /* send notification */ }
  calculateTotal(order) { /* calculate total */ }
}

// After SRP: One responsibility per class.
class OrderService {
  createOrder(orderDetails) { /* create order */ }
}

class NotificationService {
  sendNotification(orderId) { /* send notification */ }
}

class BillingService {
  calculateTotal(order) { /* calculate total */ }
}
Enter fullscreen mode Exit fullscreen mode

With SRP in place, Sam’s code becomes more organized, and each service now has a clear, focused purpose.

Open/Closed Principle (OCP): The Closet Conundrum

The next principle, Alex explains, is the Open/Closed Principle. “This principle keeps code open for extension but closed for modification. Think of it as renting a car that you can customize for comfort without modifying its engine. You want the flexibility to add new features without altering the core structure.”

Sam reflects on some parts of their code that could benefit from this approach, allowing for easy expansion without rewriting existing functions.

// Original: Hard to extend without modifying
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

// Extended without modifying original
class Square extends Rectangle {
  constructor(side) {
    super(side, side);
  }
}
Enter fullscreen mode Exit fullscreen mode

By extending classes instead of modifying them, Sam can now add new shapes without risking changes to the foundational Rectangle class.

Liskov Substitution Principle (LSP): The Car Rental Surprise

The Liskov Substitution Principle, Alex explains, is best illustrated through a car rental analogy: “When you rent a car, you expect it to be compatible with your expectations — four wheels, functional steering, safe to drive. If you were given a motorcycle instead, it wouldn’t be a substitute for the car you expected. In code, any derived class should be substitutable for its parent class without breaking functionality.”

class Bird {
  fly() { console.log("Flies in the sky!"); }
}

class Sparrow extends Bird {}

class Ostrich extends Bird {
  fly() {
    throw new Error("Ostriches can't fly!");
  }
}

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

letBirdFly(new Sparrow());  // Works fine
letBirdFly(new Ostrich()); 
Enter fullscreen mode Exit fullscreen mode

In Sam’s code, the takeaway is clear: ensure that any subclass can reliably replace its superclass without unexpected behavior.

Interface Segregation Principle (ISP): Tailored Interfaces for Specific Needs

Alex introduces the Interface Segregation Principle with an analogy: “Imagine you’re offered a premium car with extra features like voice control and massage seats, but you only need it for a quick drive to work. In code, you don’t want classes to have to implement unnecessary methods. Each interface should be tailored to the specific needs of its users.”

// Violating ISP: Too many unrelated methods for all workers
class Worker {
  performDuties() { /* core job duties */ }
  attendMeetings() { /* participate in meetings */ }
  writeReports() { /* draft reports */ }
}

// After ISP: Separate classes for specific roles
class Engineer {
  performDuties() { /* perform engineering tasks */ }
}

class Manager {
  attendMeetings() { /* attend meetings */ }
  writeReports() { /* write reports */ }
}
Enter fullscreen mode Exit fullscreen mode

Sam sees the value in minimizing unnecessary dependencies and making sure each class has only the functions it truly requires.

Dependency Inversion Principle (DIP): Decoupling Dependencies

Finally, Alex introduces the Dependency Inversion Principle, which emphasizes high-level components relying on abstractions rather than specific details. “Consider your car’s tires: they’re designed to fit a broad range of vehicles without specific customizations for each model. In code, higher-level modules should depend on abstractions instead of concrete implementations, keeping things modular and flexible.”

class SMSService {
  sendSMS(message) { console.log(`Sending SMS: ${message}`); }
}

class NotificationManager {
  constructor() {
    this.service = new SMSService();
  }
  sendNotification(message) {
    this.service.sendSMS(message);
  }
}

// With DIP: NotificationManager depends on an abstract service, not a specific one.
class NotificationService {
  send(message) { /* placeholder for sending */ }
}

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

class NotificationManager {
  constructor(service) {
    this.service = service;
  }
  sendNotification(message) {
    this.service.send(message);
  }
}

const smsService = new SMSService();
const notificationManager = new NotificationManager(smsService);
notificationManager.sendNotification("Hello, World!");
Enter fullscreen mode Exit fullscreen mode

By depending on abstractions, Sam sees how easy it is to switch to a different service (like an email service) without changing the core NotificationManager logic.

With these principles in mind, Sam’s coding style has taken a new direction. Each component is now organized, adaptable, and resilient to change. The SOLID principles, once abstract concepts, are now practical tools that make Sam’s code better prepared for growth and maintenance.

Thanks, Alex,” Sam says, ready to apply these principles in future projects. The SOLID principles aren’t just about avoiding errors — they’re about creating code that’s built to last.

.