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.