🔥 0
0
Lesson 2 of 4 18 min +100 XP

SOLID Principles

SOLID is an acronym for 5 design principles that help you write code that's easier to maintain, extend, and understand. These principles were popularized by Robert C. Martin (Uncle Bob).

S
O
L
I
D

---

S — Single Responsibility Principle

> "A class should have only one reason to change."

Each class should do one thing and do it well. If a class has multiple responsibilities, changes to one responsibility might break the other.

❌ Violates SRP

One class handles user data, sends emails, AND generates reports. Change email logic? Risk breaking reports.

✓ Follows SRP

Separate classes: UserRepository, EmailService, ReportGenerator. Each can change independently.

Bad Example:
# ❌ This class does too many things
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_database(self):
        # Database logic here
        db.insert(self)

    def send_welcome_email(self):
        # Email logic here
        smtp.send(self.email, "Welcome!")

    def generate_report(self):
        # Report logic here
        return f"User Report: {self.name}"
Good Example:
# ✓ Each class has one responsibility
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        db.insert(user)

class EmailService:
    def send_welcome(self, user):
        smtp.send(user.email, "Welcome!")

class UserReportGenerator:
    def generate(self, user):
        return f"User Report: {user.name}"

---

O — Open/Closed Principle

> "Software entities should be open for extension, but closed for modification."

You should be able to add new functionality without changing existing code. This prevents breaking things that already work.

Existing Code (Closed) New Feature (Extends)
Bad Example:
# ❌ Adding new shape requires modifying existing code
class AreaCalculator:
    def calculate(self, shape):
        if shape.type == "circle":
            return 3.14 * shape.radius ** 2
        elif shape.type == "rectangle":
            return shape.width * shape.height
        elif shape.type == "triangle":  # Had to modify!
            return 0.5 * shape.base * shape.height
Good Example:
# ✓ New shapes extend without modifying existing code
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# Adding triangle? Just add a new class!
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

---

L — Liskov Substitution Principle

> "Subtypes must be substitutable for their base types."

If you have a parent class, any child class should be able to replace it without breaking the program.

The Classic Example:

A Penguin is a Bird, but it can't fly. If your Bird class has a fly() method, Penguin violates LSP because calling penguin.fly() would fail or behave unexpectedly.

Bad Example:
# ❌ Penguin can't properly substitute for Bird
class Bird:
    def fly(self):
        return "Flying high!"

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")  # Breaks substitution!

def make_bird_fly(bird):
    print(bird.fly())  # Crashes with Penguin!
Good Example:
# ✓ Proper hierarchy respects LSP
class Bird:
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        return "Flying!"

class SwimmingBird(Bird):
    def move(self):
        return "Swimming!"

class Eagle(FlyingBird):
    pass

class Penguin(SwimmingBird):
    pass

def make_bird_move(bird):
    print(bird.move())  # Works for all birds!

---

I — Interface Segregation Principle

> "No client should be forced to depend on methods it doesn't use."

Don't create "fat" interfaces that force classes to implement methods they don't need. Split them into smaller, focused interfaces.

Fat Interface IWorker work() eat() sleep() Robot Must implement eat()? Segregated Interfaces Workable Eatable Sleepable Robot Human
Bad Example:
# ❌ Forces Robot to implement methods it can't use
class IWorker(ABC):
    @abstractmethod
    def work(self): pass

    @abstractmethod
    def eat(self): pass

    @abstractmethod
    def sleep(self): pass

class Robot(IWorker):
    def work(self):
        return "Working..."

    def eat(self):
        pass  # Robots don't eat! Forced to implement.

    def sleep(self):
        pass  # Robots don't sleep! Forced to implement.
Good Example:
# ✓ Small, focused interfaces
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self): pass

class Human(Workable, Eatable, Sleepable):
    def work(self): return "Working..."
    def eat(self): return "Eating lunch..."
    def sleep(self): return "Sleeping..."

class Robot(Workable):  # Only implements what it needs
    def work(self): return "Working 24/7..."

---

D — Dependency Inversion Principle

> "High-level modules should not depend on low-level modules. Both should depend on abstractions."

Don't let your business logic depend directly on specific implementations (like MySQL or SendGrid). Depend on abstractions (interfaces) instead.

Without DIP OrderService MySQLDatabase With DIP OrderService IDatabase
Bad Example:
# ❌ High-level module depends on low-level implementation
class MySQLDatabase:
    def save(self, data):
        # MySQL-specific code
        pass

class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()  # Tightly coupled!

    def create_order(self, order):
        self.db.save(order)

# Want to switch to PostgreSQL? Have to modify OrderService!
Good Example:
# ✓ Depend on abstraction, not implementation
class Database(ABC):
    @abstractmethod
    def save(self, data): pass

class MySQLDatabase(Database):
    def save(self, data):
        # MySQL-specific code
        pass

class PostgreSQLDatabase(Database):
    def save(self, data):
        # PostgreSQL-specific code
        pass

class OrderService:
    def __init__(self, db: Database):  # Depends on abstraction
        self.db = db

    def create_order(self, order):
        self.db.save(order)

# Easy to switch databases!
order_service = OrderService(PostgreSQLDatabase())

---

SOLID Summary

S
Single Responsibility

One class = one job. Easier to understand and change.

O
Open/Closed

Extend behavior without modifying existing code.

L
Liskov Substitution

Child classes must work wherever parent is expected.

I
Interface Segregation

Many small interfaces > one fat interface.

D
Dependency Inversion

Depend on abstractions, not concrete implementations.

When to Apply SOLID

✓ Do Apply
  • Production code
  • Long-lived projects
  • Code with many collaborators
  • Code that needs to be testable
⚠️ Don't Over-Apply
  • Quick prototypes
  • One-off scripts
  • Very simple programs
  • When it adds complexity without benefit

Next up: Design Patterns - reusable solutions to common problems that help you apply SOLID in practice.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What does the 'S' in SOLID stand for?

2

According to Open/Closed Principle, how should you add new functionality?

3

What problem does Dependency Inversion solve?

4

If a Bird class has a fly() method, and Penguin extends Bird, what principle is violated?

What is Low Level Design?