DEV Community

Cover image for Plot Twist! My Life Was Teaching Me SOLID Principles All Along
muskan thakur
muskan thakur

Posted on

Plot Twist! My Life Was Teaching Me SOLID Principles All Along

As developer, I’ve come to realize that coding isn’t just about writing logic—it’s about crafting something meaningful, like brewing the perfect cup of coffee or taking in a gorgeous view from a hilltop. Life itself can teach us lessons about writing better, smarter, and more adaptable code.

Let me walk you through the SOLID principles of software design, explained through some of my favorite life moments, complete with coding examples. Ready? Let’s dive in!


1. Single Responsibility Principle (SRP)

"My AeroPress is a coffee wizard. One job, one mission: making the best espresso. Simple and efficient."

In the coding world, this principle means a class, module, or function should have only one reason to change—it should do one thing and do it well.

Example:
Let’s say we’re building a task management app. Instead of dumping all the task-related logic in a single controller, we break it up.

// TaskService.js
class TaskService {
  addTask(task) {
    // Logic to add a task to the database
  }

  getTasks() {
    // Logic to fetch tasks from the database
  }
}
Enter fullscreen mode Exit fullscreen mode

This way, if we ever need to change how tasks are added or fetched, we know exactly where to go. No spaghetti code here—just like my AeroPress doesn’t suddenly start making tea!


2. Open-Closed Principle (OCP)

"My balcony has amazing mountain view. The mountains never change, but the seasons add new magic—spring flowers, autumn hues, or winter snow."

This principle says your code should be open to extension but closed to modification. That means you can add new features without breaking the existing ones.

Example:
Let's imagine we are building a notification system for an app. Initially, we only support email notifications, but later, we want to add support for SMS. Instead of changing the existing code that handles email notifications, we can extend it to support new types of notifications without modifying the core logic.

// NotificationService.js

// Base class for notifications
class Notification {
  send(message) {
    throw new Error('Method "send" must be implemented');
  }
}

// Email Notification class
class EmailNotification extends Notification {
  send(message) {
    console.log(`Sending email: ${message}`);
  }
}

// SMS Notification class
class SMSNotification extends Notification {
  send(message) {
    console.log(`Sending SMS: ${message}`);
  }
}


// Notification Service that can send different notifications
class NotificationService {
  constructor(notificationStrategy) {
    this.notificationStrategy = notificationStrategy;
  }

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

// Example usage:
const emailService = new NotificationService(new EmailNotification());
emailService.sendNotification('Hello via Email!');

const smsService = new NotificationService(new SMSNotification());
smsService.sendNotification('Hello via SMS!');
Enter fullscreen mode Exit fullscreen mode

Now, we’ve added SMS notifications without touching the email logic. The mountains (core logic) stay solid, while the seasons (new features) add flair.


3. Liskov Substitution Principle (LSP)

"At college, we had this hilltop that gave a stunning view of the campus. There were multiple paths to the top, but the view was always the same."

In coding, this means you can swap out a subclass for its parent class without breaking the functionality. It’s all about consistency.

Example:
Suppose we’re building a payment system where users can pay via credit cards or wallets.

class Payment {
  processPayment(amount) {
    console.log(`Processing payment of $${amount}`);
  }
}

class CreditCardPayment extends Payment {
  processPayment(amount) {
    console.log(`Processing credit card payment of $${amount}`);
  }
}

class WalletPayment extends Payment {
  processPayment(amount) {
    console.log(`Processing wallet payment of $${amount}`);
  }
}

function makePayment(paymentMethod, amount) {
  paymentMethod.processPayment(amount);
}

// Usage
const payment = new WalletPayment();
makePayment(payment, 100); // Works seamlessly
Enter fullscreen mode Exit fullscreen mode

No matter which path (payment method) you take, the functionality (processing payment) is consistent. Just like the hilltop view was always spectacular, regardless of the path.


4. Interface Segregation Principle (ISP)

"Pookie, my cat, is an indoor queen. She needs her cozy space, while my dogs love running wild outdoors. Different needs, different designs."

This principle says don’t overload your interfaces. Each should cater to specific needs—just like Pookie wouldn’t appreciate an interface meant for the dogs.

Example:
Let’s say we’re designing APIs for pets. Instead of creating one giant API for all animals, we break it into smaller, specific APIs.

class IndoorPet {
  stayIndoors() {
    console.log("Staying indoors where it’s safe and cozy.");
  }
}

class OutdoorPet {
  roamOutside() {
    console.log("Running wild in the great outdoors!");
  }
}

// Usage
const pookie = new IndoorPet();
pookie.stayIndoors();

const dog = new OutdoorPet();
dog.roamOutside();
Enter fullscreen mode Exit fullscreen mode

Each API does only what’s needed for its client. Pookie gets her cozy corners, and the dogs get their wild adventures.


5. Dependency Inversion Principle (DIP)

"Bonfires are magical! Whether it’s a small firepit or a big bonfire, it’s the warmth and connection that matter—not the fire’s size."

This principle says high-level modules shouldn’t depend on low-level details. Both should rely on abstractions.

Example:
Suppose we’re building a service to fetch user data. Instead of hardcoding a specific database, we use an abstraction layer.

class UserRepository {
  getUsers() {
    // Fetch users from the database
  }
}

class UserService {
  constructor(repository) {
    this.repository = repository;
  }

  fetchUsers() {
    return this.repository.getUsers();
  }
}

// Usage
const userService = new UserService(new UserRepository());
userService.fetchUsers();
Enter fullscreen mode Exit fullscreen mode

Now, if we switch from MongoDB to MySQL, we don’t need to rewrite the service logic. The warmth (abstraction) stays constant, whether it’s a firepit or a bonfire.


Conclusion

Life’s little moments can teach us big lessons about writing better code. Whether it’s brewing coffee, enjoying a mountain view, or cuddling your cat, the principles of SOLID design are all around us.

For us developers, following these principles means cleaner, more maintainable code, happier teams, and smoother projects. So, which SOLID principle is your favorite? Got your own quirky analogy? Let’s chat in the comments!

Top comments (0)