Python Design Patterns

Design patterns are for common problems in software developmentreusable solutions. They are not complete designs that can be directly converted into code, but rather for solving specific problemstemplates or blueprints。

Core Value of Design Patterns

Design patterns are like standard blueprints in the construction field, providing the following benefits for software development:

  • Improve code reusability: Avoid reinventing the wheel
  • Enhance code maintainability: Make the code structure clearer
  • Promote team collaboration: Provide a unified programming language and way of thinking
  • Improve code quality: Solutions tested in practice are more reliable

Analogy of Design Patterns in Daily Life

Imagine you are going to build a house:

  • Primitive way: Designing from scratch every time is error-prone and inefficient
  • Using design patterns: Use standard architectural drawings, knowing the standard layouts for living room, kitchen, and bedroom

Design patterns are the "standard architectural drawings" of software development.

Types of Design Patterns

According to the classic book"Design Patterns - Elements of Reusable Object-Oriented Software" (Chinese translation: "Design Patterns: Reusable Object-Oriented Software Elements")According to its definition, there are 23 classic design patterns. These patterns can be divided into three major categories based on their focuses: Creational Patterns, Structural Patterns, and Behavioral Patterns. Additionally, in enterprise-level development, there is also a commonly used class of architectural design patterns.

NumberPattern & DescriptionIncludes
1 Creational Patterns
Used to solve object creation problems, encapsulating instantiation logic, hiding concrete implementation details, making the system more flexible and extensible when creating objects.
  • Factory Pattern
  • Abstract Factory Pattern
  • Singleton Pattern
  • Builder Pattern
  • Prototype Pattern
2 Structural Patterns
Focus on the composition and collaboration of classes and objects, helping us build flexible, reusable, and extensible system structures.
  • Adapter Pattern
  • Bridge Pattern
  • Filter / Criteria Pattern
  • Composite Pattern
  • Decorator Pattern
  • Facade Pattern
  • Flyweight Pattern
  • Proxy Pattern
3 Behavioral Patterns
Focus on communication and responsibility assignment between objects, enhancing system flexibility and maintainability by encapsulating algorithms, states, or request logic.
  • Chain of Responsibility Pattern
  • Command Pattern
  • Interpreter Pattern
  • Iterator Pattern
  • Mediator Pattern
  • Memento Pattern
  • Observer Pattern
  • State Pattern
  • Null Object Pattern
  • Strategy Pattern
  • Template Method Pattern
  • Visitor Pattern
4 Enterprise-Level Architectural Patterns
These patterns are commonly used in layered architectures of large systems, focusing on collaboration between the presentation layer and the business logic layer.
  • MVC Pattern (Model-View-Controller Pattern)
  • Business Delegate Pattern
  • Composite Entity Pattern
  • Data Access Object Pattern
  • Front Controller Pattern
  • Intercepting Filter Pattern
  • Service Locator Pattern
  • Transfer Object Pattern

The image below shows the relationships and hierarchical structure among various design patterns:

设计模式之间的关系

Advantages of Design Patterns

  • Provide a unified design language, enabling developers to quickly communicate design intent.
  • Provide mature, reusable solutions to common problems.
  • Reduce system coupling and improve code maintainability and extensibility.
  • Reduce redundant design, improving development efficiency and code quality.
  • Help new members quickly understand the system architecture and design philosophy.

Six Principles of Design Patterns

1. Open-Closed Principle

Open for extension, closed for modification. Software should adapt to changes by extending new functionality rather than modifying existing code. The core of implementing this principle lies in using abstractions (interfaces or base classes) to define stable behavior.

2. Liskov Substitution Principle

Subclasses should be able to replace any place where a base class appears. Inheritance is meaningful only when derived classes can completely replace the base class. This principle ensures the correctness of polymorphism and is an important complement to the Open-Closed Principle.

3. Dependency Inversion Principle

High-level modules should not depend on low-level modules; both should depend on abstractions. Concrete implementations should depend on interfaces or abstract classes rather than directly on concrete classes. This principle makes the system easier to extend and test.

4. Interface Segregation Principle

A class should not depend on interfaces it does not need. Large interfaces should be split into smaller, more specific ones to reduce coupling and improve flexibility.

5. Law of Demeter

Also known as the "Principle of Least Knowledge": an object should minimize its interactions with other objects. In other words, a class should only know about objects directly related to itself, thereby reducing system complexity.

6. Composite Reuse Principle

Prefer using composition or aggregation relationships for reuse over inheritance. Inheritance creates strong coupling, while composition allows flexible replacement of dependencies at runtime, achieving more elegant structural design.


Common Design Pattern Examples in Python

Let's understand the application of design patterns in Python through a few concrete examples.

Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point.

Example

class DatabaseConnection:
    _instance = None
   
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            print("Creating new database connection")
        return cls._instance
   
    def connect(self):
        print("Connecting to database")

# Usage example
db1 = DatabaseConnection()
db2 = DatabaseConnection()

print(f"Are db1 and db2 the same instance? {db1 is db2}")  # Output: True

Application Scenarios:

  • Database Connection Pool
  • Configuration Manager
  • Logger

Factory Pattern

The Factory Pattern provides an interface for creating objects, allowing subclasses to decide which class to instantiate.

Example

from abc import ABC, abstractmethod

# Abstract Product
class Notification(ABC):
    @abstractmethod
    def send(self, message: str):
        pass

# Concrete Product
class EmailNotification(Notification):
    def send(self, message: str):
        print(f"Sending email: {message}")

class SMSNotification(Notification):
    def send(self, message: str):
        print(f"Sending SMS: {message}")

# Factory Class
class NotificationFactory:
    @staticmethod
    def create_notification(notification_type: str) -> Notification:
        if notification_type == "email":
            return EmailNotification()
        elif notification_type == "sms":
            return SMSNotification()
        else:
            raise ValueError("Unsupported notification type")

# Usage Example
email = NotificationFactory.create_notification("email")
sms = NotificationFactory.create_notification("sms")

email.send("Your order has been shipped")
sms.send("Verification code: 123456")

Code Explanation:

  • Notificationis an abstract base class that defines the interface
  • EmailNotificationandSMSNotificationis the concrete implementation
  • NotificationFactoryis responsible for creating concrete notification objects

Observer Pattern

The Observer Pattern defines a one-to-many dependency between objects. When one object's state changes, all objects that depend on it are notified.

Example

from abc import ABC, abstractmethod

# Observer Interface
class Observer(ABC):
    @abstractmethod
    def update(self, message: str):
        pass

# Concrete Observer
class EmailSubscriber(Observer):
    def __init__(self, name: str):
        self.name = name
   
    def update(self, message: str):
        print(f"{self.name} received email notification: {message}")

class SMSSubscriber(Observer):
    def __init__(self, name: str):
        self.name = name
   
    def update(self, message: str):
        print(f"{self.name} received SMS notification: {message}")

# Subject (Observable)
class NewsPublisher:
    def __init__(self):
        self._subscribers = []
   
    def subscribe(self, subscriber: Observer):
        self._subscribers.append(subscriber)
   
    def unsubscribe(self, subscriber: Observer):
        self._subscribers.remove(subscriber)
   
    def notify_subscribers(self, message: str):
        for subscriber in self._subscribers:
            subscriber.update(message)

# Usage Example
publisher = NewsPublisher()

# Create subscribers
alice = EmailSubscriber("Alice")
bob = SMSSubscriber("Bob")

# Subscribe to news
publisher.subscribe(alice)
publisher.subscribe(bob)

# Publish news
publisher.notify_subscribers("Python 3.12 has been released!")

# Unsubscribe
publisher.unsubscribe(alice)
publisher.notify_subscribers("This is a notification that only Bob can see")

Special Considerations for Design Patterns in Python

Python's Dynamic Nature

As a dynamic language, Python makes some design patterns simpler to implement:

Example

# Python-style simple factory
def create_payment(method):
    payment_methods = {
        'credit_card': CreditCardPayment,
        'paypal': PayPalPayment,
        'alipay': AlipayPayment
    }
    return payment_methods[method]()

# Use directly, no complex class hierarchy needed
payment = create_payment('alipay')

Built-in Support for the Decorator Pattern

Python has built-in decorator syntax, making the decorator pattern more elegant to implement:

Example

def log_execution_time(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} execution time: {end - start:.2f} seconds")
        return result
    return wrapper

@log_execution_time
def process_data(data):
    # Simulate data processing
    import time
    time.sleep(1)
    return f"Processed data: {data}"

# Usage
result = process_data("Sample data")
Other Extensions