SOLID Principles
Master the 5 Golden Rules for Writing Clean, Professional, and Maintainable Code
Building Strong Code Foundations
Imagine building a LEGO tower. If you build it carelessly, it might look okay at first, but as you add more pieces, it becomes wobbly, hard to change, and eventually falls apart!
The SOLID principles are like building instructions for creating strong, stable, and easy-to-modify code. These 5 rules help professional programmers write code that lasts and grows gracefully.
Single Responsibility Principle
"A class should have only ONE reason to change"
The Pizza Shop Analogy
In a pizza shop, each person has ONE job: The cashier takes orders, the chef makes pizza, the delivery driver brings it to you. Imagine if one person tried to do all three jobs - they'd be overwhelmed, make mistakes, and if they got sick, the whole shop stops!
In Code: Each class should do ONE thing well. A "User" class shouldn't also send emails and log data. Keep them separate: User, EmailSender, and Logger.
// ❌ BAD: UserManager does too many things!
class UserManager {
createUser(userData) {
// Save to database
}
sendWelcomeEmail(email) {
// Send email
}
logActivity(message) {
// Write to log file
}
generateReport() {
// Create PDF report
}
validatePassword(password) {
// Check password strength
}
// 5 different responsibilities! Too much!
}
// ✅ GOOD: Separate responsibilities!
// Only handles user data
class UserRepository {
createUser(userData) { /* save to database */ }
getUser(id) { /* fetch from database */ }
}
// Only handles sending emails
class EmailService {
sendWelcomeEmail(email) { /* send email */ }
sendPasswordReset(email) { /* send reset link */ }
}
// Only handles logging
class Logger {
log(message) { /* write to log file */ }
}
// Only handles reports
class ReportGenerator {
generateUserReport() { /* create PDF */ }
}
// Only handles password rules
class PasswordValidator {
validate(password) { /* check strength */ }
}
Remember This
- If a class has "AND" or "OR" in its name, it might be doing too much!
- Small, focused classes are easier to understand and test
- When requirements change, you only need to change ONE class
Open/Closed Principle
"Open for extension, closed for modification"
The Phone Charger Analogy
Your phone's charging port is designed to accept ANY compatible charger - USB, wireless, car charger - without changing the phone itself! You can extend how you charge it, but you don't need to modify the phone's internal wiring.
In Code: Write code so you can add NEW features without changing existing, working code. Like adding new payment methods without touching the checkout system.
// ❌ BAD: Adding a new payment means changing existing code!
class PaymentProcessor {
processPayment(type, amount) {
if (type === "credit_card") {
// Process credit card
} else if (type === "paypal") {
// Process PayPal
}
// ❌ Need to ADD code here for every new payment type!
// What if we want to add Apple Pay, Google Pay, Bitcoin...?
}
}
// ✅ GOOD: Add new payment types WITHOUT touching existing code!
// Define the interface
class PaymentMethod {
pay(amount) {
throw new Error("Must implement pay method");
}
}
// Credit Card (existing - never needs changing)
class CreditCardPayment extends PaymentMethod {
pay(amount) {
console.log(`💳 Processing $${amount} via Credit Card`);
}
}
// PayPal (existing - never needs changing)
class PayPalPayment extends PaymentMethod {
pay(amount) {
console.log(`🅿️ Processing $${amount} via PayPal`);
}
}
// ✅ Just ADD new payment types without touching old code!
class ApplePayPayment extends PaymentMethod {
pay(amount) {
console.log(`🍎 Processing $${amount} via Apple Pay`);
}
}
class BitcoinPayment extends PaymentMethod {
pay(amount) {
console.log(`₿ Processing $${amount} via Bitcoin`);
}
}
// The processor works with ANY payment method!
class PaymentProcessor {
processPayment(paymentMethod, amount) {
paymentMethod.pay(amount); // Works for ANY payment type!
}
}
// Usage
let processor = new PaymentProcessor();
processor.processPayment(new CreditCardPayment(), 100); // 💳
processor.processPayment(new ApplePayPayment(), 50); // 🍎 (just added!)
The Power of Extension
- New features = Add new files, don't touch old ones
- Existing code stays stable and bug-free
- Use inheritance and interfaces to make code extendable
Liskov Substitution Principle
"Child classes should be usable in place of parent classes"
The Chair Analogy
If a restaurant has "chairs" that you can sit on, and they bring you a "stool" (a type of chair), you expect to sit on it just the same! But if they bring a "decorative chair" that collapses when you sit, it breaks your expectation and you fall!
In Code: If a program expects a "Bird" and you give it a "Penguin", the program shouldn't crash because penguins can't fly. A child class must honor the parent's promises!
// ❌ BAD: Penguin breaks Bird's contract!
class Bird {
fly() {
console.log("Flying high! ✈️");
}
}
class Penguin extends Bird {
fly() {
throw new Error("❌ Penguins can't fly!");
// Problem! Code expecting a Bird will crash!
}
}
// This function expects any Bird to work...
function makeBirdFly(bird) {
bird.fly(); // 💥 CRASHES with Penguin!
}
let penguin = new Penguin();
makeBirdFly(penguin); // ❌ Error: Penguins can't fly!
// ✅ GOOD: Proper class design!
class Animal {
eat() {
console.log("Eating food 🍽️");
}
}
// FlyingBird for birds that CAN fly
class FlyingBird extends Animal {
fly() {
console.log("Flying high! ✈️");
}
}
// FlightlessBird for birds that CANNOT fly
class FlightlessBird extends Animal {
walk() {
console.log("Walking around 🐾");
}
}
class Eagle extends FlyingBird {
// Eagle can fly! ✅
}
class Penguin extends FlightlessBird {
swim() {
console.log("Swimming gracefully 🏊");
}
}
// Functions work safely!
function makeBirdFly(bird) {
bird.fly(); // ✅ Safe! Only FlyingBirds have fly()
}
let eagle = new Eagle();
makeBirdFly(eagle); // ✅ Works!
let penguin = new Penguin();
// makeBirdFly(penguin); ❌ Would give error BEFORE running!
penguin.swim(); // ✅ Penguins swim instead!
Substitution Rules
- A child class must work everywhere a parent class works
- Don't weaken parent promises (like removing fly())
- Design your class hierarchy carefully!
Interface Segregation Principle
"Don't force clients to depend on methods they don't use"
The Restaurant Menu Analogy
Imagine a restaurant with one giant menu that includes breakfast, lunch, dinner, drinks, and desserts all mixed together. As a customer who only wants breakfast, you have to flip through 50 pages of food you don't want. Wouldn't separate menus be better?
In Code: Don't create one giant interface with 20 methods. Split it into smaller, focused interfaces so classes only implement what they actually need.
// ❌ BAD: Worker must implement EVERYTHING!
class SuperWorker {
work() { /* work */ }
eat() { /* eat */ }
sleep() { /* sleep */ }
manageTeam() { /* manage */ }
writeCode() { /* code */ }
attendMeeting() { /* meeting */ }
}
// A Robot worker is forced to implement eat() and sleep()!
class RobotWorker extends SuperWorker {
eat() {
throw new Error("❌ Robots don't eat!");
}
sleep() {
throw new Error("❌ Robots don't sleep!");
}
// ❌ Forced to have useless methods!
}
// ✅ GOOD: Small, focused interfaces!
// For anything that can work
class Workable {
work() {
throw new Error("Must implement work");
}
}
// For living beings that need food
class Feedable {
eat() {
throw new Error("Must implement eat");
}
}
// For living beings that need rest
class Sleepable {
sleep() {
throw new Error("Must implement sleep");
}
}
// Human implements all three
class HumanWorker {
work() {
console.log("👷 Working hard!");
}
eat() {
console.log("🍽️ Eating lunch");
}
sleep() {
console.log("😴 Getting rest");
}
}
// Robot only implements Workable - perfect!
class RobotWorker {
work() {
console.log("🤖 Working 24/7 without breaks!");
}
// ✅ No need for eat() or sleep()!
}
// Alien only implements Workable and Feedable
class AlienWorker {
work() {
console.log("👽 Doing alien work");
}
eat() {
console.log("👽 Consuming nutrients");
}
// ✅ No need for sleep() - aliens don't sleep!
}
Segregation Benefits
- Small interfaces are easier to understand and implement
- Classes don't carry baggage of unused methods
- Changes to one interface don't affect unrelated classes
Dependency Inversion Principle
"Depend on abstractions, not concrete implementations"
The Power Outlet Analogy
Your laptop doesn't care which power plant generated the electricity. It just plugs into the wall socket (an abstraction). Whether the power comes from coal, solar, or wind, your laptop works the same! It depends on the idea of power, not a specific generator.
In Code: Your code should depend on interfaces or abstract classes, not specific implementations. This makes it easy to swap parts (like changing from MySQL to PostgreSQL) without rewriting everything.
// ❌ BAD: UserService is stuck with MySQL forever!
class MySQLDatabase {
save(data) {
console.log("💾 Saving to MySQL database...");
}
}
class UserService {
constructor() {
// ❌ Hard-coded dependency on MySQL!
this.database = new MySQLDatabase();
}
createUser(user) {
this.database.save(user);
// If we want to use MongoDB or PostgreSQL later?
// We have to rewrite this class!
}
}
// ✅ GOOD: UserService works with ANY database!
// Abstract interface (the power outlet)
class Database {
save(data) {
throw new Error("Must implement save");
}
find(id) {
throw new Error("Must implement find");
}
}
// MySQL implementation (one power plant)
class MySQLDatabase extends Database {
save(data) {
console.log("💾 [MySQL] Saving data...");
}
find(id) {
console.log("🔍 [MySQL] Finding data...");
return { id: id, name: "User" };
}
}
// MongoDB implementation (another power plant)
class MongoDBDatabase extends Database {
save(data) {
console.log("🍃 [MongoDB] Saving document...");
}
find(id) {
console.log("🔍 [MongoDB] Finding document...");
return { id: id, name: "User" };
}
}
// PostgreSQL implementation (yet another power plant)
class PostgreSQLDatabase extends Database {
save(data) {
console.log("🐘 [PostgreSQL] Saving record...");
}
find(id) {
console.log("🔍 [PostgreSQL] Finding record...");
return { id: id, name: "User" };
}
}
// UserService doesn't care which database it uses!
class UserService {
constructor(database) {
// ✅ Accepts ANY database that follows the interface!
this.database = database;
}
createUser(user) {
this.database.save(user);
console.log("✅ User created successfully!");
}
getUser(id) {
return this.database.find(id);
}
}
// Can easily switch databases!
let mysqlService = new UserService(new MySQLDatabase());
mysqlService.createUser({ name: "Alice" });
let mongoService = new UserService(new MongoDBDatabase());
mongoService.createUser({ name: "Bob" });
// Tomorrow: Want to use PostgreSQL? Just swap it!
let postgresService = new UserService(new PostgreSQLDatabase());
postgresService.createUser({ name: "Charlie" });
// UserService didn't need to change at all!
Dependency Inversion Visual
Inversion Benefits
- Flexibility: Swap implementations without changing high-level code
- Testability: Use fake/mock databases for testing
- Modularity: Parts are independent and interchangeable
SOLID Mastery Summary
Single Responsibility
One class, one job
Open/Closed
Extend, don't modify
Liskov Substitution
Children honor parents
Interface Segregation
Small interfaces
Dependency Inversion
Depend on abstractions
SOLID Principles Mastery Check
Ready to prove you understand the 5 golden rules of clean code?