logo
banner
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:

...chances are, you're breaking one (or more) SOLID principles.

Stick to them and your future self — and your teammates — will silently thank you.