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.