Python Design Patterns: Factory, Strategy, Observer, and More
Design patterns are proven recipes for solving common software problems. Think of them like architectural blueprints -- you wouldn't redesign plumbing from scratch for every house. Patterns give your code a shared vocabulary so other developers instantly understand your intent.
Why Design Patterns in Python?
The classic "Gang of Four" patterns were written for Java and C++, languages with rigid type systems. Python's flexibility means many patterns are simpler -- or even unnecessary. A Strategy pattern that takes 20 lines in Java takes 3 in Python with first-class functions. We'll cover both the traditional OOP approach and the Pythonic shortcut for each pattern.
Factory Pattern: Let Someone Else Build It
The Factory pattern delegates object creation to a separate function or class. Instead of calling constructors directly, you ask a factory to give you the right object. This is perfect when you have a family of related classes and the choice depends on some input.
Strategy Pattern: Swap Behavior at Runtime
The Strategy pattern lets you change an algorithm without modifying the object that uses it. In Java, you'd create an interface and multiple implementing classes. In Python, you can just pass a function.
class SortStrategy:
def sort(self, data): ...
class BubbleSort(SortStrategy):
def sort(self, data):
return sorted(data)
class ReverseSort(SortStrategy):
def sort(self, data):
return sorted(data, reverse=True)
class Sorter:
def __init__(self, strategy):
self.strategy = strategy
def do_sort(self, data):
return self.strategy.sort(data)def sort_ascending(data):
return sorted(data)
def sort_descending(data):
return sorted(data, reverse=True)
def do_sort(data, strategy=sort_ascending):
return strategy(data)
print(do_sort([3,1,2], sort_descending))Observer Pattern: Notify When Things Change
The Observer pattern lets objects subscribe to events and get notified automatically when something changes. Think of it like a newsletter -- subscribers sign up, and the publisher sends updates to everyone on the list.
Singleton Pattern: One Instance Only
A Singleton ensures a class has only one instance. It's useful for shared resources like database connections or configuration objects. In Python, the simplest approach uses a module-level variable -- modules are natural singletons since they're only imported once.
Decorator Pattern (Not @decorator)
The Decorator design pattern wraps an object to add new behavior without modifying the original class. This is different from Python's @decorator syntax, though they share the same goal of extending functionality.
Template Method: Define the Skeleton
The Template Method defines the overall algorithm in a base class and lets subclasses override specific steps. The base class controls the flow; subclasses fill in the details.
Practice Exercises
Create a factory function called create_shape that takes a string ('circle', 'square', or 'triangle') and returns an instance of the corresponding class. Each class should have an area() method:
Circle: area() returns 3.14 (simplified)Square: area() returns 4 (side=2, area=2*2)Triangle: area() returns 3.0 (base=3, height=2, area=0.5*3*2)Raise ValueError for unknown shapes. Print the area of a circle.
Create a function apply_discount(price, strategy) that takes a price and a discount strategy function, then returns the discounted price.
Define two strategy functions:
half_off(price) -- returns price * 0.5ten_percent(price) -- returns price * 0.9Print the result of applying half_off to a price of 100, then print the result of applying ten_percent to 100.
Create an EventEmitter class with:
on(event, callback) -- register a callback for an eventemit(event, *args) -- call all callbacks registered for that event, passing *argsThen:
1. Create an emitter instance
2. Register a callback for 'greet' that prints Hello, followed by the argument
3. Register another callback for 'greet' that prints Hi there, followed by the argument
4. Emit 'greet' with argument 'World'
What does the following code print?
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.debug = False
return cls._instance
a = Config()
a.debug = True
b = Config()
print(b.debug)
print(a is b)Create a text processing pipeline using the Decorator pattern:
1. PlainText class with __init__(self, text) and render() returning the text as-is
2. BoldText class that wraps any text object and render() returns **<inner render>**
3. UpperText class that wraps any text object and render() returns the inner render in uppercase
Create a PlainText with 'hello', wrap it in BoldText, then wrap that in UpperText. Print the final render() result.
Create a Report base class using the Template Method pattern:
generate() method that calls self.header(), self.body(), self.footer() in orderheader() prints === Report ===footer() prints === End ===body() should raise NotImplementedErrorThen create SalesReport(Report) that overrides body() to print Sales: $1000.
Create a SalesReport instance and call generate().