Ever looked at code and thought, "What spaghetti monster wrote this?" Plot twist: it was you, 6 months ago.
Don't worry, we've all been there. That's exactly why SOLID principles exist — to save us from our past selves.
What the Heck is SOLID?
SOLID is an acronym coined by Robert C. Martin (aka "Uncle Bob" — yes, that's his actual nickname in the dev world). It represents five principles that help you write code that doesn't make future-you want to cry.
| Letter | Principle | The Gist |
|---|---|---|
| S | Single Responsibility | Do ONE thing well |
| O | Open/Closed | Add features without breaking stuff |
| L | Liskov Substitution | Kids should behave like their parents |
| I | Interface Segregation | Don't force-feed unnecessary features |
| D | Dependency Inversion | Depend on promises, not specifics |
Let's break these down with real-world analogies that actually make sense.
S — Single Responsibility Principle
"A class should have only one reason to change."
The Restaurant Analogy
Imagine a restaurant where one person is the chef, waiter, cashier, dishwasher, AND accountant.
Sounds efficient? Nope. It's chaos.
- Chef burns food while taking orders
- Bills get mixed up while washing dishes
- The health inspector is NOT happy
Good restaurants have specialists:
- Chef → cooks food
- Waiter → serves customers
- Cashier → handles payments
Same with code. Each class should have ONE job.
Bad Code (The One-Man Band)
class SuperEmployee {
cookFood() { /* ... */ }
serveCustomers() { /* ... */ }
calculateBills() { /* ... */ }
washDishes() { /* ... */ }
doTaxes() { /* ... */ }
}
// This poor class needs therapyGood Code (The Dream Team)
class Chef {
cookFood() { /* ... */ }
}
class Waiter {
serveCustomers() { /* ... */ }
}
class Cashier {
processPayment() { /* ... */ }
}
// Each class: "I know my job. I do it well."O — Open/Closed Principle
"Open for extension, closed for modification."
The Smartphone Analogy
Your iPhone doesn't require Apple to surgically open it every time you want a new app. You just... download the app.
The phone is closed (you can't modify iOS core) but open (you can extend it with apps).
Bad Code (Surgery Required)
class PaymentProcessor {
process(type: string, amount: number) {
if (type === 'credit') {
// credit card logic
} else if (type === 'paypal') {
// paypal logic
} else if (type === 'crypto') {
// crypto logic
}
// Adding Apple Pay? Time to crack open this class again...
}
}Every new payment method = modifying existing code = potential bugs = sad developers.
Good Code (Plugin Architecture)
interface PaymentMethod {
pay(amount: number): void;
}
class CreditCard implements PaymentMethod {
pay(amount: number) { /* credit card magic */ }
}
class PayPal implements PaymentMethod {
pay(amount: number) { /* paypal wizardry */ }
}
// New payment method? Just add a new class!
class ApplePay implements PaymentMethod {
pay(amount: number) { /* apple sorcery */ }
}
// Original code: untouched and unbotheredL — Liskov Substitution Principle
"Subclasses should be substitutable for their parent classes."
The Duck Analogy
If it looks like a duck, quacks like a duck, but needs batteries — you have a violation.
A child class should be able to do everything its parent can do. No surprises, no exceptions, no "well, actually..."
The Classic Mistake: Square vs Rectangle
Mathematically, a square IS a rectangle. In code? It's a trap.
class Rectangle {
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
}
class Square extends Rectangle {
// Uh oh... setting width MUST also set height
setWidth(w: number) {
this.width = w;
this.height = w; // SURPRISE!
}
}Someone using a Rectangle expects setWidth to ONLY change width. The Square breaks that expectation.
The Fix: Don't Force It
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea() { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
getArea() { return this.side * this.side; }
}
// No inheritance drama. Just shapes doing shape things.I — Interface Segregation Principle
"Don't force classes to implement methods they don't use."
The Swiss Army Knife Problem
A Swiss Army Knife is cool for camping. But imagine forcing a surgeon to use one for operations.
"Sorry, I can't find the scalpel between the corkscrew and the toothpick."
Bad Code (The Monster Interface)
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
writeCode(): void;
managePeople(): void;
}
class Robot implements Worker {
work() { /* beep boop */ }
eat() { throw new Error("I'm a robot!"); }
sleep() { throw new Error("I don't sleep!"); }
// ... more exceptions
}
// Robot is having an existential crisisGood Code (Focused Interfaces)
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Manageable {
managePeople(): void;
}
class Human implements Workable, Eatable {
work() { /* typing sounds */ }
eat() { /* nom nom */ }
}
class Robot implements Workable {
work() { /* beep boop */ }
// No identity crisis here!
}D — Dependency Inversion Principle
"Depend on abstractions, not concretions."
The Power Outlet Analogy
Your laptop charger doesn't care if the electricity comes from solar panels, wind turbines, or a hamster running on a wheel.
It just needs something that provides electricity through a standard outlet.
Bad Code (Hardwired)
class OrderService {
private db = new MySQLDatabase(); // Married to MySQL forever
private email = new SendGrid(); // Tied to SendGrid
createOrder() {
this.db.save(order); // What if we switch to PostgreSQL?
this.email.send(receipt); // What if SendGrid raises prices?
}
}
// Changing anything = rewriting everythingGood Code (Pluggable)
interface Database {
save(data: any): void;
}
interface EmailService {
send(to: string, message: string): void;
}
class OrderService {
constructor(
private db: Database, // Any database works!
private email: EmailService // Any email service works!
) {}
createOrder() {
this.db.save(order);
this.email.send(receipt);
}
}
// Switching databases? Just pass a different implementation
const service = new OrderService(
new PostgreSQL(), // or MongoDB, or SQLite...
new Mailchimp() // or AWS SES, or smoke signals...
);SOLID in 30 Seconds
| Principle | One-Liner | Real World |
|---|---|---|
| Single Responsibility | One job per class | Chef cooks, waiter serves |
| Open/Closed | Extend, don't modify | Apps on your phone |
| Liskov Substitution | Kids behave like parents | A duck that needs batteries isn't a duck |
| Interface Segregation | Small, focused interfaces | Buffet > set menu |
| Dependency Inversion | Depend on abstractions | Power outlets, not specific power plants |
When NOT to Over-Engineer
SOLID is great, but don't go crazy:
- Writing a one-off script? Skip the ceremony.
- Building a prototype? Get it working first.
- Simple CRUD app? Don't architect it like Netflix.
Wrapping Up
SOLID principles aren't commandments carved in stone. They're guidelines that help you write code that:
- Your teammates can understand
- Future-you won't curse at
- Actually works when requirements change (and they always do)
Start with awareness. Notice when code feels off. That gut feeling? It's usually one of these principles being violated.
Now go forth and write code that doesn't suck.
Resources
- SOLID Principles - DigitalOcean
- Why SOLID Still Matters - Stack Overflow
- SOLID with Examples - freeCodeCamp
- Clean Code by Uncle Bob
Need help cleaning up your codebase? Contact CODERCOPS — we promise not to judge your spaghetti code.
Comments