1
In-Depth Guide to Python Design Patterns: From Singleton to State

2024-10-24

Introduction

Have you ever felt that your code, though functional, lacks elegance? Or that while it runs, the architecture feels loose and hard to maintain? Today, let's dive into Python's design patterns and see how these excellent design ideas help us write better code.

Creational

When it comes to creating objects, you might think it's just about instantiating a class. But in reality, the methods of creating objects can be very flexible and powerful. Let's start with the most basic: the Singleton pattern.

The Magic of Singleton

I still remember the first time I encountered the Singleton pattern. Imagine in a large system, what would happen if resources like database connections and configuration management were repeatedly created? That's right, resource waste and inconsistent states would follow.

This is where the Singleton pattern comes into play. The Singleton pattern implemented through metaclasses is particularly elegant:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class Singleton(metaclass=SingletonMeta):
    pass

Want to verify that only one instance is created? It's simple:

singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2)  # True

I find this implementation particularly clever, using the __call__ method of metaclasses to intercept during class instantiation to ensure only one instance is created. It's more elegant than the traditional implementation in the __new__ method.

The Power of Factory

After Singleton, let's look at the Factory pattern. Have you ever encountered a scenario where you need to create different objects based on different conditions? For example, in a game, creating different enemies for different levels? The Factory pattern is tailor-made for such scenarios.

class Shape:
    @classmethod
    def factory(cls, name, *args, **kwargs):
        types = {c.__name__: c for c in cls.__subclasses__()}
        shape_class = types[name]
        return shape_class(*args, **kwargs)

This implementation uses Python's reflection mechanism, obtaining all subclasses through the __subclasses__ method, which is very Pythonic. It's also easy to use:

class Circle(Shape):
    def __str__(self):
        return "Circle"

class Square(Shape):
    def __str__(self):
        return "Square"

shapes = ["Circle", "Square"]
for shape_name in shapes:
    shape = Shape.factory(shape_name)
    print(shape)

Structural

The Elegance of MVC

Speaking of structural patterns, we can't forget about MVC. This is possibly one of the most well-known design patterns. I remember being captivated by the elegance of MVC when I first started learning web development.

Check out this simple implementation:

class Model:
    products = {
        'milk': {'price': 1.50, 'quantity': 10},
        'eggs': {'price': 0.20, 'quantity': 100},
    }

    def get(self, name):
        return self.products.get(name)

class View:
    def show_item_list(self, item_list):
        print('-' * 20)
        for item in item_list:
            print(f"* Name: {item}")
        print('-' * 20)

class Controller:
    def __init__(self, model, view):
        self.model = model
        self.view = view

    def show_items(self):
        items = self.model.products.keys()
        self.view.show_item_list(items)

This layered design keeps the code structure clear and responsibilities distinct. The model handles data, the view handles display, and the controller coordinates, allowing each part to be modified independently without affecting others.

The Wisdom of Proxy

The Proxy pattern might be one of the most underrated patterns. It can be used not only to control access to objects but also to implement caching, logging, and other functions.

class Implementation:
    def add(self, x, y):
        return x + y

class Proxy:
    def __init__(self, impl):
        self._impl = impl

    def __getattr__(self, name):
        return getattr(self._impl, name)

This implementation uses Python's __getattr__ magic method to elegantly forward all method calls.

Behavioral

Command Pattern

The Command pattern is one of my favorites, especially when implementing undo/redo functionality. Check out this implementation:

from collections import deque

class Document:
    value = ""
    cmd_stack = deque()

    @classmethod
    def execute(cls, cmd):
        cmd.redo()
        cls.cmd_stack.append(cmd)

    @classmethod
    def undo(cls):
        cmd = cls.cmd_stack.pop()
        cmd.undo()

class AddTextCommand:
    def __init__(self, text):
        self.text = text

    def redo(self):
        Document.value += self.text

    def undo(self):
        Document.value = Document.value[:-len(self.text)]

This implementation supports not only executing commands but also undoing them. Using deque as a command stack is both efficient and elegant.

State Pattern

Finally, let's talk about the State pattern. This pattern is particularly useful when handling object state changes:

class StateMachine:
    def __init__(self):
        self.state = "initial"

    def change_state(self, new_state):
        self.state = new_state
        print(f"State changed to: {self.state}")

Conclusion

Design patterns aren't a silver bullet, but they are a great assistant in software design. The important thing is not to remember the names and implementations of these patterns, but to understand the design ideas behind them. What do you think? Feel free to share your experiences and insights on using design patterns in the comments section.

Which of these design patterns do you find most useful in your actual work? Or have you encountered other interesting design pattern applications? Let's discuss and learn together.