
Photo by engin akyurt on Unsplash
Write Code Like A Pro: Mastering The SOLID Principles
If you're a developer, you've probably heard whispers of this ancient wisdom in code reviews, design docs, or the hushed conversations between two senior devs in the corner of your office:
"You should follow SOLID principles."
But what exactly are these? Some kind of secret cult? A new JavaScript framework? Fear not — SOLID is simply an acronym, and one of the best blueprints for writing maintainable, scalable, and... non-disaster-prone code.
Let’s break it down, with some real-world humor sprinkled in.
🧠 S — Single Responsibility Principle (SRP)
"One class, one reason to change."
Real-world analogy: Imagine you hired a plumber to fix your sink, and halfway through the job, he starts giving you a lecture on tax planning. That’s what violating SRP feels like in code.
Code example:
Bad:
class UserManager { public void createUser() { /* ... */ } public void deleteUser() { /* ... */ } public void generateUserReport() { /* ... */ } // 🚨 Mixing concerns! }
Good:
class UserManager { public void createUser() { /* ... */ } public void deleteUser() { /* ... */ } }
class UserReportGenerator { public void generateUserReport() { /* ... */ } }
Each class now does one job. Fewer surprise side effects, fewer headaches.
🧠 O — Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
Real-world analogy: When your phone gets a new feature, you install an app. You don’t break out a screwdriver and start rewiring the motherboard. Your code should work the same way.
Code example:
Bad:
class NotificationService { public void send(String message, String type) { if (type.equals("Email")) { /* send email */ } else if (type.equals("SMS")) { /* send SMS */ } } }
Good:
interface NotificationSender { void send(String message); } class EmailSender implements NotificationSender { public void send(String message) { /* send email */ } } class SMSSender implements NotificationSender { public void send(String message) { /* send SMS */ } } class NotificationService { private NotificationSender sender; public NotificationService(NotificationSender sender) { this.sender = sender; } public void notify(String message) { sender.send(message); } }
New types? Just add a new class. Your core logic stays untouched, stress levels stay low.
🧠 L — Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without affecting correctness."
Real-world analogy: If you rent a car, you expect to drive it — whether it’s a sedan, SUV, or convertible. If the rental company handed you a boat with wheels, you'd be furious.
Code example:
Bad:
class Bird { public void fly() { /* flying logic */ } } class Penguin extends Bird { public void fly() { throw new UnsupportedOperationException("Penguins can't fly!"); } }
Better:
interface Bird { void eat(); } interface FlyingBird extends Bird { void fly(); } class Sparrow implements FlyingBird { public void eat() { /* eat */ } public void fly() { /* fly */ } } class Penguin implements Bird { public void eat() { /* eat */ } }
Now penguins aren’t pretending to be something they’re not. Less deception, fewer exceptions.
🧠 I — Interface Segregation Principle (ISP)
"No client should be forced to depend on methods it does not use."
Real-world analogy: Ordering a coffee and getting a full 10-course meal, whether you asked for it or not.
Code example:
Bad:
interface Worker { void work(); void eat(); } class Robot implements Worker { public void work() { /* working... */ } public void eat() { /* ??? Robots don't eat! */ } }
Better:
interface Workable { void work(); } interface Eatable { void eat(); } class Human implements Workable, Eatable { public void work() { /* work */ } public void eat() { /* eat */ } } class Robot implements Workable { public void work() { /* work */ } }
Now each class only implements what it actually needs. Simple. Clean. Logical.
🧠 D — Dependency Inversion Principle (DIP)
"Depend on abstractions, not on concretions."
Real-world analogy: If your smartphone was hard-wired to only work with one charger brand, you’d riot. Thankfully, it uses USB or wireless — an abstraction.
Code example:
Bad:
class MySQLDatabase { public void connect() { /* ... */ } } class UserRepository { private MySQLDatabase db = new MySQLDatabase(); public void saveUser() { db.connect(); /* save logic */ } }
Good:
interface Database { void connect(); } class MySQLDatabase implements Database { public void connect() { /* ... */ } } class UserRepository { private Database db; public UserRepository(Database db) { this.db = db; } public void saveUser() { db.connect(); /* save logic */ } }
Now you can swap databases like changing socks. Dependency injection = freedom.
💡 Wrapping Up
SOLID principles are like traffic rules for your code. They won’t stop you from writing spaghetti, but they’ll give you the map to a much safer, maintainable, and scalable design.
If you start noticing that:
-
your classes are doing too many things,
-
adding a new feature breaks four others,
-
or your code makes you want to fake your own death and start a new identity...
...chances are, you're breaking one (or more) SOLID principles.
Stick to them and your future self — and your teammates — will silently thank you.