A Deep Dive into SOLID Principles: The Backbone of Clean Code

A Deep Dive into SOLID Principles: The Backbone of Clean Code
A Deep Dive into SOLID Principles: The Backbone of Clean Code


Writing code is easy. Writing good code? That’s a whole different game. If you’ve ever looked back at your own project after a few months and thought, who wrote this spaghetti?—yeah, we’ve all been there. That’s why the SOLID principles exist, to help you write cleaner, more maintainable, and scalable code that won’t haunt you later.

So, let's break this down. No fluff. No robotic definitions. Just real talk about how to actually use SOLID in your code.

Single Responsibility Principle (SRP)

Imagine you're at a restaurant. The chef cooks, the waiter serves, and the cashier handles payments. Now, imagine one person doing all three—yeah, chaos. That’s what happens when a class tries to do too much.

Keep each class focused. A UserService should handle user-related logic. A NotificationService should deal with notifications. Don't mix them up. It’s easier to test, debug, and extend when each piece of code has a clear job.

🛠 Bad Example:

class UserManager {
  createUser() { /* create user logic */ }
  sendEmail() { /* email logic */ }
  logActivity() { /* logging logic */ }
}


🛠 Good Example:

class UserService { createUser() { /* user logic */ } }
class EmailService { sendEmail() { /* email logic */ } }
class LogService { logActivity() { /* logging logic */ } }


Clean. Simple. No unnecessary baggage.

Open-Closed Principle (OCP)

Let’s say you have a class handling payments. Today, it only supports credit cards. Tomorrow, you need to add PayPal. Next week, crypto. If you keep modifying the same class, it’ll turn into a monster. Instead, make it extensible.

Write code that allows adding features without touching existing logic.

🛠 Messy Code:

class PaymentService {
  pay(method: string) {
    if (method === 'creditCard') { /* credit card logic */ }
    else if (method === 'paypal') { /* PayPal logic */ }
  }
}


🛠 Better Approach:

interface PaymentMethod { pay(): void }
class CreditCardPayment implements PaymentMethod { pay() { /* logic */ } }
class PayPalPayment implements PaymentMethod { pay() { /* logic */ } }

Now, you can just add a new payment method without touching PaymentService. Less risk, more flexibility.

Liskov Substitution Principle (LSP)

Ever tried to plug in a charger that should work but doesn’t? That’s what breaking LSP feels like in code.

If you have a parent class and a child class, the child should be able to replace the parent without breaking anything.

Don't create subclasses that change expected behavior.

🛠 Oops, this is bad:

class Bird { fly() { /* flying logic */ } }
class Penguin extends Bird { fly() { throw new Error('Penguins can’t fly') } }


Calling penguin.fly() throws an error. That’s a violation of LSP because not all birds can fly. A better approach? Separate behaviors properly.

🛠 A cleaner way:

interface Bird { }
interface CanFly { fly(): void }
class Sparrow implements Bird, CanFly { fly() { /* fly logic */ } }
class Penguin implements Bird { swim() { /* swim logic */ } }


Now, no unexpected errors. Everything behaves as expected.

Interface Segregation Principle (ISP)

Ever signed up for a service and they asked for way too much info? Annoying, right? The same applies to interfaces.

Don’t force classes to implement things they don’t need.

🛠 Bad move:

interface Worker {
  work(): void;
  eat(): void;
}
class Robot implements Worker {
  work() { /* robot logic */ }
  eat() { throw new Error('Robots don’t eat') }
}


Robots won't eat, so why make them implement eat()? That’s unnecessary baggage. Break down interfaces.

🛠 Much better:

interface Workable { work(): void }
interface Eatable { eat(): void }
class Robot implements Workable { work() { /* logic */ } }
class Human implements Workable, Eatable { work() { /* logic */ } eat() { /* logic */ } }


Everyone gets what they need, nothing extra.

Dependency Inversion Principle (DIP)

Do you know how apps let you change themes without rewriting the whole UI? That’s DIP in action.

High-level modules shouldn’t depend on low-level ones. Both should depend on abstractions.

🛠 Uh-oh, here’s the problem:

class UserService {
  private db = new MySQLDatabase(); // Direct dependency 😬
  saveUser() { this.db.save(); }
}


What if tomorrow you switch to MongoDB? You’d have to rewrite this class. Instead, use an abstraction.

🛠 Way smoother:

interface Database { save(): void }
class MySQLDatabase implements Database { save() { /* MySQL logic */ } }
class MongoDBDatabase implements Database { save() { /* MongoDB logic */ } }
class UserService {
  constructor(private db: Database) {}
  saveUser() { this.db.save(); }
}


Now, switching databases is plug-and-play. No breaking changes. Clean and flexible.

Final Thoughts

Start small. Don’t try to refactor everything overnight. Begin applying SOLID principles bit by bit. Your future self will thank you when debugging is no longer a nightmare.

If this made sense to you, you're already on your way to writing pro-level code.

What’s the trickiest SOLID principle for you? Drop a comment and let’s discuss.

Post a Comment

Previous Post Next Post