The Core Principles of Object Creation Design patterns emerged in an era where Object-Oriented Programming (OOP) reigned supreme. Traditionally, the Factory Pattern relies heavily on classes and inheritance to achieve its goals. At its heart, the pattern serves three vital design principles. First, it separates creation from use. By injecting objects of a specific subtype into an application, the system uses those objects without needing to know their exact implementation. This reduces coupling significantly. Second, it adheres to the **Single Responsibility Principle**. Creating an object and using that object are two distinct tasks that should not live in the same function. Finally, it supports the **Open-Closed Principle**: the system remains open for extension (adding new types of exports) but closed for modification (not changing the existing export logic). While the 90s-style implementation uses Abstract Base Classes (ABCs), modern Python offers more streamlined alternatives. Structural Typing with Protocols One of the most powerful shifts in modern Python is the move from nominal typing to structural typing via Protocols, introduced in Python 3.8. Traditional ABCs require explicit inheritance, creating a rigid vertical hierarchy. Protocols, however, work like interfaces in TypeScript. If an object has the required methods, it satisfies the type. ```python from typing import Protocol class VideoExporter(Protocol): def prepare_export(self, video_data): ... def do_export(self, folder): ... ``` By switching to `Protocol`, you eliminate the need for subclasses to explicitly inherit from a base. This matches Python's "duck typing" philosophy. The code remains clean, and the type checker still catches mismatches. However, note that by abandoning inheritance, you lose the ability to provide default method implementations in a superclass. Using Tuples as Lightweight Containers Often, a "Factory" class is just a glorified container for a few related functions or classes. You can replace entire factory hierarchies with simple Tuples. If you need to group a specific video exporter with a specific audio exporter, a tuple does this efficiently without the overhead of a class structure. ```python Mapping quality keys to tuples of classes FACTORIES = { "low": (H264BPVideoExporter, AACAudioExporter), "high": (H264HiVideoExporter, AACAudioExporter), "master": (LosslessVideoExporter, WavAudioExporter) } ``` You can then destructure these tuples at the point of use. This approach is incredibly fast and memory-efficient. The downside? You must remember the order of elements (e.g., video first, then audio), and adding configuration data to a tuple quickly becomes messy. The Pythonic Sweet Spot: Dataclasses and \_\_call__ For a balance of flexibility and readability, Dataclasses combined with the `__call__` dunder method provide a superior alternative to traditional factory objects. This allows you to treat a class instance like a function, providing a clean API for object construction. ```python from dataclasses import dataclass from typing import Type @dataclass class MediaExporterFactory: video_class: Type[VideoExporter] audio_class: Type[AudioExporter] def __call__(self) -> MediaExporter: return MediaExporter(self.video_class(), self.audio_class()) ``` This structure provides named access to components, preventing the ordering mistakes common with tuples. It maintains the decoupling of the original pattern while utilizing Python's most ergonomic features. While dataclasses are slightly slower and use more memory than tuples, the gain in code clarity is usually worth the trade-off in most application-level code. Choosing between these methods depends on your performance requirements, but moving away from rigid inheritance is almost always a win for maintainability.
dataclasses
Concepts
- Sep 10, 2021
- Jun 18, 2021