The Strategy Pattern: Replacing Conditionals with Composition
The Strategy pattern encapsulates a family of algorithms and makes them interchangeable. It is the cure for long if-elif chains that dispatch to different behavior based on a type or flag. In Python the most natural implementation uses first-class functions. Instead of defining a hierarchy of Strategy classes you pass functions as arguments. A data export module might take a formatting_strategy parameter that can be json_format, csv_format, or xml_format functions. Switching the export format is as simple as passing a different function. For more complex strategies that need state or configuration Protocol classes define the interface without requiring inheritance, embracing Python’s duck typing philosophy.
The Factory Pattern: Centralizing Object Creation
Factory patterns encapsulate object creation logic allowing you to create objects without specifying their exact class at the call site. The Factory Method pattern becomes a registry in idiomatic Python. A dictionary mapping type names to classes allows dynamic registration of new types without modifying existing code. Plugin systems built this way allow new behavior to be added by registering new classes with zero modification to the core factory code. Python’s dataclasses combined with class method factories provide an elegant way to create objects from different sources. A User.from_dict() class method and a User.from_database_row() class method are both factories, each knowing how to construct a User from a different input source.
The Observer Pattern: Event-Driven Decoupling
The Observer pattern defines a one-to-many dependency where when one object changes state all its dependents are notified automatically. This is the pattern behind event systems, pub/sub messaging, and reactive UIs. Python’s blinker library provides a clean production-tested implementation of the Observer pattern for intra-application signaling. Defining a signal and connecting handlers to it decouples the component that generates events from the components that respond to them.
The Repository Pattern: Abstracting Data Access
The Repository pattern provides a collection-like interface for accessing domain objects abstracting the underlying data store. Code that needs to retrieve or persist objects calls repository methods, unaware of whether those objects are stored in a SQL database, a document store, an API, or an in-memory dictionary. This abstraction makes testing dramatically easier. Production code uses a SQLAlchemy-backed repository. Tests use an in-memory repository that stores objects in a Python dictionary. Abstract base classes (ABC) or Protocols define the repository interface and concrete implementations for different storage backends implement this interface.
The Command Pattern: Encapsulating Actions
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. In Python the most natural implementation uses callable objects or functions. A task queue is a natural command pattern implementation where commands (callables) are pushed onto a queue and executed by workers. For undo/redo functionality the Command pattern maintains a command history stack. Execute adds a command to the history and calls its execute method. Undo pops the last command from the history and calls its undo method. Design patterns are solutions looking for problems. The right question to ask before introducing a pattern is whether this abstraction makes the code easier to understand, test, or extend.
