Skip to content

Design Patterns

Introduction

Design patterns are reusable solutions to recurring problems in software design. This article covers the SOLID principles and the most commonly used patterns from the GoF (Gang of Four) classic.


1. SOLID Principles

Principle Full Name Meaning
S Single Responsibility A class should have only one reason to change
O Open/Closed Open for extension, closed for modification
L Liskov Substitution Subtypes must be substitutable for their base types
I Interface Segregation Interfaces should be small and specific
D Dependency Inversion Depend on abstractions, not concrete implementations

Example: Dependency Inversion

# Violating DIP
class MySQLDatabase:
    def query(self, sql): ...

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # direct dependency on concrete implementation

# Following DIP
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def query(self, sql): ...

class MySQLDatabase(Database):
    def query(self, sql): ...

class PostgresDatabase(Database):
    def query(self, sql): ...

class UserService:
    def __init__(self, db: Database):  # depends on abstraction
        self.db = db

2. Creational Patterns

2.1 Singleton

Ensures a class has only one instance.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Thread-safe version
import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # double-checked locking
                    cls._instance = super().__new__(cls)
        return cls._instance

Use cases: database connection pools, loggers, configuration management.

2.2 Factory

Delegates object creation to factory methods.

class Animal(ABC):
    @abstractmethod
    def speak(self): ...

class Dog(Animal):
    def speak(self): return "Woof!"

class Cat(Animal):
    def speak(self): return "Meow!"

class AnimalFactory:
    @staticmethod
    def create(animal_type: str) -> Animal:
        factories = {
            "dog": Dog,
            "cat": Cat,
        }
        if animal_type not in factories:
            raise ValueError(f"Unknown animal: {animal_type}")
        return factories[animal_type]()

animal = AnimalFactory.create("dog")
print(animal.speak())  # Woof!

Use cases: creating different types of objects based on configuration or input.

2.3 Builder

Constructs complex objects step by step.

class QueryBuilder:
    def __init__(self):
        self._table = None
        self._conditions = []
        self._order_by = None
        self._limit = None

    def table(self, name):
        self._table = name
        return self

    def where(self, condition):
        self._conditions.append(condition)
        return self

    def order(self, field):
        self._order_by = field
        return self

    def limit(self, n):
        self._limit = n
        return self

    def build(self):
        sql = f"SELECT * FROM {self._table}"
        if self._conditions:
            sql += " WHERE " + " AND ".join(self._conditions)
        if self._order_by:
            sql += f" ORDER BY {self._order_by}"
        if self._limit:
            sql += f" LIMIT {self._limit}"
        return sql

query = (QueryBuilder()
    .table("users")
    .where("age > 18")
    .where("active = 1")
    .order("created_at DESC")
    .limit(10)
    .build())
# SELECT * FROM users WHERE age > 18 AND active = 1 ORDER BY created_at DESC LIMIT 10

Use cases: building complex configurations, SQL queries, HTTP requests.

2.4 Prototype

Creates new objects by cloning existing ones.

import copy

class Prototype:
    def clone(self):
        return copy.deepcopy(self)

class GameUnit(Prototype):
    def __init__(self, name, hp, attack):
        self.name = name
        self.hp = hp
        self.attack = attack

template = GameUnit("Warrior", 100, 15)
unit1 = template.clone()
unit1.name = "Warrior_1"

3. Structural Patterns

3.1 Adapter

Converts one interface into another that clients expect.

# Existing legacy interface
class OldPaymentSystem:
    def make_payment(self, amount_in_cents):
        print(f"Paid {amount_in_cents} cents")

# New interface
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount_in_dollars): ...

# Adapter
class PaymentAdapter(PaymentProcessor):
    def __init__(self, old_system: OldPaymentSystem):
        self.old_system = old_system

    def pay(self, amount_in_dollars):
        self.old_system.make_payment(int(amount_in_dollars * 100))

adapter = PaymentAdapter(OldPaymentSystem())
adapter.pay(9.99)  # Paid 999 cents

3.2 Decorator

Dynamically adds responsibilities to objects.

class DataSource(ABC):
    @abstractmethod
    def write(self, data: str): ...
    @abstractmethod
    def read(self) -> str: ...

class FileDataSource(DataSource):
    def __init__(self, filename):
        self.filename = filename
    def write(self, data):
        with open(self.filename, 'w') as f:
            f.write(data)
    def read(self):
        with open(self.filename) as f:
            return f.read()

class EncryptionDecorator(DataSource):
    def __init__(self, source: DataSource):
        self.source = source
    def write(self, data):
        encrypted = data[::-1]  # simplified "encryption"
        self.source.write(encrypted)
    def read(self):
        return self.source.read()[::-1]

class CompressionDecorator(DataSource):
    def __init__(self, source: DataSource):
        self.source = source
    def write(self, data):
        import zlib
        self.source.write(zlib.compress(data.encode()).hex())
    def read(self):
        import zlib
        return zlib.decompress(bytes.fromhex(self.source.read())).decode()

# Composing decorators
source = EncryptionDecorator(CompressionDecorator(FileDataSource("data.txt")))
source.write("Hello, World!")

3.3 Proxy

class ImageProxy:
    """Lazy-loading proxy"""
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None

    def display(self):
        if self._real_image is None:
            print(f"Loading {self.filename}...")
            self._real_image = self._load_image()
        self._real_image.display()

    def _load_image(self):
        return RealImage(self.filename)

3.4 Facade

Provides a simple interface to a complex subsystem.

class VideoConverter:
    """Facade: hides the complex video conversion subsystem"""
    def convert(self, filename, format):
        file = VideoFile(filename)
        codec = CodecFactory.extract(file)
        if format == "mp4":
            result = MPEG4Compressor().compress(codec)
        elif format == "avi":
            result = AVICompressor().compress(codec)
        return result

# The client only needs:
converter = VideoConverter()
mp4 = converter.convert("video.ogg", "mp4")

4. Behavioral Patterns

4.1 Observer

Automatically notifies dependents when an object's state changes.

class EventEmitter:
    def __init__(self):
        self._listeners = {}

    def on(self, event, callback):
        self._listeners.setdefault(event, []).append(callback)

    def emit(self, event, *args, **kwargs):
        for callback in self._listeners.get(event, []):
            callback(*args, **kwargs)

# Usage
emitter = EventEmitter()
emitter.on("user_created", lambda user: print(f"Welcome {user}!"))
emitter.on("user_created", lambda user: send_email(user))
emitter.emit("user_created", "Alice")

4.2 Strategy

Defines a family of algorithms that are interchangeable.

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list: ...

class QuickSort(SortStrategy):
    def sort(self, data):
        if len(data) <= 1: return data
        pivot = data[0]
        left = [x for x in data[1:] if x <= pivot]
        right = [x for x in data[1:] if x > pivot]
        return self.sort(left) + [pivot] + self.sort(right)

class MergeSort(SortStrategy):
    def sort(self, data):
        # merge sort implementation
        ...

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy

    def sort(self, data):
        return self.strategy.sort(data)

# Switch strategy at runtime
sorter = Sorter(QuickSort())
result = sorter.sort([3, 1, 4, 1, 5])

4.3 Command

Encapsulates requests as objects, supporting undo/redo.

class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class InsertTextCommand(Command):
    def __init__(self, editor, text, position):
        self.editor = editor
        self.text = text
        self.position = position

    def execute(self):
        self.editor.insert(self.position, self.text)

    def undo(self):
        self.editor.delete(self.position, len(self.text))

class CommandHistory:
    def __init__(self):
        self._history = []

    def execute(self, command):
        command.execute()
        self._history.append(command)

    def undo(self):
        if self._history:
            command = self._history.pop()
            command.undo()

4.4 Iterator

class BinaryTree:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

    def __iter__(self):
        """In-order traversal iterator"""
        if self.left:
            yield from self.left
        yield self.value
        if self.right:
            yield from self.right

tree = BinaryTree(2, BinaryTree(1), BinaryTree(3))
for val in tree:
    print(val)  # 1, 2, 3

4.5 State

class State(ABC):
    @abstractmethod
    def handle(self, context): ...

class IdleState(State):
    def handle(self, context):
        print("Starting...")
        context.state = RunningState()

class RunningState(State):
    def handle(self, context):
        print("Pausing...")
        context.state = PausedState()

class PausedState(State):
    def handle(self, context):
        print("Resuming...")
        context.state = RunningState()

class Player:
    def __init__(self):
        self.state = IdleState()

    def press_button(self):
        self.state.handle(self)

5. Anti-Patterns

Anti-Pattern Description Improvement
God Object One class takes on too many responsibilities Split into multiple classes (SRP)
Spaghetti Code Chaotic structure, lacks modularity Introduce clear modules/layers
Golden Hammer Using the same technology for every problem Choose appropriate solutions per problem
Premature Optimization Optimizing unimportant parts too early Profile first, then optimize bottlenecks
Copy-Paste Extensive duplicated code Extract common methods/base classes
Magic Numbers Hard-coded numbers in code Use named constants
Singleton Abuse Overuse of singletons Dependency injection

6. Design Pattern Selection Guide

Need to create objects?
├── One type, control instance count → Singleton
├── Multiple types, create based on conditions → Factory
├── Complex object construction process → Builder
└── Copy from a template → Prototype

Need to compose/wrap objects?
├── Incompatible interfaces → Adapter
├── Dynamically add functionality → Decorator
├── Control access → Proxy
└── Simplify complex interfaces → Facade

Need to manage inter-object interactions?
├── One-to-many notification → Observer
├── Swappable algorithms → Strategy
├── Support undo → Command
├── State-driven behavior → State
└── Uniform traversal → Iterator

Relations to Other Topics

References

  • "Design Patterns: Elements of Reusable Object-Oriented Software" - GoF
  • "Head First Design Patterns" - Freeman & Robson
  • "Clean Architecture" - Robert C. Martin
  • Refactoring Guru: https://refactoring.guru/design-patterns

评论 #