Article 5: SOLID Principles and Design Patterns

Introduction

SOLID principles are the foundation of maintainable object-oriented design. Through refactoring many codebases, I've seen how violating these principles leads to fragile, rigid code—and how following them creates systems that are a joy to work with.

This article covers the five SOLID principles with Python examples, plus advanced design patterns that build on the foundation from the previous article.

The SOLID Principles

spinner

S - Single Responsibility Principle

A class should have only one reason to change.

The Problem

# Bad: Class has multiple responsibilities
class UserService:
    def __init__(self, db_connection):
        self.db = db_connection

    def create_user(self, email: str, password: str) -> User:
        # Validation logic
        if not self._is_valid_email(email):
            raise ValueError("Invalid email")
        if len(password) < 8:
            raise ValueError("Password too short")

        # Password hashing
        password_hash = bcrypt.hash(password)

        # Database logic
        user = User(email=email, password_hash=password_hash)
        self.db.add(user)
        self.db.commit()

        # Email notification
        self._send_welcome_email(user)

        return user

    def _is_valid_email(self, email: str) -> bool:
        return "@" in email and "." in email

    def _send_welcome_email(self, user: User) -> None:
        # SMTP logic here
        smtp = smtplib.SMTP("localhost")
        smtp.send_message(...)

This class has four responsibilities: validation, password hashing, database operations, and email sending.

The Solution

Benefits

  • Easier testing: Each class can be tested in isolation

  • Easier changes: Changing email logic doesn't affect database logic

  • Better reuse: EmailValidator can be used elsewhere

  • Clearer code: Each class has a clear purpose

O - Open/Closed Principle

Software entities should be open for extension but closed for modification.

The Problem

The Solution

L - Liskov Substitution Principle

Subtypes must be substitutable for their base types.

The Problem

The Solution

Another Example: Rectangle/Square

I - Interface Segregation Principle

Clients should not be forced to depend on interfaces they don't use.

The Problem

The Solution

D - Dependency Inversion Principle

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

The Problem

The Solution

Dependency Injection Patterns

Advanced Design Patterns

Building on the patterns from Article 4, here are more patterns that work with SOLID principles.

Observer Pattern

Allows objects to notify others of state changes:

Decorator Pattern

Add behavior to objects dynamically:

Builder Pattern

Construct complex objects step by step:

Adapter Pattern

Make incompatible interfaces work together:

Practical Exercise

Exercise 1: Apply SOLID Refactoring

Exercise 2: Implement Observer Pattern

Create a task management system where multiple services react to task status changes:

SOLID Cheat Sheet

Principle
One-liner
Key Question

Single Responsibility

One class, one job

"How many reasons to change?"

Open/Closed

Extend, don't modify

"Can I add without changing?"

Liskov Substitution

Subtypes are swappable

"Can any subclass be used?"

Interface Segregation

Small, focused interfaces

"Do clients use everything?"

Dependency Inversion

Depend on abstractions

"Am I depending on concrete types?"

Key Takeaways

  1. SRP makes testing easier - Small, focused classes are simple to test

  2. OCP enables extensibility - Add new features without breaking existing code

  3. LSP ensures reliability - Subtypes must honor base type contracts

  4. ISP keeps interfaces clean - Don't force unnecessary implementations

  5. DIP enables flexibility - Abstractions allow swapping implementations

What's Next?

With a solid understanding of design principles, it's time to ensure your code works correctly. In Article 6: Testing Fundamentals, we'll cover unit testing with pytest, test structure, and how to write tests that give you confidence in your code.


This article is part of the Software Engineering 101 series.

Last updated