
Design Patterns Expert
- 312 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
design-patterns-expert is an agent skill that guides selection and application of all 23 Gang of Four design patterns with anti-over-engineering checks for developers who shape services, modules, and extension points in
About
design-patterns-expert is an agent skill (version 1.0.0) that provides progressive-disclosure guidance on all 23 Gang of Four design patterns organized into five creational, seven structural, and eleven behavioral entries. It activates on pattern names, GoF categories, and problem phrases like plugin systems, undo mechanisms, or algorithm families, then recommends patterns only when at least two concrete current use cases exist per amplihack simplicity rules. Supporting files include reference-patterns.md for specifications, examples.md with ten production-ready scenarios, and antipatterns.md for misuse cases such as Singleton overuse or premature Visitor adoption. Developers reach for design-patterns-expert when refactoring services or reviewing pull requests and need pragmatic pattern matching—not pattern enthusiasm—across Factory Method, Strategy, Observer, Command, CQRS-adjacent structures, and related GoF solutions.
- Recommends patterns matched to concrete constraints
- Flags anti-patterns and needless abstraction
- Aligns interfaces with testable module boundaries
- Balances flexibility against implementation cost
- Documents pattern intent for future maintainers
Design Patterns Expert by the numbers
- 312 all-time installs (skills.sh)
- +2 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #277 of 1,354 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill design-patterns-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 312 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
Which GoF design pattern fits this refactoring?
Choose and apply proven patterns—repository, strategy, observer, CQRS—when shaping amplihack services, modules, and extension points without over-engineering.
Who is it for?
Backend developers refactoring object-oriented services who need pragmatic GoF pattern selection with explicit over-engineering warnings.
Skip if: Codebases favoring functional composition or microservice boundaries where GoF object patterns add unnecessary abstraction layers.
When should I use this skill?
Trigger design-patterns-expert when a developer asks which pattern to use, names a GoF pattern, or describes plugin, observer, strategy, or undo problems in OOP code.
What you get
Named GoF pattern recommendation, applicability rationale, anti-pattern warnings, and code-structure guidance from reference and example files.
- pattern recommendation
- anti-pattern warnings
- structural refactor guidance
By the numbers
- Catalogs all 23 Gang of Four design patterns
- Includes 10 production-ready examples in examples.md
- Published as version 1.0.0 by amplihack
Files
Gang of Four Design Patterns Expert
You are a specialized knowledge skill providing comprehensive, philosophy-aligned guidance on all 23 Gang of Four design patterns.
Navigation Guide
This skill uses progressive disclosure with supporting files for deep knowledge.
reference-patterns.md - Complete pattern specifications, decision frameworks, and how to use this skill effectively
examples.md - 10 production-ready code examples with real-world scenarios
antipatterns.md - Common mistakes and when NOT to use patterns
Start here for quick reference, request supporting files for deeper knowledge.
---
Role & Philosophy
You provide authoritative knowledge on design patterns while maintaining amplihack's ruthless simplicity philosophy. You are not a cheerleader for patterns - you are a pragmatic guide who knows when patterns help and when they over-engineer.
Simplicity First: Always start by questioning if a pattern is needed. The simplest solution that works is the best solution.
YAGNI: Warn against adding patterns "for future flexibility" without concrete current need.
Two Real Use Cases: Never recommend a pattern unless there are at least 2 actual use cases RIGHT NOW.
Patterns Serve Code: Patterns are tools, not destinations. Code shouldn't be contorted to fit a pattern.
---
Pattern Catalog
Quick reference catalog of all 23 patterns organized by category.
Creational Patterns (5)
Object creation mechanisms to increase flexibility and code reuse.
1. Factory Method - Define interface for creating objects, let subclasses decide which class to instantiate 2. Abstract Factory - Create families of related objects without specifying concrete classes 3. Builder - Construct complex objects step by step with same construction process creating different representations 4. Prototype - Create objects by copying prototypical instance rather than instantiating 5. Singleton - Ensure class has only one instance with global access point (OFTEN OVERUSED)
Structural Patterns (7)
Compose objects into larger structures while keeping structures flexible and efficient.
6. Adapter - Convert interface of class into another interface clients expect 7. Bridge - Decouple abstraction from implementation so both can vary independently 8. Composite - Compose objects into tree structures to represent part-whole hierarchies 9. Decorator - Attach additional responsibilities to object dynamically 10. Facade - Provide unified interface to set of interfaces in subsystem 11. Flyweight - Share common state among large numbers of objects efficiently 12. Proxy - Provide surrogate or placeholder for another object to control access
Behavioral Patterns (11)
Algorithms and assignment of responsibilities between objects.
13. Chain of Responsibility - Pass request along chain of handlers until one handles it 14. Command - Encapsulate request as object to parameterize, queue, log, or support undo 15. Interpreter - Define grammar representation and interpreter for simple language (RARELY NEEDED) 16. Iterator - Access elements of aggregate sequentially without exposing underlying representation 17. Mediator - Encapsulate how set of objects interact to promote loose coupling 18. Memento - Capture and externalize object's internal state for later restoration 19. Observer - Define one-to-many dependency where state changes notify all dependents automatically 20. State - Allow object to alter behavior when internal state changes 21. Strategy - Define family of algorithms, encapsulate each, make them interchangeable 22. Template Method - Define algorithm skeleton, defer some steps to subclasses 23. Visitor - Represent operation on elements of object structure without changing element classes (COMPLEX)
---
External References
This skill synthesizes knowledge from:
- Gang of Four (1994) - The authoritative source
- Refactoring Guru, Source Making - Modern explanations
- Game Programming Patterns, Python Patterns Guide - Practical implementations
- Amplihack Philosophy - Ruthless simplicity lens
See reference-patterns.md for detailed pattern specifications and source citations.
Gang of Four Design Patterns - Anti-Patterns and Common Mistakes
This file documents 10 common mistakes and anti-patterns when using design patterns.
---
Anti-Pattern 1: Singleton Abuse (Global State Hell)
Symptom: Everything is a Singleton because "we only need one instance".
Bad Example:
class DatabaseConnection(Singleton):
"""DON'T DO THIS"""
pass
class Logger(Singleton):
"""DON'T DO THIS"""
pass
class Config(Singleton):
"""DON'T DO THIS"""
pass
class CacheManager(Singleton):
"""DON'T DO THIS"""
pass
# Testing nightmare - can't mock, can't isolate
def test_user_service():
service = UserService() # Implicitly uses DatabaseConnection singleton
# Can't inject mock database!Why It's Wrong:
- Creates hidden dependencies (implicit coupling)
- Makes testing nearly impossible (can't mock/inject)
- Violates Single Responsibility (manages both creation and behavior)
- Global state causes race conditions in concurrent code
- Memory leaks (singletons never garbage collected)
Correct Approach - Dependency Injection:
class UserService:
"""GOOD: Dependencies explicit and injectable"""
def __init__(self, db: DatabaseConnection, logger: Logger, config: Config):
self.db = db
self.logger = logger
self.config = config
def create_user(self, username: str) -> User:
self.logger.info(f"Creating user: {username}")
# Use injected dependencies
return self.db.insert(User(username))
# Easy to test - inject mocks
def test_user_service():
mock_db = MockDatabase()
mock_logger = MockLogger()
mock_config = MockConfig()
service = UserService(mock_db, mock_logger, mock_config)
# Full control over dependencies!When Singleton IS Acceptable:
- True hardware resources (printer spooler, device driver)
- Read-only configuration loaded once at startup
- Application-wide logging infrastructure (with careful design)
- Criteria: Must be genuinely global, immutable after init, and benefit from shared instance
---
Anti-Pattern 2: Factory Overuse ("Just In Case" Factories)
Symptom: Every class has a factory "for future flexibility" even with only one implementation.
Bad Example:
# DON'T DO THIS - Over-engineered for single use case
class UserFactory:
"""Factory for User... but we only have one User type"""
def create_user(self, name: str, email: str) -> User:
return User(name, email) # Could just call User() directly!
class EmailFactory:
"""Factory for Email... but we only send one email type"""
def create_email(self, to: str, subject: str, body: str) -> Email:
return Email(to, subject, body) # Just call Email()!
# Usage - pointless indirection
factory = UserFactory()
user = factory.create_user("Alice", "alice@example.com")
# VS direct (simpler, clearer)
user = User("Alice", "alice@example.com")Why It's Wrong:
- YAGNI violation ("You Aren't Gonna Need It")
- Adds complexity with zero benefit
- Harder to understand (extra layer of indirection)
- More classes to maintain
Correct Approach - Wait Until You Need It:
# Start simple
user = User("Alice", "alice@example.com")
# Add Factory ONLY when you have ≥2 implementations
# Example: NOW we have multiple user types
class UserFactory:
"""GOOD: Factory justified by multiple concrete types"""
def create_user(self, user_type: str, **kwargs) -> User:
if user_type == "admin":
return AdminUser(**kwargs)
elif user_type == "guest":
return GuestUser(**kwargs)
elif user_type == "premium":
return PremiumUser(**kwargs)
else:
return RegularUser(**kwargs)When Factory IS Appropriate:
- ≥2 concrete product types exist NOW (not "might exist")
- Object creation involves complex logic/configuration
- Need to decouple client from concrete classes
- Runtime type determination required
---
Anti-Pattern 3: Pattern Stuffing (Using All Patterns)
Symptom: Project uses 10+ patterns in 1000 lines of code because "patterns are best practices".
Bad Example:
# DON'T DO THIS - Pattern stuffing for simple to-do app
# Singleton + Factory + Builder + Observer + Strategy + Command
# ...for a simple CRUD app!
class TodoListSingleton(Singleton):
"""Unnecessary Singleton"""
pass
class TodoFactory(AbstractFactory):
"""Unnecessary Factory"""
def create_todo(self): pass
def create_list(self): pass
class TodoBuilder:
"""Unnecessary Builder for simple object"""
def __init__(self):
self.todo = None
def set_title(self, title): pass
def set_due_date(self, date): pass
def build(self): return self.todo
class TodoObserver(Observer):
"""Unnecessary Observer"""
pass
# Just to create a single to-do item!
singleton = TodoListSingleton()
factory = TodoFactory()
builder = factory.create_builder()
todo = builder.set_title("Buy milk").set_due_date("2024-01-01").build()Why It's Wrong:
- Over-engineering simple problems
- Code becomes unreadable (too many abstractions)
- Maintenance nightmare
- Poor onboarding (new devs overwhelmed)
Correct Approach - Ruthless Simplicity:
# GOOD: Simple, direct, readable
@dataclass
class Todo:
"""Simple data class"""
title: str
due_date: Optional[datetime] = None
completed: bool = False
class TodoList:
"""Simple class with clear responsibilities"""
def __init__(self):
self.todos: List[Todo] = []
def add(self, todo: Todo) -> None:
self.todos.append(todo)
def complete(self, index: int) -> None:
self.todos[index].completed = True
def get_incomplete(self) -> List[Todo]:
return [t for t in self.todos if not t.completed]
# Usage - clear and simple
todo_list = TodoList()
todo_list.add(Todo("Buy milk", datetime(2024, 1, 1)))Guideline: Start with simplest solution. Add patterns ONLY when:
- Complexity justifies them
- ≥2 concrete use cases exist NOW
- Pattern reduces overall system complexity
---
Anti-Pattern 4: Abstract Factory for Small Families
Symptom: Using Abstract Factory when you have only 1-2 products or 1-2 families.
Bad Example:
# DON'T DO THIS - Abstract Factory for tiny product family
class GUIFactory(ABC):
"""Abstract Factory for... 1 product type?"""
@abstractmethod
def create_button(self) -> Button:
pass
class WindowsFactory(GUIFactory):
def create_button(self) -> Button:
return WindowsButton()
class MacFactory(GUIFactory):
def create_button(self) -> Button:
return MacButton()
# Usage - overcomplicated for one product type
factory = WindowsFactory()
button = factory.create_button() # Just call WindowsButton()!Why It's Wrong:
- Abstract Factory justified only for ≥3 products AND ≥2 families
- Single product = use Factory Method instead
- Adds unnecessary abstraction layers
Correct Approach - Use Simpler Pattern:
# GOOD: Factory Method for single product type
class ButtonFactory:
"""Factory Method sufficient for single product"""
@staticmethod
def create_button(platform: str) -> Button:
if platform == "windows":
return WindowsButton()
elif platform == "mac":
return MacButton()
else:
raise ValueError(f"Unknown platform: {platform}")
# Or even simpler - direct instantiation with parameter
class Button:
def __init__(self, platform: str):
self.platform = platform
@classmethod
def for_platform(cls, platform: str) -> 'Button':
# Factory method built into class
return cls(platform)Use Abstract Factory When:
- ≥3 product types (Button, Checkbox, TextBox)
- ≥2 product families (Windows, Mac, Linux)
- Products must be used together (consistency enforced)
---
Anti-Pattern 5: Observer With No Unsubscribe (Memory Leaks)
Symptom: Observers are attached but never detached, causing memory leaks.
Bad Example:
# DON'T DO THIS - No detach mechanism
class EventPublisher:
def __init__(self):
self.subscribers = []
def subscribe(self, observer):
self.subscribers.append(observer)
# Missing: unsubscribe method!
def notify(self, event):
for observer in self.subscribers:
observer.update(event)
# Problem: Observers never garbage collected
class UIComponent:
def __init__(self, publisher):
publisher.subscribe(self) # Component registered
# When component destroyed, subscriber reference remains!
def update(self, event):
print(f"Got event: {event}")
# Memory leak: 1000 dead components still in subscribers list!
publisher = EventPublisher()
for i in range(1000):
component = UIComponent(publisher)
del component # Destroyed, but subscriber reference remains!Why It's Wrong:
- Memory leaks (observers never garbage collected)
- Performance degradation (notify iterates over dead observers)
- Unexpected behavior (dead observers may still receive notifications)
Correct Approach - Proper Lifecycle Management:
# GOOD: Proper subscribe/unsubscribe with context manager
class EventPublisher:
def __init__(self):
self._subscribers: List[Observer] = []
def subscribe(self, observer: Observer) -> None:
if observer not in self._subscribers:
self._subscribers.append(observer)
def unsubscribe(self, observer: Observer) -> None:
"""IMPORTANT: Allow observers to unsubscribe"""
if observer in self._subscribers:
self._subscribers.remove(observer)
def notify(self, event: Any) -> None:
# Iterate over copy (allows unsubscribe during iteration)
for observer in list(self._subscribers):
observer.update(event)
class UIComponent:
def __init__(self, publisher: EventPublisher):
self.publisher = publisher
self.publisher.subscribe(self)
def update(self, event: Any) -> None:
print(f"Got event: {event}")
def cleanup(self) -> None:
"""IMPORTANT: Unsubscribe when component destroyed"""
self.publisher.unsubscribe(self)
def __del__(self):
"""Automatic cleanup on garbage collection"""
self.cleanup()
# Even better: Context manager
from contextlib import contextmanager
@contextmanager
def subscribe_to(publisher: EventPublisher, observer: Observer):
"""Auto-unsubscribe when exiting context"""
publisher.subscribe(observer)
try:
yield
finally:
publisher.unsubscribe(observer)
# Usage
with subscribe_to(publisher, component):
# Component receives events
pass
# Automatically unsubscribed---
Anti-Pattern 6: Visitor for Simple Operations
Symptom: Using complex Visitor pattern when simple polymorphism suffices.
Bad Example:
# DON'T DO THIS - Visitor for simple operation
class Shape(ABC):
@abstractmethod
def accept(self, visitor):
pass
class Circle(Shape):
def accept(self, visitor):
visitor.visit_circle(self)
class Square(Shape):
def accept(self, visitor):
visitor.visit_square(self)
class AreaVisitor:
"""Complex visitor for simple calculation!"""
def visit_circle(self, circle):
return 3.14 * circle.radius ** 2
def visit_square(self, square):
return square.side ** 2
# Usage - overcomplicated
circle = Circle()
visitor = AreaVisitor()
area = circle.accept(visitor) # Why not circle.area()?Why It's Wrong:
- Visitor is heavyweight (double dispatch complexity)
- Simple polymorphism is clearer
- Harder to understand and maintain
- Adds many classes for simple operations
Correct Approach - Simple Polymorphism:
# GOOD: Simple polymorphism for simple operations
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Simple polymorphic method"""
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14 * self.radius ** 2
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
# Usage - clear and simple
circle = Circle(5)
square = Square(4)
print(f"Circle area: {circle.area()}")
print(f"Square area: {square.area()}")Use Visitor When:
- ≥5 different operations on stable structure
- Structure rarely changes, but operations change frequently
- Operations don't belong in element classes (violate SRP)
---
Anti-Pattern 7: Deep Decorator Chains (Debugging Nightmare)
Symptom: Stacking 5+ decorators creating unreadable, unmaintainable code.
Bad Example:
# DON'T DO THIS - Decorator chain hell
connection = LoggingDecorator(
RetryDecorator(
TimeoutDecorator(
CacheDecorator(
CompressionDecorator(
EncryptionDecorator(
AuthenticationDecorator(
RateLimitDecorator(
BaseConnection()
)
)
)
)
)
)
)
)
# What order do decorators execute?
# Which decorator is causing the bug?
# How to test individual decorators?
# Debugging nightmare!Why It's Wrong:
- Hard to read (nested structure unclear)
- Difficult to debug (which decorator failed?)
- Inflexible (hard to reorder or remove decorators)
- Testing nightmare (can't test decorators in isolation)
Correct Approach - Pipeline or Builder Pattern:
# GOOD: Pipeline pattern with explicit builder
class ConnectionBuilder:
"""Builder for creating decorated connections"""
def __init__(self, connection: Connection):
self._connection = connection
self._decorators: List[str] = []
def with_logging(self) -> 'ConnectionBuilder':
self._connection = LoggingDecorator(self._connection)
self._decorators.append("logging")
return self
def with_retry(self, max_attempts: int = 3) -> 'ConnectionBuilder':
self._connection = RetryDecorator(self._connection, max_attempts)
self._decorators.append("retry")
return self
def with_encryption(self) -> 'ConnectionBuilder':
self._connection = EncryptionDecorator(self._connection)
self._decorators.append("encryption")
return self
def build(self) -> Connection:
print(f"Built connection with: {', '.join(self._decorators)}")
return self._connection
# Usage - readable, testable, debuggable
connection = (
ConnectionBuilder(BaseConnection())
.with_logging()
.with_retry(max_attempts=3)
.with_encryption()
.build()
)Guideline:
- Limit decorator chains to 2-3 decorators
- Use builder pattern for complex configurations
- Consider alternatives (Strategy, middleware pipeline)
---
Anti-Pattern 8: Command Without Undo (Missing Core Benefit)
Symptom: Implementing Command pattern but not supporting undo/redo.
Bad Example:
# DON'T DO THIS - Command without undo = just callbacks!
class Command(ABC):
@abstractmethod
def execute(self):
pass
# Missing: undo() method!
class PrintCommand(Command):
def __init__(self, message):
self.message = message
def execute(self):
print(self.message) # Can't undo print!
# This is just a callback - use functions instead!
def print_message(message):
print(message)Why It's Wrong:
- Command pattern's main benefit is undo/redo support
- Without undo, it's just callbacks with extra complexity
- Simple functions/lambdas are clearer
Correct Approach - Either Support Undo or Use Callbacks:
# Option 1: GOOD - Command WITH undo support
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
"""IMPORTANT: Support undo for Command pattern"""
pass
class AddTextCommand(Command):
def __init__(self, document, text, position):
self.document = document
self.text = text
self.position = position
def execute(self):
self.document.insert(self.position, self.text)
def undo(self):
self.document.delete(self.position, len(self.text))
# Option 2: GOOD - Use simple callbacks when no undo needed
from typing import Callable
class Button:
def __init__(self, on_click: Callable[[], None]):
self.on_click = on_click
def click(self):
self.on_click()
# Usage - simpler than Command without undo
button = Button(lambda: print("Clicked!"))
button.click()Use Command When:
- Need undo/redo functionality
- Need to queue/log/schedule operations
- Need macro commands (composite)
- Don't Use: For simple callbacks (use functions/lambdas)
---
Anti-Pattern 9: State Machine for Simple Booleans
Symptom: Using State pattern when simple boolean flags suffice.
Bad Example:
# DON'T DO THIS - State pattern for on/off
class State(ABC):
@abstractmethod
def toggle(self, light):
pass
class OnState(State):
def toggle(self, light):
light.state = OffState()
print("Light OFF")
class OffState(State):
def toggle(self, light):
light.state = OnState()
print("Light ON")
class Light:
def __init__(self):
self.state = OffState()
def toggle(self):
self.state.toggle(self)
# Usage - overcomplicated for boolean!
light = Light()
light.toggle() # Just use: light.is_on = not light.is_onWhy It's Wrong:
- State pattern overkill for 2-3 simple states
- Boolean/enum is clearer and simpler
- More classes to maintain for no benefit
Correct Approach - Use Simple State Variables:
# GOOD: Simple boolean for simple cases
class Light:
def __init__(self):
self.is_on = False
def toggle(self):
self.is_on = not self.is_on
print("Light ON" if self.is_on else "Light OFF")
# Usage - clear and simple
light = Light()
light.toggle()
# GOOD: Use State pattern for complex state machines (≥5 states)
from enum import Enum
class DocumentState(Enum):
DRAFT = "draft"
REVIEW = "review"
APPROVED = "approved"
PUBLISHED = "published"
ARCHIVED = "archived"
class Document:
"""Complex state machine - State pattern justified"""
def __init__(self):
self.state = DocumentState.DRAFT
def submit_for_review(self):
if self.state == DocumentState.DRAFT:
self.state = DocumentState.REVIEW
else:
raise ValueError(f"Can't review from {self.state}")
def approve(self):
if self.state == DocumentState.REVIEW:
self.state = DocumentState.APPROVED
else:
raise ValueError(f"Can't approve from {self.state}")
# Complex transitions justify State patternUse State Pattern When:
- ≥5 states with complex transitions
- State-specific behavior is substantial
- Need to enforce state transition rules
- Don't Use: For simple on/off or 2-3 state systems
---
Anti-Pattern 10: Template Method With No Variance
Symptom: Template Method where all subclasses implement steps identically.
Bad Example:
# DON'T DO THIS - Template Method with no variance
class DataProcessor(ABC):
def process(self):
self.load()
self.transform()
self.save()
@abstractmethod
def load(self): pass
@abstractmethod
def transform(self): pass
@abstractmethod
def save(self): pass
class JSONProcessor(DataProcessor):
def load(self):
return json.load() # Same as others
def transform(self):
return data.upper() # Same as others
def save(self):
return json.dump() # Same as others
class XMLProcessor(DataProcessor):
"""Only difference is load/save format!"""
def load(self):
return xml.parse() # Different
def transform(self):
return data.upper() # SAME - shouldn't be overridden!
def save(self):
return xml.write() # DifferentWhy It's Wrong:
- Template Method requires both invariant AND variant parts
- If everything varies: use Strategy instead
- If nothing varies: don't use pattern at all
- Forces override of methods that shouldn't vary
Correct Approach - Strategy for Variable Algorithms:
# GOOD: Strategy pattern when algorithm varies
class DataProcessor:
"""Single class with strategy for variable parts"""
def __init__(self, loader, saver):
self.loader = loader
self.saver = saver
def process(self, input_path, output_path):
# Load (variable)
data = self.loader.load(input_path)
# Transform (invariant - shared by all)
transformed = data.upper()
# Save (variable)
self.saver.save(transformed, output_path)
# Strategies only for variable parts
class JSONLoader:
def load(self, path):
return json.load(open(path))
class XMLLoader:
def load(self, path):
return xml.parse(path)
# Usage - clearer separation of concerns
json_processor = DataProcessor(JSONLoader(), JSONSaver())
xml_processor = DataProcessor(XMLLoader(), XMLSaver())Use Template Method When:
- Algorithm has BOTH invariant and variant parts
- Invariant parts substantial (≥3 fixed steps)
- Variant parts are minority (1-2 customization points)
- Don't Use: When everything varies (Strategy) or nothing varies (simple function)
---
Summary of Guidelines
Before using a pattern, ask:
1. Do I have ≥2 concrete use cases RIGHT NOW? (not "might need later") 2. Is there a simpler solution? (functions, simple classes, composition) 3. Does the pattern reduce overall complexity? (or just add abstraction?) 4. Will future me understand this? (or be confused by overengineering?)
Pattern Selection Checklist:
- Singleton: Only for truly global, immutable resources (prefer DI)
- Factory: Only when ≥2 product types exist now
- Abstract Factory: Only when ≥3 products AND ≥2 families
- Observer: Only with dynamic observer set (≥2 observers)
- Strategy: Only with ≥3 swappable algorithms
- Command: Only when undo/redo/queuing needed
- Visitor: Only with ≥5 operations on stable structure
- Decorator: Limit to 2-3 decorators (use builder for more)
- State: Only with ≥5 states and complex transitions
- Template Method: Only with substantial invariant parts
Remember: Patterns are tools, not goals. The best code is often the simplest code that works.
Gang of Four Design Patterns - Production Examples
This file contains 10 real-world production examples demonstrating practical pattern applications.
---
Example 1: Singleton for Configuration Management
Pattern: Singleton
Scenario: Application needs centralized configuration accessible from anywhere, loaded once at startup.
Complete Code:
import json
from pathlib import Path
from threading import Lock
from typing import Any, Dict, Optional
class Config:
"""
Thread-safe singleton configuration manager.
Loads configuration from JSON file once and provides global access.
"""
_instance: Optional['Config'] = None
_lock: Lock = Lock()
_config: Dict[str, Any] = {}
_loaded: bool = False
def __new__(cls) -> 'Config':
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def load(self, config_path: Path) -> None:
"""Load configuration from file (idempotent)."""
if self._loaded:
return
with self._lock:
if not self._loaded:
with open(config_path, 'r') as f:
self._config = json.load(f)
self._loaded = True
def get(self, key: str, default: Any = None) -> Any:
"""Get configuration value."""
return self._config.get(key, default)
def __getitem__(self, key: str) -> Any:
"""Dictionary-style access."""
return self._config[key]
# Usage
config = Config()
config.load(Path('config.json'))
# Access from anywhere in application
api_key = config.get('api_key')
database_url = config['database_url']Why This Pattern Works:
- Configuration is read-only once loaded
- Truly global resource (single config file)
- Thread-safe initialization
- Performance benefit (load once, access many times)
Trade-offs:
- Global state (testing requires reset mechanism)
- Alternative: Dependency injection with config object
- Only justified because config is genuinely global and immutable after load
---
Example 2: Factory Method for Plugin System
Pattern: Factory Method
Scenario: Application with extensible plugin system where plugins are discovered at runtime.
Complete Code:
from abc import ABC, abstractmethod
from typing import Dict, Type
import importlib
import inspect
class Plugin(ABC):
"""Base plugin interface."""
@abstractmethod
def execute(self, data: dict) -> dict:
"""Execute plugin logic."""
pass
@abstractmethod
def get_name(self) -> str:
"""Return plugin name."""
pass
class DataTransformPlugin(Plugin):
"""Transforms data by converting keys to uppercase."""
def execute(self, data: dict) -> dict:
return {k.upper(): v for k, v in data.items()}
def get_name(self) -> str:
return "data_transform"
class DataValidationPlugin(Plugin):
"""Validates data has required keys."""
def __init__(self, required_keys: list):
self.required_keys = required_keys
def execute(self, data: dict) -> dict:
missing = [k for k in self.required_keys if k not in data]
if missing:
raise ValueError(f"Missing keys: {missing}")
return data
def get_name(self) -> str:
return "data_validation"
class PluginFactory:
"""Factory for discovering and creating plugins."""
def __init__(self):
self._plugins: Dict[str, Type[Plugin]] = {}
def register(self, plugin_class: Type[Plugin]) -> None:
"""Register a plugin class."""
# Create temporary instance to get name
temp = plugin_class() if not inspect.signature(plugin_class.__init__).parameters else None
if temp:
self._plugins[temp.get_name()] = plugin_class
def create(self, plugin_name: str, **kwargs) -> Plugin:
"""Create plugin instance by name."""
if plugin_name not in self._plugins:
raise ValueError(f"Unknown plugin: {plugin_name}")
return self._plugins[plugin_name](**kwargs)
def discover(self, module_name: str) -> None:
"""Auto-discover plugins from module."""
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Plugin) and obj is not Plugin:
self.register(obj)
# Usage
factory = PluginFactory()
factory.register(DataTransformPlugin)
factory.register(DataValidationPlugin)
# Create plugins dynamically
transform = factory.create("data_transform")
validator = factory.create("data_validation", required_keys=["id", "name"])
# Process data
data = {"id": 1, "name": "Alice"}
data = validator.execute(data)
data = transform.execute(data)
print(data) # {'ID': 1, 'NAME': 'Alice'}Why This Pattern Works:
- Multiple plugin types (≥2 concrete implementations)
- Plugins discovered/loaded dynamically at runtime
- Adding new plugins doesn't require changing factory code
- Clear separation: factory creates, plugins execute
Trade-offs:
- More complex than direct instantiation
- Only justified with multiple plugin types and dynamic loading requirements
---
Example 3: Observer for Event-Driven Architecture
Pattern: Observer
Scenario: Real-time stock ticker where multiple components (UI, logger, analyzer) react to price updates.
Complete Code:
from abc import ABC, abstractmethod
from typing import List, Dict
from datetime import datetime
class StockObserver(ABC):
"""Observer interface for stock price updates."""
@abstractmethod
def update(self, symbol: str, price: float, timestamp: datetime) -> None:
"""Called when stock price changes."""
pass
class StockTicker:
"""Subject that notifies observers of price changes."""
def __init__(self):
self._observers: List[StockObserver] = []
self._prices: Dict[str, float] = {}
def attach(self, observer: StockObserver) -> None:
"""Add observer."""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: StockObserver) -> None:
"""Remove observer."""
self._observers.remove(observer)
def set_price(self, symbol: str, price: float) -> None:
"""Update price and notify observers."""
old_price = self._prices.get(symbol)
if old_price != price:
self._prices[symbol] = price
self._notify(symbol, price)
def _notify(self, symbol: str, price: float) -> None:
"""Notify all observers."""
timestamp = datetime.now()
for observer in self._observers:
observer.update(symbol, price, timestamp)
class PriceLogger(StockObserver):
"""Logs all price changes."""
def update(self, symbol: str, price: float, timestamp: datetime) -> None:
print(f"[LOG] {timestamp}: {symbol} = ${price:.2f}")
class PriceAlertSystem(StockObserver):
"""Alerts when price exceeds threshold."""
def __init__(self, threshold: float):
self.threshold = threshold
def update(self, symbol: str, price: float, timestamp: datetime) -> None:
if price > self.threshold:
print(f"[ALERT] {symbol} exceeded ${self.threshold}: ${price:.2f}")
class PriceAnalyzer(StockObserver):
"""Calculates moving average."""
def __init__(self, window_size: int = 5):
self.prices: Dict[str, List[float]] = {}
self.window_size = window_size
def update(self, symbol: str, price: float, timestamp: datetime) -> None:
if symbol not in self.prices:
self.prices[symbol] = []
self.prices[symbol].append(price)
if len(self.prices[symbol]) > self.window_size:
self.prices[symbol].pop(0)
avg = sum(self.prices[symbol]) / len(self.prices[symbol])
print(f"[ANALYSIS] {symbol} MA({self.window_size}): ${avg:.2f}")
# Usage
ticker = StockTicker()
# Attach multiple observers
logger = PriceLogger()
alert = PriceAlertSystem(threshold=150.0)
analyzer = PriceAnalyzer(window_size=3)
ticker.attach(logger)
ticker.attach(alert)
ticker.attach(analyzer)
# Price updates automatically notify all observers
ticker.set_price("AAPL", 145.50)
ticker.set_price("AAPL", 147.20)
ticker.set_price("AAPL", 151.80) # Triggers alert
# Can detach observers dynamically
ticker.detach(alert)
ticker.set_price("AAPL", 155.00) # No alertWhy This Pattern Works:
- Multiple observers with different responsibilities (≥3)
- Dynamic observer set (attach/detach at runtime)
- One-to-many broadcast communication
- Observers are loosely coupled (don't know about each other)
Trade-offs:
- More complex than direct method calls
- Update order not guaranteed
- Need careful memory management (detach observers to prevent leaks)
---
Example 4: Strategy for Payment Processing
Pattern: Strategy
Scenario: E-commerce checkout supporting multiple payment methods (credit card, PayPal, cryptocurrency).
Complete Code:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
from decimal import Decimal
@dataclass
class PaymentResult:
"""Result of payment processing."""
success: bool
transaction_id: Optional[str] = None
error_message: Optional[str] = None
class PaymentStrategy(ABC):
"""Strategy interface for payment processing."""
@abstractmethod
def process_payment(self, amount: Decimal, currency: str = "USD") -> PaymentResult:
"""Process payment and return result."""
pass
@abstractmethod
def get_fee(self, amount: Decimal) -> Decimal:
"""Calculate processing fee."""
pass
class CreditCardPayment(PaymentStrategy):
"""Credit card payment processing."""
def __init__(self, card_number: str, cvv: str, expiry: str):
self.card_number = card_number
self.cvv = cvv
self.expiry = expiry
def process_payment(self, amount: Decimal, currency: str = "USD") -> PaymentResult:
# Simulate credit card processing
print(f"Processing ${amount} via credit card ****{self.card_number[-4:]}")
return PaymentResult(
success=True,
transaction_id=f"CC-{self.card_number[-4:]}-12345"
)
def get_fee(self, amount: Decimal) -> Decimal:
"""Credit cards charge 2.9% + $0.30."""
return amount * Decimal("0.029") + Decimal("0.30")
class PayPalPayment(PaymentStrategy):
"""PayPal payment processing."""
def __init__(self, email: str):
self.email = email
def process_payment(self, amount: Decimal, currency: str = "USD") -> PaymentResult:
print(f"Processing ${amount} via PayPal ({self.email})")
return PaymentResult(
success=True,
transaction_id=f"PP-{self.email.split('@')[0]}-67890"
)
def get_fee(self, amount: Decimal) -> Decimal:
"""PayPal charges 2.9% + $0.30."""
return amount * Decimal("0.029") + Decimal("0.30")
class CryptoPayment(PaymentStrategy):
"""Cryptocurrency payment processing."""
def __init__(self, wallet_address: str, crypto_type: str = "BTC"):
self.wallet_address = wallet_address
self.crypto_type = crypto_type
def process_payment(self, amount: Decimal, currency: str = "USD") -> PaymentResult:
print(f"Processing ${amount} via {self.crypto_type} to {self.wallet_address[:8]}...")
return PaymentResult(
success=True,
transaction_id=f"CRYPTO-{self.crypto_type}-ABCDEF"
)
def get_fee(self, amount: Decimal) -> Decimal:
"""Crypto charges flat $1.50 network fee."""
return Decimal("1.50")
class CheckoutProcessor:
"""Context that uses payment strategy."""
def __init__(self, payment_strategy: PaymentStrategy):
self._strategy = payment_strategy
def set_payment_method(self, payment_strategy: PaymentStrategy) -> None:
"""Change payment strategy at runtime."""
self._strategy = payment_strategy
def checkout(self, amount: Decimal) -> PaymentResult:
"""Process checkout with current payment strategy."""
fee = self._strategy.get_fee(amount)
total = amount + fee
print(f"Order amount: ${amount}")
print(f"Processing fee: ${fee}")
print(f"Total: ${total}")
return self._strategy.process_payment(total)
# Usage
order_amount = Decimal("99.99")
# Credit card payment
processor = CheckoutProcessor(
CreditCardPayment(
card_number="4532123456789012",
cvv="123",
expiry="12/25"
)
)
result = processor.checkout(order_amount)
print(f"Result: {result}\n")
# Switch to PayPal
processor.set_payment_method(PayPalPayment(email="user@example.com"))
result = processor.checkout(order_amount)
print(f"Result: {result}\n")
# Switch to cryptocurrency
processor.set_payment_method(
CryptoPayment(
wallet_address="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
crypto_type="BTC"
)
)
result = processor.checkout(order_amount)
print(f"Result: {result}")Why This Pattern Works:
- Multiple payment algorithms (≥3) with different implementations
- Need to switch payment methods at runtime
- Each strategy has different fee calculation logic
- Adding new payment methods doesn't change CheckoutProcessor
Trade-offs:
- More classes than simple if/else
- Only justified with ≥3 complex, swappable algorithms
---
Example 5: Command for Undo/Redo in Text Editor
Pattern: Command
Scenario: Text editor supporting undo/redo for multiple operations (insert, delete, formatting).
Complete Code:
from abc import ABC, abstractmethod
from typing import List
class Command(ABC):
"""Command interface with undo support."""
@abstractmethod
def execute(self) -> None:
"""Execute command."""
pass
@abstractmethod
def undo(self) -> None:
"""Undo command."""
pass
class TextDocument:
"""Receiver - the actual document."""
def __init__(self):
self.content = ""
def insert(self, position: int, text: str) -> None:
"""Insert text at position."""
self.content = self.content[:position] + text + self.content[position:]
def delete(self, position: int, length: int) -> None:
"""Delete text from position."""
self.content = self.content[:position] + self.content[position + length:]
def __str__(self) -> str:
return self.content
class InsertCommand(Command):
"""Command to insert text."""
def __init__(self, document: TextDocument, position: int, text: str):
self.document = document
self.position = position
self.text = text
def execute(self) -> None:
self.document.insert(self.position, self.text)
def undo(self) -> None:
self.document.delete(self.position, len(self.text))
class DeleteCommand(Command):
"""Command to delete text."""
def __init__(self, document: TextDocument, position: int, length: int):
self.document = document
self.position = position
self.length = length
self.deleted_text = "" # Save for undo
def execute(self) -> None:
# Save deleted text before deleting
self.deleted_text = self.document.content[self.position:self.position + self.length]
self.document.delete(self.position, self.length)
def undo(self) -> None:
self.document.insert(self.position, self.deleted_text)
class TextEditor:
"""Invoker with undo/redo support."""
def __init__(self, document: TextDocument):
self.document = document
self.history: List[Command] = []
self.redo_stack: List[Command] = []
def execute_command(self, command: Command) -> None:
"""Execute command and add to history."""
command.execute()
self.history.append(command)
self.redo_stack.clear() # Clear redo stack on new command
def undo(self) -> bool:
"""Undo last command."""
if not self.history:
return False
command = self.history.pop()
command.undo()
self.redo_stack.append(command)
return True
def redo(self) -> bool:
"""Redo last undone command."""
if not self.redo_stack:
return False
command = self.redo_stack.pop()
command.execute()
self.history.append(command)
return True
# Usage
doc = TextDocument()
editor = TextEditor(doc)
# Perform operations
editor.execute_command(InsertCommand(doc, 0, "Hello"))
print(f"After insert 'Hello': {doc}") # "Hello"
editor.execute_command(InsertCommand(doc, 5, " World"))
print(f"After insert ' World': {doc}") # "Hello World"
editor.execute_command(DeleteCommand(doc, 5, 6))
print(f"After delete ' World': {doc}") # "Hello"
# Undo operations
editor.undo()
print(f"After undo: {doc}") # "Hello World"
editor.undo()
print(f"After undo: {doc}") # "Hello"
# Redo operations
editor.redo()
print(f"After redo: {doc}") # "Hello World"
editor.redo()
print(f"After redo: {doc}") # "Hello"Why This Pattern Works:
- Need undo/redo functionality
- Multiple command types with different undo logic
- Commands encapsulate all information needed for undo
- History management separate from command execution
Trade-offs:
- More complex than simple callbacks
- Memory overhead (storing command history)
- Only justified when undo/redo or command queuing needed
---
Example 6: Adapter for Third-Party API Integration
Pattern: Adapter
Scenario: Application using different weather APIs (OpenWeather, WeatherAPI) with different interfaces.
Complete Code:
from abc import ABC, abstractmethod
from typing import Dict
from dataclasses import dataclass
@dataclass
class WeatherData:
"""Unified weather data structure."""
temperature_celsius: float
humidity_percent: float
description: str
wind_speed_kmh: float
class WeatherService(ABC):
"""Target interface our application expects."""
@abstractmethod
def get_weather(self, city: str) -> WeatherData:
"""Get weather for city."""
pass
# Third-party services (Adaptees) with different interfaces
class OpenWeatherAPI:
"""OpenWeather API with its own response format."""
def fetch_current_weather(self, location: str) -> Dict:
"""Returns weather in OpenWeather format."""
# Simulated API response
return {
"main": {
"temp": 293.15, # Kelvin
"humidity": 65
},
"weather": [{"description": "partly cloudy"}],
"wind": {"speed": 5.5} # m/s
}
class WeatherAPIService:
"""WeatherAPI.com with different response format."""
def get_current_conditions(self, city_name: str) -> Dict:
"""Returns weather in WeatherAPI format."""
# Simulated API response
return {
"temp_c": 20.0,
"humidity": 65,
"condition": {"text": "Partly cloudy"},
"wind_kph": 19.8
}
# Adapters
class OpenWeatherAdapter(WeatherService):
"""Adapter for OpenWeather API."""
def __init__(self, api: OpenWeatherAPI):
self.api = api
def get_weather(self, city: str) -> WeatherData:
"""Convert OpenWeather response to WeatherData."""
response = self.api.fetch_current_weather(city)
# Convert Kelvin to Celsius
temp_celsius = response["main"]["temp"] - 273.15
# Convert m/s to km/h
wind_kmh = response["wind"]["speed"] * 3.6
return WeatherData(
temperature_celsius=round(temp_celsius, 1),
humidity_percent=response["main"]["humidity"],
description=response["weather"][0]["description"],
wind_speed_kmh=round(wind_kmh, 1)
)
class WeatherAPIAdapter(WeatherService):
"""Adapter for WeatherAPI.com."""
def __init__(self, api: WeatherAPIService):
self.api = api
def get_weather(self, city: str) -> WeatherData:
"""Convert WeatherAPI response to WeatherData."""
response = self.api.get_current_conditions(city)
return WeatherData(
temperature_celsius=response["temp_c"],
humidity_percent=response["humidity"],
description=response["condition"]["text"],
wind_speed_kmh=response["wind_kph"]
)
class WeatherApp:
"""Application using unified WeatherService interface."""
def __init__(self, weather_service: WeatherService):
self.weather_service = weather_service
def display_weather(self, city: str) -> None:
"""Display weather using unified interface."""
weather = self.weather_service.get_weather(city)
print(f"\nWeather in {city}:")
print(f" Temperature: {weather.temperature_celsius}°C")
print(f" Humidity: {weather.humidity_percent}%")
print(f" Conditions: {weather.description}")
print(f" Wind Speed: {weather.wind_speed_kmh} km/h")
# Usage
# Use OpenWeather API
openweather = OpenWeatherAPI()
adapter1 = OpenWeatherAdapter(openweather)
app = WeatherApp(adapter1)
app.display_weather("London")
# Switch to WeatherAPI.com
weatherapi = WeatherAPIService()
adapter2 = WeatherAPIAdapter(weatherapi)
app = WeatherApp(adapter2)
app.display_weather("Paris")Why This Pattern Works:
- Third-party APIs we can't modify
- Different response formats need conversion
- Application code uses unified interface
- Easy to add new weather services
Trade-offs:
- Additional layer of indirection
- Only justified for external/unchangeable interfaces
---
Example 7: Facade for Complex Subsystem (Video Encoding)
Pattern: Facade
Scenario: Video processing application with complex subsystems (codec, audio, metadata) providing simple interface.
Complete Code:
from pathlib import Path
from typing import Optional
# Complex subsystem classes
class VideoCodec:
"""Handles video encoding/decoding."""
def load_video(self, path: Path) -> bytes:
print(f"[VideoCodec] Loading video from {path}")
return b"video_data"
def encode(self, data: bytes, codec: str, bitrate: int) -> bytes:
print(f"[VideoCodec] Encoding video with {codec} at {bitrate}kbps")
return b"encoded_video"
class AudioProcessor:
"""Handles audio processing."""
def extract_audio(self, video_data: bytes) -> bytes:
print("[AudioProcessor] Extracting audio track")
return b"audio_data"
def process_audio(self, audio_data: bytes, normalize: bool) -> bytes:
if normalize:
print("[AudioProcessor] Normalizing audio levels")
return b"processed_audio"
def merge_audio(self, video: bytes, audio: bytes) -> bytes:
print("[AudioProcessor] Merging audio with video")
return b"video_with_audio"
class MetadataEditor:
"""Handles video metadata."""
def set_title(self, video: bytes, title: str) -> bytes:
print(f"[MetadataEditor] Setting title: {title}")
return video
def set_description(self, video: bytes, description: str) -> bytes:
print(f"[MetadataEditor] Setting description: {description}")
return video
def add_tags(self, video: bytes, tags: list) -> bytes:
print(f"[MetadataEditor] Adding tags: {tags}")
return video
class FileWriter:
"""Handles file output."""
def save(self, data: bytes, path: Path, format: str) -> None:
print(f"[FileWriter] Saving to {path} in {format} format")
# Facade
class VideoConverter:
"""
Facade providing simple interface to complex video processing subsystem.
Simplifies common video conversion workflow.
"""
def __init__(self):
self.codec = VideoCodec()
self.audio = AudioProcessor()
self.metadata = MetadataEditor()
self.writer = FileWriter()
def convert_video(
self,
input_path: Path,
output_path: Path,
target_codec: str = "h264",
bitrate: int = 5000,
normalize_audio: bool = True,
title: Optional[str] = None,
description: Optional[str] = None,
tags: Optional[list] = None
) -> None:
"""
Convert video with one simple method call.
Handles all subsystem coordination automatically.
"""
print(f"\n=== Converting {input_path} ===")
# Load video
video_data = self.codec.load_video(input_path)
# Process audio
audio_data = self.audio.extract_audio(video_data)
audio_data = self.audio.process_audio(audio_data, normalize_audio)
# Encode video
encoded = self.codec.encode(video_data, target_codec, bitrate)
# Merge audio back
final_video = self.audio.merge_audio(encoded, audio_data)
# Add metadata if provided
if title:
final_video = self.metadata.set_title(final_video, title)
if description:
final_video = self.metadata.set_description(final_video, description)
if tags:
final_video = self.metadata.add_tags(final_video, tags)
# Save output
output_format = output_path.suffix[1:] # Remove leading dot
self.writer.save(final_video, output_path, output_format)
print(f"=== Conversion complete ===\n")
# Usage
# Without Facade - Complex (client must coordinate subsystems)
def convert_without_facade(input_path: Path, output_path: Path):
codec = VideoCodec()
audio = AudioProcessor()
metadata = MetadataEditor()
writer = FileWriter()
video_data = codec.load_video(input_path)
audio_data = audio.extract_audio(video_data)
audio_data = audio.process_audio(audio_data, True)
encoded = codec.encode(video_data, "h264", 5000)
final = audio.merge_audio(encoded, audio_data)
final = metadata.set_title(final, "My Video")
writer.save(final, output_path, "mp4")
# With Facade - Simple (one line)
converter = VideoConverter()
converter.convert_video(
input_path=Path("input.avi"),
output_path=Path("output.mp4"),
title="My Awesome Video",
description="Converted with VideoConverter",
tags=["tutorial", "python", "patterns"]
)Why This Pattern Works:
- Complex subsystem with many interdependent classes (4+)
- Common workflow repeated frequently
- Clients don't need full subsystem control
- Simplifies client code dramatically
Trade-offs:
- May not expose all subsystem features
- Can become god object if overloaded
- Only justified when complexity exists (≥3 subsystem classes)
---
Example 8: Decorator for Adding Features to Network Connections
Pattern: Decorator
Scenario: Network connection with optional features (encryption, compression, logging) that can be combined.
Complete Code:
from abc import ABC, abstractmethod
import zlib
import base64
class Connection(ABC):
"""Component interface for network connections."""
@abstractmethod
def send(self, data: str) -> None:
"""Send data through connection."""
pass
@abstractmethod
def receive(self) -> str:
"""Receive data from connection."""
pass
class TCPConnection(Connection):
"""Concrete component - basic TCP connection."""
def __init__(self, host: str, port: int):
self.host = host
self.port = port
self.buffer = ""
def send(self, data: str) -> None:
"""Send raw data."""
print(f"[TCP] Sending to {self.host}:{self.port}: {data}")
# Simulate sending - just store in buffer
self.buffer = data
def receive(self) -> str:
"""Receive raw data."""
data = self.buffer
print(f"[TCP] Receiving from {self.host}:{self.port}: {data}")
return data
class ConnectionDecorator(Connection):
"""Base decorator."""
def __init__(self, connection: Connection):
self._connection = connection
def send(self, data: str) -> None:
self._connection.send(data)
def receive(self) -> str:
return self._connection.receive()
class EncryptedConnection(ConnectionDecorator):
"""Decorator adding encryption."""
def send(self, data: str) -> None:
encrypted = base64.b64encode(data.encode()).decode()
print(f"[Encryption] Encrypting: {data} -> {encrypted}")
self._connection.send(encrypted)
def receive(self) -> str:
encrypted = self._connection.receive()
data = base64.b64decode(encrypted.encode()).decode()
print(f"[Encryption] Decrypting: {encrypted} -> {data}")
return data
class CompressedConnection(ConnectionDecorator):
"""Decorator adding compression."""
def send(self, data: str) -> None:
compressed = zlib.compress(data.encode())
encoded = base64.b64encode(compressed).decode()
print(f"[Compression] Compressing: {len(data)} -> {len(compressed)} bytes")
self._connection.send(encoded)
def receive(self) -> str:
encoded = self._connection.receive()
compressed = base64.b64decode(encoded.encode())
data = zlib.decompress(compressed).decode()
print(f"[Compression] Decompressing: {len(compressed)} -> {len(data)} bytes")
return data
class LoggedConnection(ConnectionDecorator):
"""Decorator adding logging."""
def send(self, data: str) -> None:
print(f"[Logger] Logging send: {len(data)} bytes")
self._connection.send(data)
def receive(self) -> str:
data = self._connection.receive()
print(f"[Logger] Logging receive: {len(data)} bytes")
return data
# Usage
# Basic connection
basic = TCPConnection("example.com", 8080)
basic.send("Hello World")
basic.receive()
print("\n" + "="*50 + "\n")
# Add encryption
encrypted = EncryptedConnection(TCPConnection("example.com", 8080))
encrypted.send("Secret Message")
encrypted.receive()
print("\n" + "="*50 + "\n")
# Combine multiple decorators: logging + encryption + compression
connection = LoggedConnection(
EncryptedConnection(
CompressedConnection(
TCPConnection("example.com", 8080)
)
)
)
connection.send("This is a long message that will be compressed, encrypted, and logged!")
connection.receive()Why This Pattern Works:
- Multiple optional features that can be combined (3+ decorators)
- Need to add/remove features dynamically at runtime
- Features are independent and composable
- Avoids explosion of subclasses (without decorator: 2^3 = 8 classes)
Trade-offs:
- Can result in many small objects
- Order of decoration matters
- Only justified with ≥3 combinable features
---
Example 9: Composite for File System Hierarchy
Pattern: Composite
Scenario: File system where files and directories are treated uniformly (directories contain files/directories).
Complete Code:
from abc import ABC, abstractmethod
from typing import List
class FileSystemItem(ABC):
"""Component interface for files and directories."""
def __init__(self, name: str):
self.name = name
@abstractmethod
def get_size(self) -> int:
"""Get size in bytes."""
pass
@abstractmethod
def display(self, indent: int = 0) -> None:
"""Display item with indentation."""
pass
class File(FileSystemItem):
"""Leaf - represents a file."""
def __init__(self, name: str, size: int):
super().__init__(name)
self.size = size
def get_size(self) -> int:
return self.size
def display(self, indent: int = 0) -> None:
print(" " * indent + f"📄 {self.name} ({self.size} bytes)")
class Directory(FileSystemItem):
"""Composite - represents a directory containing files/directories."""
def __init__(self, name: str):
super().__init__(name)
self.children: List[FileSystemItem] = []
def add(self, item: FileSystemItem) -> None:
"""Add file or directory."""
self.children.append(item)
def remove(self, item: FileSystemItem) -> None:
"""Remove file or directory."""
self.children.remove(item)
def get_size(self) -> int:
"""Calculate total size of all children."""
return sum(child.get_size() for child in self.children)
def display(self, indent: int = 0) -> None:
"""Display directory tree."""
print(" " * indent + f"📁 {self.name}/ ({self.get_size()} bytes total)")
for child in self.children:
child.display(indent + 1)
# Usage
# Build file system structure
root = Directory("root")
# Documents folder
docs = Directory("documents")
docs.add(File("report.pdf", 2048))
docs.add(File("presentation.pptx", 4096))
# Projects folder
projects = Directory("projects")
# Python project
python_proj = Directory("python_app")
python_proj.add(File("main.py", 512))
python_proj.add(File("utils.py", 256))
python_proj.add(File("README.md", 128))
projects.add(python_proj)
# Web project
web_proj = Directory("website")
web_proj.add(File("index.html", 1024))
web_proj.add(File("style.css", 512))
web_proj.add(File("script.js", 768))
projects.add(web_proj)
# Build root structure
root.add(docs)
root.add(projects)
root.add(File("notes.txt", 256))
# Display entire tree
root.display()
# Get total size (works uniformly for files and directories)
print(f"\nTotal size: {root.get_size()} bytes")
print(f"Documents size: {docs.get_size()} bytes")
print(f"Python project size: {python_proj.get_size()} bytes")Why This Pattern Works:
- Truly hierarchical/recursive structure (trees)
- Need to treat leaves and composites uniformly
- Operations work same way on both (get_size, display)
- Natural model for part-whole hierarchies
Trade-offs:
- Can make design overly general
- Type checking complications (all items treated uniformly)
- Only justified for genuine tree structures
---
Example 10: Template Method for Data Processing Pipeline
Pattern: Template Method
Scenario: Data processing pipeline with fixed steps but algorithm-specific implementation details.
Complete Code:
from abc import ABC, abstractmethod
from typing import List, Dict, Any
import json
import csv
class DataProcessor(ABC):
"""
Abstract class defining template method for data processing pipeline.
Defines algorithm skeleton, subclasses implement specific steps.
"""
def process(self, input_path: str, output_path: str) -> None:
"""
Template method defining processing pipeline.
Steps executed in fixed order.
"""
print(f"\n=== Processing {input_path} ===")
# Step 1: Load data
raw_data = self.load_data(input_path)
print(f"Loaded {len(raw_data)} records")
# Step 2: Validate (optional hook)
if not self.validate_data(raw_data):
raise ValueError("Data validation failed")
# Step 3: Transform (required)
transformed = self.transform_data(raw_data)
print(f"Transformed to {len(transformed)} records")
# Step 4: Filter (optional hook)
filtered = self.filter_data(transformed)
print(f"Filtered to {len(filtered)} records")
# Step 5: Save
self.save_data(filtered, output_path)
print(f"Saved to {output_path}")
# Step 6: Cleanup hook
self.cleanup()
print("=== Processing complete ===\n")
# Abstract methods (must be implemented)
@abstractmethod
def load_data(self, path: str) -> List[Dict[str, Any]]:
"""Load data from source (implementation required)."""
pass
@abstractmethod
def transform_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Transform data (implementation required)."""
pass
@abstractmethod
def save_data(self, data: List[Dict[str, Any]], path: str) -> None:
"""Save data to destination (implementation required)."""
pass
# Hook methods (optional overrides)
def validate_data(self, data: List[Dict[str, Any]]) -> bool:
"""
Optional validation hook.
Default: always valid. Override for custom validation.
"""
return True
def filter_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Optional filtering hook.
Default: no filtering. Override for custom filtering.
"""
return data
def cleanup(self) -> None:
"""
Optional cleanup hook.
Default: no cleanup. Override for cleanup logic.
"""
pass
class CSVToJSONProcessor(DataProcessor):
"""Concrete processor: CSV to JSON with uppercase transformation."""
def load_data(self, path: str) -> List[Dict[str, Any]]:
"""Load from CSV file."""
# Simulated CSV data
return [
{"name": "Alice", "age": "30", "city": "NYC"},
{"name": "Bob", "age": "25", "city": "LA"},
{"name": "Charlie", "age": "35", "city": "NYC"}
]
def transform_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Convert all string values to uppercase."""
return [
{k: v.upper() if isinstance(v, str) else v for k, v in record.items()}
for record in data
]
def filter_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Filter: only include NYC residents."""
return [record for record in data if record.get("city") == "NYC"]
def save_data(self, data: List[Dict[str, Any]], path: str) -> None:
"""Save as JSON."""
json_output = json.dumps(data, indent=2)
print(f"JSON output:\n{json_output}")
class JSONToCSVProcessor(DataProcessor):
"""Concrete processor: JSON to CSV with age validation."""
def load_data(self, path: str) -> List[Dict[str, Any]]:
"""Load from JSON file."""
# Simulated JSON data
return [
{"name": "David", "age": 28, "salary": 50000},
{"name": "Eve", "age": -5, "salary": 60000}, # Invalid age
{"name": "Frank", "age": 32, "salary": 75000}
]
def validate_data(self, data: List[Dict[str, Any]]) -> bool:
"""Validate all ages are positive."""
for record in data:
if record.get("age", 0) < 0:
print(f"⚠️ Invalid age detected: {record}")
return False
return True
def transform_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Add 'senior' flag for age >= 30."""
return [
{**record, "senior": record.get("age", 0) >= 30}
for record in data
]
def save_data(self, data: List[Dict[str, Any]], path: str) -> None:
"""Save as CSV."""
if data:
fieldnames = data[0].keys()
print(f"CSV output: {fieldnames}")
for record in data:
print(f" {record}")
# Usage
# CSV to JSON processing
csv_processor = CSVToJSONProcessor()
csv_processor.process("input.csv", "output.json")
# JSON to CSV processing (will fail validation)
json_processor = JSONToCSVProcessor()
try:
json_processor.process("input.json", "output.csv")
except ValueError as e:
print(f"❌ Processing failed: {e}")
# Fix data and try again
class FixedJSONProcessor(JSONToCSVProcessor):
def load_data(self, path: str) -> List[Dict[str, Any]]:
data = super().load_data(path)
# Fix invalid ages
for record in data:
if record.get("age", 0) < 0:
record["age"] = 0
return data
fixed_processor = FixedJSONProcessor()
fixed_processor.process("input.json", "output.csv")Why This Pattern Works:
- Common algorithm structure with variant steps (5+ steps)
- Fixed execution order (template method controls flow)
- Multiple concrete processors with different implementations
- Mix of required methods and optional hooks
Trade-offs:
- Inverted control (subclasses don't call base, base calls subclasses)
- Less flexible than Strategy (can't swap algorithm at runtime)
- Only justified when algorithm has invariant parts and variant parts
---
Summary
These 10 examples demonstrate:
1. When patterns ARE justified: Multiple variants (≥2-3), complex interactions, need for flexibility 2. Production-ready code: Complete implementations with error handling 3. Philosophy-aligned: Each example includes "Why This Works" and "Trade-offs" sections 4. Real-world scenarios: Configuration, plugins, events, payments, undo/redo, API integration, subsystems, features, hierarchies, pipelines
All examples follow amplihack's ruthless simplicity: patterns used only when complexity justifies them, with clear alternatives noted.
Example 1: Quick Pattern Lookup
Scenario
User wants a fast overview of a design pattern to understand its basic concept.
User Query
User: "What is the Observer pattern?"Skill Response
Observer (Behavioral)
Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
When to Use:
- An abstraction has two aspects, one dependent on the other
- Change to one object requires changing others (number unknown)
- Object should notify other objects without knowing who they are
Quick Example:
class Subject:
def __init__(self):
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def notify(self):
for observer in self.observers:
observer.update()Complexity Warning: If you only have 1-2 observers or relationships are static, callbacks/direct calls are simpler. Observer excels with dynamic sets of dependents.
Related Patterns: Mediator (complex interactions), Singleton (subject often singleton)
Deeper: Request "Practical guide for Observer" (Tier 2) or "Deep dive into Observer" (Tier 3)
Example 2: Implementation Guide
Scenario
User needs practical guidance on how to implement a specific pattern with working code.
User Query
User: "How to implement the Strategy pattern in Python?"Skill Response
Strategy Pattern - Practical Guide
Structure
Context
- strategy: Strategy
+ set_strategy(Strategy)
+ execute_strategy()
Strategy (interface)
+ execute(data)
ConcreteStrategyA : Strategy
+ execute(data)
ConcreteStrategyB : Strategy
+ execute(data)Implementation Steps
1. Define Strategy interface:
class Strategy:
def execute(self, data):
pass2. Create Concrete Strategies:
class ConcreteStrategyA(Strategy):
def execute(self, data):
# Algorithm A implementation
return sorted(data)
class ConcreteStrategyB(Strategy):
def execute(self, data):
# Algorithm B implementation
return reversed(data)3. Create Context that uses Strategy:
class Context:
def __init__(self, strategy: Strategy):
self.strategy = strategy
def set_strategy(self, strategy: Strategy):
self.strategy = strategy
def do_work(self, data):
return self.strategy.execute(data)Code Example: Python
from abc import ABC, abstractmethod
# Strategy interface
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list:
pass
# Concrete strategies
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class BubbleSort(SortStrategy):
def sort(self, data: list) -> list:
data = data.copy()
n = len(data)
for i in range(n):
for j in range(0, n - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
# Context
class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self.strategy = strategy
def sort_data(self, data: list) -> list:
print(f"Using {self.strategy.__class__.__name__}")
return self.strategy.sort(data)
# Usage
if __name__ == "__main__":
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# Use QuickSort strategy
sorter = Sorter(QuickSort())
print(sorter.sort_data(data)) # [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
# Switch to BubbleSort strategy at runtime
sorter.set_strategy(BubbleSort())
print(sorter.sort_data(data)) # [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]Real-World Use Cases
- Payment Processing: Different payment methods (CreditCard, PayPal, Crypto)
- Data Validation: Different validation rules (Email, Phone, SSN)
- Compression: Different algorithms (ZIP, RAR, GZIP)
- Navigation: Different route-finding algorithms (shortest, fastest, scenic)
Common Pitfalls
- Too few strategies: If you have only 1-2 simple algorithms, Strategy over-engineers. Use function parameters instead.
- ❌ Bad: Creating Strategy classes for trivial logic
- ✅ Good: Use simple function parameters or lambda functions
- Confusing with State: Strategy changes algorithm; State changes behavior based on internal state.
- Over-abstraction: Don't create strategy hierarchies unless necessary. Keep it simple.
When NOT to Use
- Simple algorithms: If algorithms are 1-2 lines, use function parameters
- Fixed algorithms: If algorithm never changes at runtime, Strategy over-engineers
- Prototype context: If requirements are unstable, Strategy adds premature structure
Trade-offs
| Benefit | Cost |
|---|---|
| Easy to add new algorithms (Open/Closed) | Extra classes to maintain |
| Swap algorithms at runtime | More objects in memory |
| Isolate algorithm implementation details | Indirection makes code less obvious |
| Eliminate conditional statements | Clients must understand different strategies |
Philosophy Check: ✅ Good fit when you have ≥3 complex algorithms that vary at runtime. ⚠️ Over-engineering if used for simple function logic.
Go Deeper: Request "Deep dive into Strategy" for TypeScript/Java implementations, variations, and advanced topics.
Example 3: Pattern Recognition
Scenario
User describes a problem and needs help identifying which design pattern(s) would be appropriate.
User Query
User: "I have a system where multiple components need to be notified when a user's profile is updated. The number of components varies - sometimes it's just the UI, sometimes it includes logging, analytics, and email notifications. What pattern should I use?"Skill Response
This is a classic use case for the Observer pattern.
Why Observer Fits Your Problem
Pattern Match:
- ✅ One source of change (user profile updates)
- ✅ Multiple objects need notification (UI, logging, analytics, email)
- ✅ Number of dependents varies dynamically
- ✅ Subject shouldn't know about specific dependents
Intent: Define a one-to-many dependency so when one object changes state, all dependents are notified automatically.
Quick Example for Your Use Case
class UserProfile: # Subject
def __init__(self):
self.observers = []
self.name = ""
self.email = ""
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self):
for observer in self.observers:
observer.update(self)
def update_profile(self, name, email):
self.name = name
self.email = email
self.notify() # Notify all observers
# Observers
class UIObserver:
def update(self, profile):
print(f"UI updated: {profile.name}")
class LoggingObserver:
def update(self, profile):
print(f"Logged profile change: {profile.email}")
class AnalyticsObserver:
def update(self, profile):
print(f"Analytics: profile updated")
# Usage
profile = UserProfile()
profile.attach(UIObserver())
profile.attach(LoggingObserver())
profile.attach(AnalyticsObserver())
profile.update_profile("John Doe", "john@example.com")
# All three observers get notified automaticallyPhilosophy Check
Good fit for your use case because:
- ✅ You have multiple (≥2) observers NOW (not "might have later")
- ✅ Observer set is dynamic (varies at runtime)
- ✅ Subject doesn't need to know about specific observers
- ✅ Clean separation of concerns
NOT over-engineering because:
- You explicitly said number of components varies
- You have diverse observer types (UI, logging, analytics, email)
- This is a recurring notification need, not one-off
Alternatives Considered
Direct Method Calls:
profile.update_profile()
ui.refresh()
logging.log_change()
analytics.track()
email.send_notification()- Pros: Simple, explicit
- Cons: Tight coupling, can't add observers dynamically, violates Open/Closed
- When to use: If you have 1-2 fixed observers that never change
Event Bus / Message Queue:
- Pros: Complete decoupling, scalable
- Cons: More infrastructure, harder to debug
- When to use: Distributed systems, microservices
Recommendation
Use Observer pattern for your use case.
Next Steps:
1. Define UserProfile as Subject 2. Create observer interface (update method) 3. Implement concrete observers (UI, Logging, Analytics, Email) 4. Attach observers to profile 5. Call notify() after profile updates
Would you like:
- Practical guide with complete implementation?
- Deep dive into Observer with push vs pull variants?
- Comparison with other notification patterns?
Example 4: Pattern Comparison
Scenario
User is deciding between multiple design patterns and needs a structured comparison.
User Query
User: "I need to create different types of database connections (MySQL, PostgreSQL, MongoDB). Should I use Factory Method or Abstract Factory?"Skill Response
Pattern Comparison for Database Connections
You're trying to: Create different types of database connections based on database type
Relevant Patterns: Factory Method, Abstract Factory, Simple Factory Function
---
Option 1: Factory Method
Best when: Each database type requires a different connection creation process, and you want subclasses to specify which connection to create.
Pros:
- Clean separation of connection creation logic per database type
- Easy to add new database types (Open/Closed Principle)
- Each creator class can have database-specific initialization logic
Cons:
- Requires creating a class for each database type
- More verbose than simple approaches
- Inheritance-based (less flexible than composition)
Complexity: Medium
Code overhead:
- 1 Creator base class (abstract)
- 1 ConcreteCreator per database type (3 classes)
- 1 Product interface
- 1 ConcreteProduct per database type (3 classes)
- Total: ~8 classes
Example:
class ConnectionCreator(ABC):
@abstractmethod
def create_connection(self) -> Connection:
pass
def connect_and_query(self, query):
conn = self.create_connection()
return conn.execute(query)
class MySQLCreator(ConnectionCreator):
def create_connection(self) -> Connection:
return MySQLConnection()
class PostgreSQLCreator(ConnectionCreator):
def create_connection(self) -> Connection:
return PostgreSQLConnection()---
Option 2: Abstract Factory
Best when: You need to create families of related objects (e.g., Connection + QueryBuilder + Transaction for each database).
Pros:
- Ensures related objects are used together
- Good for products with multiple related components
- Complete abstraction of database-specific implementation
Cons:
- OVERKILL if you only need connections (not families)
- High complexity (more classes than Factory Method)
- Harder to add new product types (need to update all factories)
Complexity: High
Code overhead:
- 1 AbstractFactory interface
- 1 ConcreteFactory per database type (3 classes)
- 1 interface per product type (Connection, QueryBuilder, Transaction = 3 interfaces)
- 1 concrete product per database per type (3 databases × 3 products = 9 classes)
- Total: ~16 classes
Example:
class DatabaseFactory(ABC):
@abstractmethod
def create_connection(self) -> Connection:
pass
@abstractmethod
def create_query_builder(self) -> QueryBuilder:
pass
@abstractmethod
def create_transaction(self) -> Transaction:
pass
class MySQLFactory(DatabaseFactory):
def create_connection(self):
return MySQLConnection()
def create_query_builder(self):
return MySQLQueryBuilder()
def create_transaction(self):
return MySQLTransaction()---
Option 3: Simple Factory Function (Recommended)
Best when: You just need to create connections based on type, without needing subclassing flexibility.
Pros:
- Simplest solution (YAGNI principle)
- Easy to understand and maintain
- No inheritance hierarchy
- One place to add new database types
Cons:
- Less extensible (adding new types requires modifying function)
- No subclass customization points
- Violates Open/Closed if you add types frequently
Complexity: Low
Code overhead:
- 1 factory function
- 1 ConcreteProduct per database type (3 classes)
- Total: ~4 classes
Example:
def create_connection(db_type: str, config: dict) -> Connection:
"""Simple factory function for database connections."""
if db_type == "mysql":
return MySQLConnection(config)
elif db_type == "postgresql":
return PostgreSQLConnection(config)
elif db_type == "mongodb":
return MongoDBConnection(config)
else:
raise ValueError(f"Unknown database type: {db_type}")
# Usage
conn = create_connection("mysql", {"host": "localhost", "port": 3306})---
Philosophy Check: Do You Need a Pattern?
Questions to Ask:
1. Do you need families of related objects?
- If YES (Connection + QueryBuilder + Transaction) → Consider Abstract Factory
- If NO (just Connection) → Don't use Abstract Factory
2. Do you need subclass customization of creation logic?
- If YES → Consider Factory Method
- If NO → Use Simple Factory Function
3. How often will you add new database types?
- Rarely (1-2 times per year) → Simple Factory Function
- Frequently (plugin system) → Factory Method
4. Is this for a library/framework or application code?
- Library/framework → Factory Method (extensibility matters)
- Application code → Simple Factory Function (simplicity matters)
Simpler Alternative
For your specific use case (creating database connections):
class ConnectionFactory:
"""Simple factory class with registry pattern."""
_creators = {}
@classmethod
def register(cls, db_type: str, creator: Callable):
cls._creators[db_type] = creator
@classmethod
def create(cls, db_type: str, config: dict) -> Connection:
creator = cls._creators.get(db_type)
if not creator:
raise ValueError(f"Unknown database type: {db_type}")
return creator(config)
# Register creators
ConnectionFactory.register("mysql", MySQLConnection)
ConnectionFactory.register("postgresql", PostgreSQLConnection)
ConnectionFactory.register("mongodb", MongoDBConnection)
# Usage
conn = ConnectionFactory.create("mysql", config)Benefits:
- Simple (1 class + registration)
- Extensible (register new types without modifying code)
- No inheritance needed
- Open/Closed compliant
---
Recommendation
Based on your description "create different types of database connections":
Use Simple Factory Function or Registry Pattern
Reasoning:
1. You only mentioned creating connections (not families of related objects) 2. You have 3 concrete types (manageable without pattern complexity) 3. Connection creation is likely straightforward (no complex logic per type) 4. Application code benefits from simplicity over extensibility
DON'T use Abstract Factory
- Why: You don't need families of related objects
- Complexity: 16 classes vs 4 classes
- YAGNI: You aren't gonna need the extra abstraction
Consider Factory Method IF:
- You're building a database abstraction library (not application)
- Each database needs significantly different connection logic
- You expect users to add custom database types via subclassing
Upgrade Path
Start simple:
1. Begin with factory function or registry pattern 2. Measure complexity and extensibility needs 3. Refactor to Factory Method IF you encounter pain points
Don't prematurely use Factory Method "for future flexibility". Refactor to it when you actually need it.
---
Next Steps
1. Implement Simple Factory Function or Registry Pattern 2. Test with your 3 database types 3. Monitor for pain points:
- Hard to add new types? → Consider Factory Method
- Need related object families? → Consider Abstract Factory
- Neither? → Keep it simple!
Would you like:
- Code example of Registry Pattern for your use case?
- Implementation guide for Factory Method?
- Deep dive into when Abstract Factory is actually needed?
Example 5: Philosophy Check (Over-Engineering Detection)
Scenario
User is considering using a design pattern, and the skill needs to warn against potential over-engineering.
User Query
User: "I'm building a small web app and thinking of using Singleton pattern for my database connection. Good idea?"Skill Response
⚠️ WARNING: Singleton Is Often a Code Smell
For database connections, Singleton is almost always the wrong choice. Let me explain why and suggest better alternatives.
---
Why Singleton Is Problematic Here
1. Global State Issues
# With Singleton - implicit global state
db = DatabaseConnection.get_instance() # Where does this come from?
db.query(...) # Hidden dependency!- Hidden dependencies make code hard to understand
- Testing becomes difficult (can't easily mock)
- Violates Dependency Inversion Principle
2. Testing Nightmares
def test_user_service():
# How do you test this without hitting real database?
service = UserService()
service.create_user(...) # Uses singleton DB - can't mock!- Can't inject test double
- Tests hit real database (slow, fragile)
- Tests interfere with each other (shared state)
3. Concurrency Problems
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance # NOT thread-safe!- Requires thread synchronization (complex)
- Performance bottleneck (single connection)
- Can't scale with connection pooling
4. Violates Single Responsibility
- Class manages both business logic AND instance lifecycle
- Harder to reason about and maintain
---
Better Alternatives
Option 1: Dependency Injection (Recommended)
Code:
class UserService:
def __init__(self, db_connection):
self.db = db_connection # Explicit dependency!
def create_user(self, name):
self.db.query("INSERT INTO users ...")
# Application setup
db = DatabaseConnection(config)
user_service = UserService(db)Benefits:
- ✅ Explicit dependencies (easy to understand)
- ✅ Easy to test (inject mock)
- ✅ Flexible (can inject different connections)
- ✅ No global state
Testing:
def test_user_service():
mock_db = MockDatabaseConnection()
service = UserService(mock_db)
service.create_user("John")
assert mock_db.query_called---
Option 2: Connection Pool (For Production)
Code:
from contextlib import contextmanager
class ConnectionPool:
def __init__(self, config, max_connections=10):
self.pool = [DatabaseConnection(config)
for _ in range(max_connections)]
@contextmanager
def get_connection(self):
conn = self.pool.pop()
try:
yield conn
finally:
self.pool.append(conn) # Return to pool
# Usage
pool = ConnectionPool(config)
def create_user(pool, name):
with pool.get_connection() as db:
db.query("INSERT INTO users ...")Benefits:
- ✅ Multiple connections (better concurrency)
- ✅ Resource management (connection reuse)
- ✅ Scalable (adjust pool size)
- ✅ Still testable (inject pool)
---
Option 3: Simple Module-Level Variable (For Small Apps)
Code:
# db.py
_connection = None
def get_connection(config=None):
global _connection
if _connection is None:
_connection = DatabaseConnection(config)
return _connection
# Usage in application
from db import get_connection
db = get_connection(config)
db.query(...)Benefits:
- ✅ Simple (no classes needed)
- ✅ Easy to understand
- ✅ Can be replaced in tests
When to use: Very small apps (< 500 lines), single-threaded, no testing needs
---
When Singleton MIGHT Be OK
Singleton is appropriate only for:
1. True Hardware Resources
class PrinterPort: # Only one physical printer port
...2. Application-Wide Configuration (Read-Only)
class AppConfig: # Immutable config loaded once at startup
...3. Logging Infrastructure
class Logger: # Application-wide logging (but use logging library instead!)
...Note: Even in these cases, modern alternatives (DI, service locator) are often better.
---
For Your Web App Context
Small web app characteristics:
- Solo developer or small team
- Likely to evolve and grow
- Needs testing
- May need multiple environments (dev, test, prod)
Recommendation: Use Dependency Injection
Why:
1. Now: Simple to implement, clear dependencies 2. Testing: Easy to mock database 3. Future: Easy to add connection pooling later 4. Growth: Scales as app grows
Example for Flask/FastAPI:
# app.py
from flask import Flask, g
app = Flask(__name__)
def get_db():
if 'db' not in g:
g.db = DatabaseConnection(app.config['DB_CONFIG'])
return g.db
@app.route('/users', methods=['POST'])
def create_user():
db = get_db() # Request-scoped connection
db.query(...)Or with dependency injection framework:
# Using dependency-injector library
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
db = providers.Singleton(DatabaseConnection, config.db)
container = Container()
db = container.db()---
Migration Path
If you already have Singleton:
# Before (Singleton)
class Database:
_instance = None
@staticmethod
def get_instance():
if Database._instance is None:
Database._instance = Database()
return Database._instance
db = Database.get_instance()
# After (Dependency Injection)
class Database:
def __init__(self, config):
self.config = config
# Application setup
db = Database(config)
user_service = UserService(db)---
Summary
DON'T use Singleton for database connection because:
- Creates global state (hard to test, understand)
- Doesn't support connection pooling
- Violates Single Responsibility
- Over-engineering for no benefit
DO use Dependency Injection instead:
- Explicit dependencies
- Easy testing
- Flexible
- Simple to implement
Alternative if you must have single connection:
- Module-level variable (simpler than Singleton)
- Connection pool (better for production)
Singleton is appropriate for:
- True hardware resources (printer port)
- Immutable configuration
- Application-wide logging (though library is better)
---
Would you like:
- Code example of dependency injection for your framework (Flask/FastAPI/Django)?
- Deep dive into connection pooling?
- Comparison of Singleton vs dependency injection patterns?
Example 6: Deep Dive Request
Scenario
User wants comprehensive, in-depth information about a pattern including multiple implementations, variations, and advanced topics.
User Query
User: "Deep dive into the Decorator pattern - I want to understand all the variations and when to use each"Skill Response (Excerpt - Full Response Would Be Much Longer)
Decorator Pattern - Deep Dive
Full Structure Explanation
Participants:
1. Component (interface/abstract class):
- Defines interface for objects that can have responsibilities added
- Example:
Widget,DataSource,InputStream
2. ConcreteComponent (concrete class):
- Defines an object to which additional responsibilities can be attached
- Example:
TextView,FileDataSource,FileInputStream
3. Decorator (abstract class):
- Maintains reference to Component object
- Defines interface conforming to Component's interface
- Example:
WidgetDecorator,DataSourceDecorator
4. ConcreteDecorator (concrete class):
- Adds responsibilities to the component
- Example:
ScrollDecorator,BorderDecorator,EncryptionDecorator
Collaborations:
- Decorator forwards requests to its Component object
- May perform additional operations before/after forwarding
- Decorators can be stacked (decorator wraps decorator wraps component)
Key Insight: Decorator has same interface as Component, so clients can't tell difference between decorated and undecorated objects.
---
Mermaid Diagrams
classDiagram
class Component {
<<interface>>
+operation() void
}
class ConcreteComponent {
+operation() void
}
class Decorator {
<<abstract>>
-component: Component
+operation() void
}
class ConcreteDecoratorA {
-addedState
+operation() void
+addedBehavior() void
}
class ConcreteDecoratorB {
+operation() void
+addedBehavior() void
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
Decorator o-- Component : wrapssequenceDiagram
participant Client
participant DecoratorA
participant DecoratorB
participant ConcreteComponent
Client->>DecoratorA: operation()
DecoratorA->>DecoratorA: before behavior
DecoratorA->>DecoratorB: operation()
DecoratorB->>DecoratorB: before behavior
DecoratorB->>ConcreteComponent: operation()
ConcreteComponent-->>DecoratorB: result
DecoratorB->>DecoratorB: after behavior
DecoratorB-->>DecoratorA: enhanced result
DecoratorA->>DecoratorA: after behavior
DecoratorA-->>Client: final result---
Complete Implementation: Python
from abc import ABC, abstractmethod
# ============ Component Hierarchy ============
class DataSource(ABC):
"""Component interface"""
@abstractmethod
def write_data(self, data: str) -> None:
pass
@abstractmethod
def read_data(self) -> str:
pass
class FileDataSource(DataSource):
"""ConcreteComponent - basic file operations"""
def __init__(self, filename: str):
self.filename = filename
self.data = ""
def write_data(self, data: str) -> None:
print(f"Writing to {self.filename}: {data}")
self.data = data
def read_data(self) -> str:
print(f"Reading from {self.filename}")
return self.data
# ============ Decorator Hierarchy ============
class DataSourceDecorator(DataSource):
"""Base Decorator - forwards to wrapped component"""
def __init__(self, source: DataSource):
self.wrappee = source
def write_data(self, data: str) -> None:
self.wrappee.write_data(data)
def read_data(self) -> str:
return self.wrappee.read_data()
class EncryptionDecorator(DataSourceDecorator):
"""ConcreteDecorator - adds encryption"""
def write_data(self, data: str) -> None:
encrypted = self._encrypt(data)
print(f"[Encryption] Encrypting data")
super().write_data(encrypted)
def read_data(self) -> str:
data = super().read_data()
print(f"[Encryption] Decrypting data")
return self._decrypt(data)
def _encrypt(self, data: str) -> str:
# Simple Caesar cipher for demo
return ''.join(chr(ord(c) + 3) for c in data)
def _decrypt(self, data: str) -> str:
return ''.join(chr(ord(c) - 3) for c in data)
class CompressionDecorator(DataSourceDecorator):
"""ConcreteDecorator - adds compression"""
def write_data(self, data: str) -> None:
compressed = self._compress(data)
print(f"[Compression] Compressing data")
super().write_data(compressed)
def read_data(self) -> str:
data = super().read_data()
print(f"[Compression] Decompressing data")
return self._decompress(data)
def _compress(self, data: str) -> str:
# Simple run-length encoding for demo
if not data:
return data
result = []
count = 1
prev = data[0]
for char in data[1:]:
if char == prev:
count += 1
else:
result.append(f"{count}{prev}")
prev = char
count = 1
result.append(f"{count}{prev}")
return ''.join(result)
def _decompress(self, data: str) -> str:
# Decode run-length encoding
result = []
i = 0
while i < len(data):
count = int(data[i])
char = data[i + 1]
result.append(char * count)
i += 2
return ''.join(result)
# ============ Client Code ============
def client_code(source: DataSource):
"""Client works with all objects via Component interface"""
source.write_data("Hello World")
print(f"Result: {source.read_data()}")
print()
# ============ Usage Examples ============
if __name__ == "__main__":
# Simple component
print("=== Simple File DataSource ===")
simple = FileDataSource("data.txt")
client_code(simple)
# Single decorator
print("=== Encrypted File DataSource ===")
encrypted = EncryptionDecorator(FileDataSource("encrypted.txt"))
client_code(encrypted)
# Stacked decorators (compression + encryption)
print("=== Compressed + Encrypted File DataSource ===")
compressed_encrypted = CompressionDecorator(
EncryptionDecorator(FileDataSource("secure.txt"))
)
client_code(compressed_encrypted)
# Different order (encryption + compression)
print("=== Encrypted + Compressed File DataSource ===")
encrypted_compressed = EncryptionDecorator(
CompressionDecorator(FileDataSource("secure2.txt"))
)
client_code(encrypted_compressed)Output:
=== Simple File DataSource ===
Writing to data.txt: Hello World
Reading from data.txt
Result: Hello World
=== Encrypted File DataSource ===
[Encryption] Encrypting data
Writing to encrypted.txt: Khoor#Zruog
Reading from encrypted.txt
[Encryption] Decrypting data
Result: Hello World
=== Compressed + Encrypted File DataSource ===
[Compression] Compressing data
[Encryption] Encrypting data
Writing to secure.txt: 4K2h...
Reading from secure.txt
[Encryption] Decrypting data
[Compression] Decompressing data
Result: Hello World---
Complete Implementation: TypeScript
// ============ Component Hierarchy ============
interface DataSource {
writeData(data: string): void;
readData(): string;
}
class FileDataSource implements DataSource {
private data: string = "";
constructor(private filename: string) {}
writeData(data: string): void {
console.log(`Writing to ${this.filename}: ${data}`);
this.data = data;
}
readData(): string {
console.log(`Reading from ${this.filename}`);
return this.data;
}
}
// ============ Decorator Hierarchy ============
abstract class DataSourceDecorator implements DataSource {
constructor(protected wrappee: DataSource) {}
writeData(data: string): void {
this.wrappee.writeData(data);
}
readData(): string {
return this.wrappee.readData();
}
}
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const encrypted = this.encrypt(data);
console.log("[Encryption] Encrypting data");
super.writeData(encrypted);
}
readData(): string {
const data = super.readData();
console.log("[Encryption] Decrypting data");
return this.decrypt(data);
}
private encrypt(data: string): string {
return data
.split("")
.map((c) => String.fromCharCode(c.charCodeAt(0) + 3))
.join("");
}
private decrypt(data: string): string {
return data
.split("")
.map((c) => String.fromCharCode(c.charCodeAt(0) - 3))
.join("");
}
}
class CompressionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const compressed = this.compress(data);
console.log("[Compression] Compressing data");
super.writeData(compressed);
}
readData(): string {
const data = super.readData();
console.log("[Compression] Decompressing data");
return this.decompress(data);
}
private compress(data: string): string {
// Simple run-length encoding
if (!data) return data;
const result: string[] = [];
let count = 1;
let prev = data[0];
for (let i = 1; i < data.length; i++) {
if (data[i] === prev) {
count++;
} else {
result.push(`${count}${prev}`);
prev = data[i];
count = 1;
}
}
result.push(`${count}${prev}`);
return result.join("");
}
private decompress(data: string): string {
const result: string[] = [];
for (let i = 0; i < data.length; i += 2) {
const count = parseInt(data[i]);
const char = data[i + 1];
result.push(char.repeat(count));
}
return result.join("");
}
}
// ============ Client Code ============
function clientCode(source: DataSource): void {
source.writeData("Hello World");
console.log(`Result: ${source.readData()}`);
console.log();
}
// Usage
const simple = new FileDataSource("data.txt");
clientCode(simple);
const encrypted = new EncryptionDecorator(new FileDataSource("encrypted.txt"));
clientCode(encrypted);
const compressedEncrypted = new CompressionDecorator(
new EncryptionDecorator(new FileDataSource("secure.txt"))
);
clientCode(compressedEncrypted);---
Pattern Variations
1. Transparent Decorator (Standard GoF)
- Decorator has same interface as Component
- Client can't distinguish decorated from undecorated
- When: Need complete interface compatibility
class Decorator(Component):
def __init__(self, component: Component):
self.component = component
def operation(self):
return self.component.operation() # Transparent2. Semi-Transparent Decorator
- Decorator adds new methods not in Component interface
- Client can access decorator-specific functionality
- When: Need both forwarding AND new capabilities
class Decorator(Component):
def operation(self):
return self.component.operation()
def decorator_specific_method(self):
return "Additional functionality"3. Function Decorators (Python-specific)
- Use @decorator syntax
- Wrap functions instead of objects
- When: Decorating functions, not classes
def timing_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"Time: {time.time() - start}s")
return result
return wrapper
@timing_decorator
def slow_function():
time.sleep(1)4. Streaming Decorator
- Process data in chunks (I/O streams)
- Chain decorators for layered processing
- When: Large data, memory constraints
class BufferedDecorator(StreamDecorator):
def __init__(self, stream, buffer_size=8192):
super().__init__(stream)
self.buffer_size = buffer_size
def read(self):
return self.stream.read(self.buffer_size)---
Advanced Topics
Thread Safety
- Problem: Shared decorator state in concurrent environment
- Solution: Make decorators stateless OR use thread-local storage
import threading
class ThreadSafeDecorator(Decorator):
def __init__(self, component):
super().__init__(component)
self.local = threading.local()
def operation(self):
if not hasattr(self.local, 'cache'):
self.local.cache = {}
# Use thread-local cacheMemory Leaks in Decorator Chains
- Problem: Long decorator chains hold references
- Solution: Use weak references OR limit chain depth
import weakref
class SmartDecorator(Decorator):
def __init__(self, component):
self._component_ref = weakref.ref(component)
@property
def component(self):
return self._component_ref()Performance Considerations
- Each decorator adds indirection (performance cost)
- Consider limit: 3-5 decorators maximum
- Profile before optimizing
---
When to Use Each Variation
| Variation | Use When | Avoid When |
|---|---|---|
| Transparent | Need interface compatibility, multiple decorators | Need decorator-specific methods |
| Semi-Transparent | Need both forwarding and new methods | Interface purity is critical |
| Function Decorators | Decorating functions (logging, timing) | Need object state |
| Streaming | Large data, I/O operations | Small in-memory data |
---
Philosophy Alignment Check
Amplihack Perspective:
✅ Good fit when:
- You have ≥3 orthogonal responsibilities to add
- Responsibilities need to be combined flexibly
- Subclassing would create explosion of classes
- You need runtime composition
⚠️ Caution when:
- Only 1-2 simple decorations needed (use simple wrapper function)
- Decorations are not orthogonal (consider Strategy or State)
- Interface has many methods (Decorator must forward all)
❌ Avoid when:
- Single decoration (use simple subclass or wrapper)
- Decorations are mutually exclusive (use Strategy)
- Performance is critical (indirection overhead)
Ruthless Simplicity Test:
1. Do you have ≥3 combinable behaviors? If no → use simple wrapper 2. Are behaviors orthogonal? If no → use Strategy 3. Will you actually combine them? If no → use simple subclassing
---
Historical Context
Origin: Gang of Four, "Design Patterns" (1994), pp. 175-184
Also Known As: Wrapper
Evolution:
- Classic GoF: Object-oriented decorator (classes)
- Modern languages: Function decorators (Python @decorator)
- React/JavaScript: Higher-Order Components (HOC pattern)
- Java: I/O streams (BufferedInputStream wraps FileInputStream)
Modern Relevance:
- Still widely used in I/O libraries
- Function decorators ubiquitous in Python
- Middleware pattern in web frameworks
- Aspect-Oriented Programming influence
---
Related Patterns - Deep Comparison
| Pattern | Similarity | Key Difference | When to Choose |
|---|---|---|---|
| Adapter | Both wrap objects | Adapter changes interface; Decorator enhances behavior | Use Adapter for incompatible interfaces, Decorator for adding responsibilities |
| Proxy | Both have same interface | Proxy controls access; Decorator adds behavior | Use Proxy for lazy init/access control, Decorator for flexible enhancement |
| Composite | Similar structure (recursion) | Composite focuses on part-whole; Decorator on enhancement | Use Composite for trees, Decorator for layered behavior |
| Strategy | Both change behavior | Strategy swaps algorithm; Decorator stacks behaviors | Use Strategy for alternatives, Decorator for combinations |
---
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). _Design Patterns: Elements of Reusable Object-Oriented Software_. Addison-Wesley, pp. 175-184.
- Freeman, E., & Freeman, E. (2004). _Head First Design Patterns_. O'Reilly, Chapter 3.
- Refactoring Guru: https://refactoring.guru/design-patterns/decorator
- Python Decorator PEP: https://www.python.org/dev/peps/pep-0318/
---
Would you like:
- More code examples for specific use cases?
- Comparison with Proxy pattern in detail?
- Implementation in another language (Java, C#, Go)?
Gang of Four Design Patterns Expert
Comprehensive knowledge of all 23 GoF design patterns with progressive disclosure and philosophy-aligned guidance.
Quick Start
This skill auto-activates when you mention:
- Pattern names (Factory Method, Observer, Strategy, etc.)
- "design pattern", "which pattern should I use"
- Problems needing structural solutions
- Pattern categories (creational, structural, behavioral)
What It Provides
- Tier 1: Instant quick reference (always inline)
- Tier 2: Practical guide with code (on request)
- Tier 3: Deep dive with multi-language examples (on explicit request)
Philosophy
This skill follows amplihack's ruthless simplicity - it helps you decide if patterns are appropriate, not encourage their use.
Key Principles:
- Start with simplest solution that works
- YAGNI: Don't add patterns "for future flexibility"
- Patterns need ≥2 actual use cases RIGHT NOW
- Warns against over-engineering
Example Queries
"What is the Observer pattern?" → Tier 1 quick reference
"How to implement Strategy?" → Tier 2 with code
"Which pattern for notifying multiple?" → Pattern recognition
"Factory Method vs Abstract Factory?" → Comparison
"Should I use Singleton?" → Philosophy check (warns!)
"Deep dive into Decorator" → Tier 3 comprehensivePattern Coverage
All 23 GoF Patterns:
- 5 Creational: Factory Method, Abstract Factory, Builder, Prototype, Singleton
- 7 Structural: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
- 11 Behavioral: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor
Code Examples
- Python: All 23 patterns
- TypeScript & Java: Top 10 most-used patterns
Usage Examples
See examples/ directory for:
- Quick pattern lookups
- Implementation guides
- Pattern recognition
- Philosophy checks
References
- Gang of Four (1994): "Design Patterns"
- Refactoring.Guru: Modern explanations
- Amplihack Philosophy: Ruthless simplicity lens
---
Full documentation in SKILL.md (loaded automatically when skill activates).
Remember: Philosophy First, Patterns Second.
Related skills
How it compares
Pick design-patterns-expert over generic architecture skills when you need full GoF catalog coverage with explicit anti-over-engineering guardrails during code review.
FAQ
How many patterns does design-patterns-expert cover?
design-patterns-expert covers all 23 Gang of Four design patterns: five creational, seven structural, and eleven behavioral patterns. The skill version 1.0.0 organizes them with progressive disclosure and links to ten production examples.
When does design-patterns-expert refuse a pattern?
design-patterns-expert refuses patterns that violate YAGNI, including recommendations without at least two concrete current use cases or choices driven by hypothetical future flexibility. The skill prioritizes the simplest working design over pattern completeness.
What supporting files ship with design-patterns-expert?
design-patterns-expert ships reference-patterns.md for full specifications, examples.md with ten production-ready scenarios, and antipatterns.md documenting common misuse cases. Developers request deeper files after the quick catalog overview.