Strategy Pattern Implementor
by @quochungto
Implement the Strategy pattern to encapsulate a family of interchangeable algorithms behind a common interface. Use when you have multiple conditional branch...
Example 1: Payment Processing (Classic Conditional to Strategy)
Scenario: An e-commerce checkout has a PaymentService with a large if/else chain selecting between credit card, PayPal, and bank transfer logic. A new payment method is requested every quarter, and each addition requires modifying PaymentService directly.
Trigger: "Every time we add a payment provider, we touch the same process_payment method and it keeps growing."
Before (the code smell):
class PaymentService:
def process_payment(self, amount: float, method: str, details: dict):
if method == "credit_card":
# validate card, charge via Stripe, handle 3DS...
elif method == "paypal":
# OAuth flow, PayPal API call, webhook...
elif method == "bank_transfer":
# IBAN validation, SEPA/ACH routing...
else:
raise ValueError(f"Unknown method: {method}")
After (Strategy):
class PaymentStrategy:
def process(self, amount: float, details: dict) -> PaymentResult:
raise NotImplementedErrorclass CreditCardStrategy(PaymentStrategy):
def process(self, amount: float, details: dict) -> PaymentResult:
# Stripe integration, 3DS, card validation
class PayPalStrategy(PaymentStrategy):
def process(self, amount: float, details: dict) -> PaymentResult:
# OAuth, PayPal API, webhook handling
class BankTransferStrategy(PaymentStrategy):
def process(self, amount: float, details: dict) -> PaymentResult:
# IBAN validation, SEPA/ACH routing
class PaymentService:
def __init__(self, strategy: PaymentStrategy):
self._strategy = strategy
def process_payment(self, amount: float, details: dict) -> PaymentResult:
return self._strategy.process(amount, details)
Caller selects the strategy β PaymentService never changes
service = PaymentService(CreditCardStrategy())
result = service.process_payment(99.99, {"card_number": "..."})
Output: PaymentService is closed to modification. Adding CryptoStrategy requires zero changes to existing code.
Example 2: Lexi Document Formatter (GoF Case Study)
Scenario: Lexi, a WYSIWYG document editor, must break text into lines. Several algorithms exist: a simple greedy line-breaker, a full TeX-quality algorithm optimizing paragraph-level color, and a fixed-interval breaker for icon grids. The algorithms have different speed-quality trade-offs and the user may switch between them.
Trigger: "The formatting algorithm needs to change at runtime depending on document type, and we need to add new algorithms without touching the document structure."
Design (from GoF, pages 48-50 and 296-298):
Context: Composition (holds content glyphs, line width)
Strategy: Compositor (abstract β Compose() interface)
ConcreteA: SimpleCompositor β greedy, line-at-a-time, fast
ConcreteB: TeXCompositor β paragraph-at-a-time, optimizes whitespace color
ConcreteC: ArrayCompositor β fixed interval, for icon grids
The Compositor interface takes all layout data as explicit parameters (Approach 1 β data to the strategy):
virtual int Compose(
Coord natural[], Coord stretch[], Coord shrink[],
int componentCount, int lineWidth, int breaks[]
) = 0;
This interface is wide enough to support all three algorithms. SimpleCompositor ignores stretchability; ArrayCompositor ignores everything except component count. This is the communication overhead trade-off β some ConcreteStrategies receive data they do not use. The GoF accept this because the alternative (passing this as a context reference) would couple Compositor subclasses to Composition's full interface.
Runtime swap:
// Switching quality at runtime β no reconstruction of Composition
composition->SetCompositor(new TeXCompositor());
composition->Repair(); // delegates to compositor->Compose(...)
Output: Adding a new linebreaking algorithm is a new Compositor subclass. Neither Composition nor the glyph classes are ever modified.
Example 3: Report Exporter with Runtime Selection
Scenario: A reporting tool exports data as CSV, JSON, or PDF. The format is determined by user selection at runtime. Currently the export logic lives in a single export() method with a format string parameter driving a conditional.
Trigger: "Users can choose the export format from a dropdown β the format isn't known until they click Export."
Strategy interface (data to strategy, Approach 1):
class ExportStrategy:
def export(self, records: list[dict], output_path: str) -> None:
raise NotImplementedErrorclass CsvExportStrategy(ExportStrategy):
def export(self, records, output_path):
import csv
with open(output_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=records[0].keys())
writer.writeheader()
writer.writerows(records)
class JsonExportStrategy(ExportStrategy):
def export(self, records, output_path):
import json
with open(output_path, "w") as f:
json.dump(records, f, indent=2)
class PdfExportStrategy(ExportStrategy):
def export(self, records, output_path):
# reportlab or weasyprint integration
...
class ReportExporter:
_STRATEGIES = {
"csv": CsvExportStrategy,
"json": JsonExportStrategy,
"pdf": PdfExportStrategy,
}
def __init__(self):
self._strategy: ExportStrategy = CsvExportStrategy() # default
def set_format(self, format_key: str) -> None:
"""Runtime swap β called when user selects format in UI."""
cls = self._STRATEGIES.get(format_key)
if not cls:
raise ValueError(f"Unknown format: {format_key}")
self._strategy = cls()
def export(self, records: list[dict], output_path: str) -> None:
self._strategy.export(records, output_path)
Key decision: The _STRATEGIES registry centralizes selection logic in the Context, keeping callers simple. An alternative is to move this registry to a factory, which is preferable when selection logic becomes complex (e.g., involving feature flags or user tier).
Output: The UI dropdown maps to set_format(). Adding XML export is a new XmlExportStrategy class plus one entry in _STRATEGIES β the rest of the system is unchanged.
clawhub install bookforge-strategy-pattern-implementor