executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
factory-methodImplements the GoF Factory Method pattern for polymorphic object creation in Python using ABC-based factories, registration decorators, and type dispatch to replace if/elif chains with extensible factory hierarchies.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: factory-method description: Implements the GoF Factory Method pattern for polymorphic object creation in Python using ABC-based factories, registration decorators, and type dispatch to replace if/elif chains with extensible factory hierarchies. archetypes:
Senior Python engineer implementing the Factory Method pattern to encapsulate object creation logic behind polymorphic factory interfaces. This skill makes the model replace brittle if/elif type-dispatch chains with extensible ABC-based factories that allow subclasses to decide which concrete class to instantiate without the caller knowing concrete types.
create() method returning the product typeUse this skill when:
if/elif type checks that grow unboundedly as new types are addedAvoid this skill for:
__init__ calls insteadDefine the Product ABC — Create an abstract base class defining the interface all products must implement. Include @abstractmethod for all required methods. Checkpoint: Every concrete product must implement every abstract method; use typing.Protocol if you prefer duck-typing over inheritance.
Define the Factory ABC — Create an abstract base class with an abstract create() method (or similar) that returns a product instance. The factory ABC declares the creation contract without specifying concrete types. Checkpoint: The factory's return type should be the product ABC, not a concrete implementation.
Implement Concrete Products — Create concrete classes implementing the product ABC. Each represents one variant of the created object with its own behavior. Checkpoint: Verify each concrete product passes duck-typing against the product ABC using isinstance() checks in tests.
Implement Concrete Factories — For each concrete product, create a factory subclass that instantiates and returns the corresponding product. Each factory encapsulates one creation path. Checkpoint: The factory should not leak concrete types into caller code; always return via the product ABC interface.
Add Registration Mechanism — Implement a registry using module-level dict or typing.Literal mapping that maps type identifiers to factory classes. Use functools.singledispatch or custom decorators for auto-discovery. Checkpoint: New factory variants should register themselves automatically via decorator; no manual registration code needed.
Wire the Type Dispatch — Provide a lookup function that receives a type identifier and returns the appropriate factory, then calls its create() method. Checkpoint: Lookup must raise a descriptive error for unknown types rather than returning None silently.
This is the canonical GoF Factory Method in Python using the abc module. The factory ABC declares the creation contract, and subclasses decide which concrete product to instantiate.
from abc import ABC, abstractmethod
import json
# Product ABC
class PaymentProcessor(ABC):
"""Abstract base class defining the payment processing interface."""
@abstractmethod
def process_payment(self, amount: float, currency: str) -> dict:
"""Process a payment and return transaction details.
Args:
amount: Positive numeric amount to charge.
currency: ISO 4217 currency code (e.g., 'USD', 'EUR').
Returns:
Dict with keys: transaction_id, status, fee.
"""
...
@abstractmethod
def supports_currency(self, currency: str) -> bool:
"""Check if this processor can handle the given currency."""
...
# Concrete Products
class StripeProcessor(PaymentProcessor):
"""Processes payments via Stripe API."""
def __init__(self, api_key: str, webhook_url: str | None = None) -> None:
self.api_key = api_key
self.webhook_url = webhook_url
self._supported_currencies = {"USD", "EUR", "GBP", "CAD"}
def process_payment(self, amount: float, currency: str) -> dict:
if not self.supports_currency(currency):
raise ValueError(f"Stripe does not support {currency}")
return {
"transaction_id": f"stripe_{hash((amount, currency))}",
"status": "completed",
"fee": round(amount * 0.029 + 0.30, 2),
}
def supports_currency(self, currency: str) -> bool:
return currency in self._supported_currencies
class PayPalProcessor(PaymentProcessor):
"""Processes payments via PayPal API."""
def __init__(self, client_id: str, client_secret: str) -> None:
self.client_id = client_id
self.client_secret = client_secret
self._supported_currencies = {"USD", "EUR", "JPY"}
def process_payment(self, amount: float, currency: str) -> dict:
if not self.supports_currency(currency):
raise ValueError(f"PayPal does not support {currency}")
return {
"transaction_id": f"paypal_{hash((amount, currency))}",
"status": "completed",
"fee": round(amount * 0.034 + 0.49, 2),
}
def supports_currency(self, currency: str) -> bool:
return currency in self._supported_currencies
class CryptoProcessor(PaymentProcessor):
"""Processes payments via cryptocurrency (on-chain)."""
def __init__(self, network: str = "mainnet") -> None:
self.network = network
def process_payment(self, amount: float, currency: str) -> dict:
return {
"transaction_id": f"tx_{hash((amount, currency, self.network))}",
"status": "pending_confirmation",
"fee": 0.00,
}
def supports_currency(self, currency: str) -> bool:
return currency.upper() in {"BTC", "ETH", "USDC"}
# Factory ABC
class PaymentProcessorFactory(ABC):
"""Abstract factory that creates payment processor instances."""
@abstractmethod
def create_processor(self, **config: object) -> PaymentProcessor:
"""Create and return a configured payment processor.
Args:
**config: Provider-specific configuration (api_key, client_id, etc.).
Returns:
A configured PaymentProcessor instance.
Raises:
NotImplementedError: If the provider type is not supported.
"""
...
# Concrete Factory
class StripeFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return StripeProcessor(
api_key=config.get("api_key", ""),
webhook_url=config.get("webhook_url"),
)
class PayPalFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return PayPalProcessor(
client_id=config.get("client_id", ""),
client_secret=config.get("client_secret", ""),
)
class CryptoFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return CryptoProcessor(network=config.get("network", "mainnet"))
The BAD approach uses manual registration that must be updated every time a new processor is added. The GOOD approach uses a decorator for zero-touch auto-discovery.
# ❌ BAD — Manual registry requires updating when adding new types
PROVIDER_REGISTRY: dict[str, type[PaymentProcessorFactory]] = {
"stripe": StripeFactory,
"paypal": PayPalFactory,
} # Forgot to add crypto? Now a silent runtime error.
def create_processor_manual(provider: str, **config: object) -> PaymentProcessor:
factory_cls = PROVIDER_REGISTRY.get(provider)
if factory_cls is None:
raise LookupError(
f"Unknown provider '{provider}'. "
f"Available: {list(PROVIDER_REGISTRY.keys())}"
)
return factory_cls().create_processor(**config)
# ✅ GOOD — Registration decorator provides auto-discovery
_PROVIDER_REGISTRY: dict[str, type[PaymentProcessorFactory]] = {}
def register_provider(name: str):
"""Class decorator that auto-registers a PaymentProcessorFactory subclass."""
def decorator(cls: type[PaymentProcessorFactory]) -> type[PaymentProcessorFactory]:
_PROVIDER_REGISTRY[name] = cls
return cls
return decorator
@register_provider("stripe")
class StripeAutoFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return StripeProcessor(
api_key=config.get("api_key", ""),
webhook_url=config.get("webhook_url"),
)
@register_provider("paypal")
class PayPalAutoFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return PayPalProcessor(
client_id=config.get("client_id", ""),
client_secret=config.get("client_secret", ""),
)
@register_provider("crypto")
class CryptoAutoFactory(PaymentProcessorFactory):
def create_processor(self, **config: object) -> PaymentProcessor:
return CryptoProcessor(network=config.get("network", "mainnet"))
def create_processor_auto(provider: str, **config: object) -> PaymentProcessor:
"""Create a processor using the auto-discovery registry.
Args:
provider: Provider name registered via @register_provider decorator.
**config: Provider-specific configuration.
Returns:
A configured PaymentProcessor instance.
Raises:
LookupError: If provider is not registered.
"""
factory_cls = _PROVIDER_REGISTRY.get(provider)
if factory_cls is None:
raise LookupError(
f"Unknown provider '{provider}'. "
f"Available: {list(_PROVIDER_REGISTRY.keys())}"
)
return factory_cls().create_processor(**config)
# Usage — no manual registry updates needed:
# processor = create_processor_auto("stripe", api_key="sk_test_...")
For strongly-typed factories where each factory produces a specific concrete product type, use typing.TypeVar to preserve type information through the factory method.
from typing import TypeVar, Protocol, Any
# Strictly typed product protocol (duck-typing alternative to ABC)
class Drawable(Protocol):
"""Protocol for any object that can render itself."""
def draw(self, ctx: Any) -> None: ...
def bounds(self) -> tuple[float, float, float, float]: ...
T = TypeVar("T", bound=Drawable)
# Generic factory using TypeVar — preserves concrete type in return annotation
class ShapeFactory(ABC):
"""Generic shape factory that produces Drawable shapes."""
@abstractmethod
def create(self, **params: Any) -> T: ...
class CircleFactory(ShapeFactory["Circle"]):
def create(self, radius: float = 1.0, color: str = "#FFFFFF") -> "Circle":
return Circle(radius=radius, color=color)
class SquareFactory(ShapeFactory["Square"]):
def create(self, side: float = 1.0, color: str = "#FFFFFF") -> "Square":
return Square(side=side, color=color)
# Concrete products
class Circle:
"""A circle drawable shape."""
def __init__(self, radius: float = 1.0, color: str = "#FFFFFF") -> None:
self.radius = radius
self.color = color
def draw(self, ctx: Any) -> None:
ctx.circle(self.radius, self.color)
def bounds(self) -> tuple[float, float, float, float]:
r = self.radius
return (-r, -r, r * 2, r * 2)
class Square:
"""A square drawable shape."""
def __init__(self, side: float = 1.0, color: str = "#FFFFFF") -> None:
self.side = side
self.color = color
def draw(self, ctx: Any) -> None:
ctx.rect(0, 0, self.side, self.side, self.color)
def bounds(self) -> tuple[float, float, float, float]:
s = self.side
return (0, 0, s, s)
# Typed factory registry using Literal for compile-time checking
from typing import Literal
SHAPE_REGISTRY: dict[Literal["circle", "square"], ShapeFactory] = {
"circle": CircleFactory(),
"square": SquareFactory(),
}
def create_shape(shape_type: Literal["circle", "square"], **params: Any) -> Drawable:
"""Create a shape via the typed registry.
Args:
shape_type: Must be 'circle' or 'square'.
**params: Shape-specific parameters (radius, side, color).
Returns:
A Drawable shape instance.
"""
factory = SHAPE_REGISTRY.get(shape_type)
if factory is None:
raise ValueError(f"Unknown shape type '{shape_type}'")
return factory.create(**params)
create() — never leak concrete types to callersisinstance checks on concrete types inside caller code (violates DIP)| Skill | Purpose |
|---|---|
builder-pattern | Use Builder when object construction is multi-step with many optional parameters; Factory Method when you need simple polymorphic instantiation |
creational-design-patterns | Broader overview of all GoF creational patterns including Singleton, Prototype, and Abstract Factory for comparison |
strategy-pattern | Combine Factory Method (creates strategy objects) with Strategy pattern (selects algorithm at runtime) |
dependency-inversion-principle | Factory Method is the primary implementation vehicle for DIP — depends on abstractions, not concretions |
Authoritative documentation links for design patterns and Python typing.