Skip to content

Lesson 05 – SOLID Design Principles

Prerequisites: Review Lesson 04 – OOP and Algorithm Basics to see how robust code structure enables maintainable systems.

Software projects become difficult to maintain when classes try to do too much or depend heavily on each other. The SOLID principles offer guidelines for designing flexible and testable code. Each letter represents a principle that encourages small, well-defined components.


1. Single Responsibility Principle (SRP)

A class should have one reason to change. Split large classes into focused parts so each handles a single job.

class InvoicePrinter:
    """Create formatted text for an invoice."""

    def __init__(self, invoice: Invoice) -> None:
        self.invoice = invoice

    def render(self) -> str:
        """Return the printable invoice text."""
        return f"Invoice for {self.invoice.customer}"

2. Open/Closed Principle (OCP)

Code should be open for extension but closed for modification. New behavior should come from subclassing or composition.

class Shape:
    """Base type for drawable shapes."""

    def area(self) -> float:
        """Return the area of the shape."""
        raise NotImplementedError()

class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

3. Liskov Substitution Principle (LSP)

Subclasses must be usable wherever their parent classes are expected. Avoid breaking inherited contracts.

class Bird:
    """Base bird capable of flying."""

    def fly(self) -> str:
        """Return a flying action description."""
        return "Flapping wings"

class Ostrich(Bird):
    def fly(self) -> str:
        raise NotImplementedError("Ostriches can't fly")

The Ostrich class violates LSP because it changes the expected behavior of fly.

4. Interface Segregation Principle (ISP)

Prefer small, role-specific interfaces over large, general ones. Clients should depend only on methods they actually use.

class Printer:
    def print_report(self) -> None:
        raise NotImplementedError()

class Scanner:
    def scan_document(self) -> bytes:
        raise NotImplementedError()

class MultiFunctionMachine(Printer, Scanner):
    ...

5. Dependency Inversion Principle (DIP)

Depend on abstractions, not concrete implementations. This makes code easier to reuse and test.

class Database(Protocol):
    def save(self, item: dict) -> None:
        """Persist a dictionary of fields."""
        ...

class Service:
    def __init__(self, db: Database) -> None:
        self.db = db

    def create_user(self, fields: dict) -> None:
        self.db.save(fields)

6. Principle Relationships

Below is a simple diagram showing how SOLID principles interact to produce maintainable code.

flowchart LR
    SRP-->OCP
    OCP-->LSP
    LSP-->ISP
    ISP-->DIP

Key Takeaways

  • Keep classes focused and cohesive.
  • Extend behavior through composition or inheritance, not modifications.
  • Maintain substitutable subclasses.
  • Break large interfaces into smaller roles.
  • Depend on abstractions to reduce tight coupling.

Next Up

Apply these principles to real data tasks in Lesson 06 – CSV Training: Importing, Exporting, and Practical Context.