Design Patterns¶
Design patterns are reusable solutions to common problems in software design. They aren't finished code you can copy-paste; rather, they are templates or best practices for solving recurring design problems in a maintainable and scalable way.
They help software engineers communicate designs clearly, avoid reinventing the wheel, and improve code quality by promoting consistency, modularity, and maintainability. The seminal book "Design Patterns: Elements of Reusable Object-Oriented Software" (1994) by the Gang of Four (GoF) formalized 23 classic patterns that remain foundational today.
Characteristics of Design Patterns¶
- Reusability: Patterns provide proven solutions that can be adapted to similar problems.
- Abstraction: Focus on design rather than specific implementation.
- Decoupling: Promote loose coupling between components for flexibility.
- Communicability: Provide a common vocabulary among developers.
- Scalability and maintainability: Help systems evolve without major rewrites.
Categories of Design Patterns¶
Design patterns are usually grouped into three main categories:
| Category | Purpose | Patterns |
|---|---|---|
| Creational | Object creation mechanisms | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| Structural | Object composition and relationships | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |
| Behavioral | Object communication and responsibilities | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor |
Advantages of Using Design Patterns¶
- Reduces development time by providing tested solutions.
- Improves code readability and maintainability.
- Promotes scalable and flexible architectures.
- Facilitates team communication by giving common terminology.
- Helps avoid common pitfalls and anti-patterns.
Disadvantages / Limitations¶
- Can lead to over-engineering if used unnecessarily.
- Might increase complexity in small applications.
- Requires experience to choose the right pattern for the right problem.
- Patterns are not a one-size-fits-all solution; improper use can make code harder to maintain.
Use Cases¶
- When you see repeated design problems across your system.
- When you want loose coupling and high cohesion.
- When building extensible and maintainable architectures.
- When working in teams and want to communicate designs clearly.
Creational Patterns¶
Creational patterns focus on object creation mechanisms, abstracting the instantiation process to make systems independent of how objects are created, composed, and represented.
1. Singleton Pattern¶
Intent: Ensures a class has only one instance and provides a global access point to it.
Real-world analogy: Think of a country's government—there's only one official government at a time, and everyone refers to the same one. Similarly, a Singleton ensures only one instance exists throughout the application.
How it works: The pattern hides the constructor (making it private), and provides a static method that returns the same instance every time it's called. The instance is typically created lazily (on first access) or eagerly (at class loading).
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
self.connection = "Connected to database"
print("Database connection established")
# Usage - both variables reference the same instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True
- Characteristics: Single instance, global access point, controlled instantiation, thread-safety considerations
- Advantages: Controlled access to shared resources, avoids multiple instances, lazy initialization possible, memory efficient
- Disadvantages: Can introduce global state (making testing harder), difficult to mock in unit tests, may lead to tight coupling, violates Single Responsibility Principle
- Use cases: Logging services, configuration management, database connection pools, caching systems, hardware interface access
2. Factory Method Pattern¶
Intent: Defines an interface for creating objects but lets subclasses decide which class to instantiate. It delegates instantiation to subclasses.
Real-world analogy: Consider a logistics company that can deliver by truck or ship. Rather than the main company handling the specifics of each transport type, it delegates to specialized divisions (TruckLogistics, SeaLogistics) that know how to create and manage their respective vehicles.
How it works: A creator class declares a factory method that returns objects of a product interface. Subclasses override this method to produce different types of products. The client code works with creators and products through their abstract interfaces.
from abc import ABC, abstractmethod
# Product interface
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# Concrete products
class EmailNotification(Notification):
def send(self, message: str) -> None:
print(f"Sending EMAIL: {message}")
class SMSNotification(Notification):
def send(self, message: str) -> None:
print(f"Sending SMS: {message}")
class PushNotification(Notification):
def send(self, message: str) -> None:
print(f"Sending PUSH: {message}")
# Creator with factory method
class NotificationFactory(ABC):
@abstractmethod
def create_notification(self) -> Notification:
pass
def notify(self, message: str) -> None:
notification = self.create_notification()
notification.send(message)
# Concrete creators
class EmailNotificationFactory(NotificationFactory):
def create_notification(self) -> Notification:
return EmailNotification()
class SMSNotificationFactory(NotificationFactory):
def create_notification(self) -> Notification:
return SMSNotification()
# Usage
factory = EmailNotificationFactory()
factory.notify("Hello, World!") # Sending EMAIL: Hello, World!
- Characteristics: Encapsulates object creation, promotes loose coupling, uses inheritance for instantiation, follows Open/Closed Principle
- Advantages: Decouples client code from concrete classes, makes adding new product types easy, promotes code reuse through inheritance
- Disadvantages: Can increase the number of classes significantly, adds complexity for simple creations, requires subclassing
- Use cases: Frameworks, plugin architectures, cross-platform UI elements, document processing (PDF, Word, HTML exporters)
3. Abstract Factory Pattern¶
Intent: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Real-world analogy: Think of a furniture store that sells matching sets—Victorian, Modern, or Art Deco. Each style has its own chair, sofa, and table designs. The Abstract Factory is like choosing a style catalog: once you pick "Modern," all furniture you order will be consistently Modern-styled.
How it works: The pattern defines interfaces for each product in a family, then creates factory interfaces that produce all products of a family. Concrete factories implement these interfaces to create products that belong together.
from abc import ABC, abstractmethod
# Abstract products
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str:
pass
# Concrete products - Windows family
class WindowsButton(Button):
def render(self) -> str:
return "Rendering Windows-style button"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "Rendering Windows-style checkbox"
# Concrete products - macOS family
class MacButton(Button):
def render(self) -> str:
return "Rendering macOS-style button"
class MacCheckbox(Checkbox):
def render(self) -> str:
return "Rendering macOS-style checkbox"
# Abstract factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete factories
class WindowsFactory(GUIFactory):
def create_button(self) -> Button:
return WindowsButton()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self) -> Button:
return MacButton()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
# Client code works with factories and products via abstract interfaces
def create_ui(factory: GUIFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())
print(checkbox.render())
# Usage - switch entire UI family by changing factory
create_ui(WindowsFactory())
create_ui(MacFactory())
- Characteristics: Factory of factories, supports multiple product families, decouples client from concrete classes, ensures product compatibility
- Advantages: Ensures consistency among related objects, isolates concrete classes, makes exchanging product families easy
- Disadvantages: Can be overkill for simple products, adding new product types requires changing all factories, complex class hierarchy
- Use cases: Cross-platform GUI toolkits, database access layers (MySQL, PostgreSQL, SQLite families), themed UI components
4. Builder Pattern¶
Intent: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Real-world analogy: Building a house involves many steps: laying foundation, building walls, installing roof, adding windows, etc. A construction director (Director) knows the order of steps, while different builders (concrete builders) know how to execute each step for different house types (wooden, stone, modern).
How it works: The Builder pattern extracts object construction into a separate Builder class. The construction is done step-by-step through method calls, and the final product is retrieved via a build() or get_result() method. An optional Director class can define the order of construction steps.
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Pizza:
size: str = "medium"
cheese: bool = True
pepperoni: bool = False
mushrooms: bool = False
onions: bool = False
bacon: bool = False
extra_cheese: bool = False
def __str__(self):
toppings = []
if self.cheese: toppings.append("cheese")
if self.pepperoni: toppings.append("pepperoni")
if self.mushrooms: toppings.append("mushrooms")
if self.onions: toppings.append("onions")
if self.bacon: toppings.append("bacon")
if self.extra_cheese: toppings.append("extra cheese")
return f"{self.size} pizza with {', '.join(toppings)}"
class PizzaBuilder:
def __init__(self):
self._pizza = Pizza()
def size(self, size: str) -> "PizzaBuilder":
self._pizza.size = size
return self
def add_pepperoni(self) -> "PizzaBuilder":
self._pizza.pepperoni = True
return self
def add_mushrooms(self) -> "PizzaBuilder":
self._pizza.mushrooms = True
return self
def add_onions(self) -> "PizzaBuilder":
self._pizza.onions = True
return self
def add_bacon(self) -> "PizzaBuilder":
self._pizza.bacon = True
return self
def add_extra_cheese(self) -> "PizzaBuilder":
self._pizza.extra_cheese = True
return self
def build(self) -> Pizza:
return self._pizza
# Usage with fluent interface
pizza = (PizzaBuilder()
.size("large")
.add_pepperoni()
.add_mushrooms()
.add_extra_cheese()
.build())
print(pizza) # large pizza with cheese, pepperoni, mushrooms, extra cheese
- Characteristics: Step-by-step construction, separates construction from representation, fluent interfaces often used, immutable products possible
- Advantages: Simplifies creation of complex objects, produces different representations using same process, isolates construction code, provides fine-grained control
- Disadvantages: Adds extra complexity with multiple new classes, may require builder for each product variant
- Use cases: Complex object creation (HTTP requests, SQL queries), configuration builders, document generation (PDF, HTML), test data builders
5. Prototype Pattern¶
Intent: Creates new objects by copying an existing object (prototype), avoiding costly initialization from scratch.
Real-world analogy: Cell division in biology—instead of building a new cell from basic molecules, an existing cell divides and clones itself. The clone can then be modified independently. Similarly, the Prototype pattern clones existing objects as a starting point.
How it works: Objects that support cloning implement a clone() method. When you need a new object similar to an existing one, you clone the prototype instead of instantiating from scratch. This is especially useful when object creation is expensive or when objects have many configurations.
import copy
from abc import ABC, abstractmethod
class Prototype(ABC):
@abstractmethod
def clone(self):
pass
class Document(Prototype):
def __init__(self, title: str, content: str, formatting: dict):
self.title = title
self.content = content
self.formatting = formatting # Complex nested object
# Simulate expensive initialization
self._process_templates()
def _process_templates(self):
# Expensive operation that we want to avoid repeating
print(f"Processing templates for '{self.title}'...")
def clone(self) -> "Document":
# Deep copy to ensure nested objects are also cloned
cloned = copy.deepcopy(self)
# Skip expensive initialization for cloned objects
return cloned
def __str__(self):
return f"Document('{self.title}', formatting={self.formatting})"
# Create an original document (expensive operation)
original = Document(
"Report Template",
"Content here...",
{"font": "Arial", "size": 12, "margins": {"top": 1, "bottom": 1}}
)
# Clone and modify (no expensive template processing)
quarterly_report = original.clone()
quarterly_report.title = "Q1 2024 Report"
quarterly_report.formatting["size"] = 14
annual_report = original.clone()
annual_report.title = "Annual Report 2024"
annual_report.formatting["font"] = "Times New Roman"
print(original) # Document('Report Template', ...)
print(quarterly_report) # Document('Q1 2024 Report', ...)
print(annual_report) # Document('Annual Report 2024', ...)
- Characteristics: Cloning of existing objects, supports deep or shallow copy, avoids expensive construction, maintains object state
- Advantages: Reduces initialization costs, simplifies creation of complex pre-configured objects, avoids subclassing, dynamic object configuration
- Disadvantages: Requires careful handling of deep vs shallow copy, circular references can be tricky, mutable nested objects need attention
- Use cases: Object duplication, prototype registries, caching expensive objects, game entity spawning, document templates
Structural Patterns¶
Structural patterns focus on how objects are composed to form larger structures while keeping them flexible and efficient. They help ensure that when parts of a system change, the entire structure doesn't need to change.
6. Adapter Pattern¶
Intent: Converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to work together.
Real-world analogy: A power adapter lets you plug a US device into a European outlet. The adapter doesn't change how either the device or outlet works—it just makes them compatible. Similarly, the Adapter pattern wraps an object to make its interface compatible with what the client expects.
How it works: An adapter wraps an object with an incompatible interface (adaptee) and exposes a compatible interface to the client. The adapter translates client requests into calls the adaptee understands.
from abc import ABC, abstractmethod
# Target interface that clients expect
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
# Existing class with incompatible interface (Adaptee)
class LegacyPaymentGateway:
def make_payment(self, dollars: int, cents: int, currency: str) -> dict:
"""Old API that uses different parameter format"""
print(f"Legacy gateway processing ${dollars}.{cents:02d} {currency}")
return {"success": True, "transaction_id": "TXN123"}
# Modern third-party service with different interface
class StripeAPI:
def charge(self, amount_cents: int, currency: str = "usd") -> dict:
"""Stripe-like API that uses cents"""
print(f"Stripe charging {amount_cents} cents")
return {"paid": True, "id": "ch_abc123"}
# Adapters make incompatible classes work with our interface
class LegacyPaymentAdapter(PaymentProcessor):
def __init__(self, legacy_gateway: LegacyPaymentGateway):
self._gateway = legacy_gateway
def process_payment(self, amount: float) -> bool:
dollars = int(amount)
cents = int((amount - dollars) * 100)
result = self._gateway.make_payment(dollars, cents, "USD")
return result.get("success", False)
class StripeAdapter(PaymentProcessor):
def __init__(self, stripe_api: StripeAPI):
self._stripe = stripe_api
def process_payment(self, amount: float) -> bool:
amount_cents = int(amount * 100)
result = self._stripe.charge(amount_cents)
return result.get("paid", False)
# Client code works uniformly with any payment processor
def checkout(processor: PaymentProcessor, amount: float):
if processor.process_payment(amount):
print("Payment successful!\n")
else:
print("Payment failed!\n")
# Usage - same client code, different adapters
checkout(LegacyPaymentAdapter(LegacyPaymentGateway()), 99.99)
checkout(StripeAdapter(StripeAPI()), 49.99)
- Characteristics: Interface conversion, wrapper object, promotes decoupling, can work with classes or objects
- Advantages: Increases reusability of existing code, enables integration of incompatible systems, follows Single Responsibility Principle
- Disadvantages: Adds extra layer of indirection, can increase overall complexity, may need multiple adapters for complex systems
- Use cases: Legacy system integration, third-party API wrappers, library compatibility layers, data format conversion
7. Bridge Pattern¶
Intent: Decouples an abstraction from its implementation so that the two can vary independently.
Real-world analogy: Consider a remote control (abstraction) and a TV (implementation). You can have different remotes (basic, advanced) and different devices (Sony TV, Samsung TV). The Bridge pattern lets you combine any remote with any device without creating RemoteForSonyTV, RemoteForSamsungTV, etc.
How it works: Instead of using inheritance to extend both abstraction and implementation, Bridge uses composition. The abstraction contains a reference to an implementation object and delegates work to it. Both hierarchies can evolve independently.
from abc import ABC, abstractmethod
# Implementation interface
class Renderer(ABC):
@abstractmethod
def render_circle(self, x: int, y: int, radius: int) -> str:
pass
@abstractmethod
def render_rectangle(self, x: int, y: int, width: int, height: int) -> str:
pass
# Concrete implementations
class VectorRenderer(Renderer):
def render_circle(self, x: int, y: int, radius: int) -> str:
return f"Drawing circle as vectors at ({x},{y}) with radius {radius}"
def render_rectangle(self, x: int, y: int, width: int, height: int) -> str:
return f"Drawing rectangle as vectors at ({x},{y}) {width}x{height}"
class RasterRenderer(Renderer):
def render_circle(self, x: int, y: int, radius: int) -> str:
return f"Drawing circle as pixels at ({x},{y}) with radius {radius}"
def render_rectangle(self, x: int, y: int, width: int, height: int) -> str:
return f"Drawing rectangle as pixels at ({x},{y}) {width}x{height}"
# Abstraction
class Shape(ABC):
def __init__(self, renderer: Renderer):
self._renderer = renderer # Bridge to implementation
@abstractmethod
def draw(self) -> str:
pass
@abstractmethod
def resize(self, factor: float) -> None:
pass
# Refined abstractions
class Circle(Shape):
def __init__(self, renderer: Renderer, x: int, y: int, radius: int):
super().__init__(renderer)
self.x, self.y, self.radius = x, y, radius
def draw(self) -> str:
return self._renderer.render_circle(self.x, self.y, self.radius)
def resize(self, factor: float) -> None:
self.radius = int(self.radius * factor)
class Rectangle(Shape):
def __init__(self, renderer: Renderer, x: int, y: int, width: int, height: int):
super().__init__(renderer)
self.x, self.y, self.width, self.height = x, y, width, height
def draw(self) -> str:
return self._renderer.render_rectangle(self.x, self.y, self.width, self.height)
def resize(self, factor: float) -> None:
self.width = int(self.width * factor)
self.height = int(self.height * factor)
# Usage - mix any shape with any renderer
vector = VectorRenderer()
raster = RasterRenderer()
shapes = [
Circle(vector, 5, 10, 20),
Circle(raster, 5, 10, 20),
Rectangle(vector, 0, 0, 100, 50),
Rectangle(raster, 0, 0, 100, 50),
]
for shape in shapes:
print(shape.draw())
- Characteristics: Two separate hierarchies (abstraction and implementation), composition over inheritance, runtime binding of implementation
- Advantages: Decouples interface from implementation, improves extensibility, hides implementation details, allows runtime switching
- Disadvantages: Increases complexity with additional classes, may be overkill for simple hierarchies
- Use cases: Cross-platform applications, graphics rendering (vector/raster), database drivers, device drivers, notification systems (email/SMS/push)
8. Composite Pattern¶
Intent: Composes objects into tree structures to represent part-whole hierarchies. Clients can treat individual objects and compositions uniformly.
Real-world analogy: A file system has files and folders. A folder can contain files and other folders. Whether you're calculating the size of a single file or an entire folder tree, the operation is the same—the folder just sums up its contents recursively.
How it works: Both leaf objects and composite objects implement the same interface. A composite holds a collection of children (which can be leaves or other composites) and delegates operations to them. This creates a recursive tree structure that can be traversed uniformly.
from abc import ABC, abstractmethod
from typing import List
# Component interface
class FileSystemItem(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def display(self, indent: int = 0) -> str:
pass
# Leaf
class File(FileSystemItem):
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) -> str:
return " " * indent + f"📄 {self.name} ({self._size} bytes)"
# Composite
class Directory(FileSystemItem):
def __init__(self, name: str):
super().__init__(name)
self._children: List[FileSystemItem] = []
def add(self, item: FileSystemItem) -> None:
self._children.append(item)
def remove(self, item: FileSystemItem) -> None:
self._children.remove(item)
def get_size(self) -> int:
# Recursively calculate total size
return sum(child.get_size() for child in self._children)
def display(self, indent: int = 0) -> str:
lines = [" " * indent + f"📁 {self.name}/"]
for child in self._children:
lines.append(child.display(indent + 1))
return "\n".join(lines)
# Usage - build a file system tree
root = Directory("project")
src = Directory("src")
tests = Directory("tests")
src.add(File("main.py", 1500))
src.add(File("utils.py", 800))
src.add(File("config.py", 300))
tests.add(File("test_main.py", 1200))
tests.add(File("test_utils.py", 600))
root.add(src)
root.add(tests)
root.add(File("README.md", 2000))
root.add(File(".gitignore", 100))
print(root.display())
print(f"\nTotal size: {root.get_size()} bytes")
print(f"src/ size: {src.get_size()} bytes")
- Characteristics: Tree-like structures, uniform interface for leaves and composites, recursive composition, part-whole hierarchy
- Advantages: Simplifies client code (no type checking needed), easy to add new component types, naturally represents hierarchies
- Disadvantages: Can make design overly general, hard to restrict what can be added to composites, may violate Interface Segregation
- Use cases: GUI component hierarchies, file systems, organizational charts, menu systems, graphics (shapes containing shapes)
9. Decorator Pattern¶
Intent: Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Real-world analogy: Coffee ordering—you start with a basic coffee, then add decorators: milk, sugar, whipped cream. Each addition wraps the previous, adding cost and description. You can stack multiple decorators in any order.
How it works: A decorator wraps a component and implements the same interface. It forwards requests to the wrapped object and may add behavior before or after. Multiple decorators can be stacked, each adding its own layer of functionality.
from abc import ABC, abstractmethod
# Component interface
class Coffee(ABC):
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
# Concrete component
class SimpleCoffee(Coffee):
def get_description(self) -> str:
return "Simple coffee"
def get_cost(self) -> float:
return 2.00
# Base decorator
class CoffeeDecorator(Coffee, ABC):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def get_description(self) -> str:
return self._coffee.get_description()
def get_cost(self) -> float:
return self._coffee.get_cost()
# Concrete decorators
class Milk(CoffeeDecorator):
def get_description(self) -> str:
return f"{self._coffee.get_description()}, milk"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.50
class Sugar(CoffeeDecorator):
def get_description(self) -> str:
return f"{self._coffee.get_description()}, sugar"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.25
class WhippedCream(CoffeeDecorator):
def get_description(self) -> str:
return f"{self._coffee.get_description()}, whipped cream"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.75
class Vanilla(CoffeeDecorator):
def get_description(self) -> str:
return f"{self._coffee.get_description()}, vanilla"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.60
# Usage - stack decorators to build complex orders
coffee = SimpleCoffee()
print(f"{coffee.get_description()} = ${coffee.get_cost():.2f}")
coffee_with_milk = Milk(SimpleCoffee())
print(f"{coffee_with_milk.get_description()} = ${coffee_with_milk.get_cost():.2f}")
# Stack multiple decorators
fancy_coffee = WhippedCream(Vanilla(Milk(SimpleCoffee())))
print(f"{fancy_coffee.get_description()} = ${fancy_coffee.get_cost():.2f}")
# Can add same decorator multiple times
double_sugar = Sugar(Sugar(SimpleCoffee()))
print(f"{double_sugar.get_description()} = ${double_sugar.get_cost():.2f}")
- Characteristics: Dynamic behavior extension, wrapper objects, same interface as wrapped object, stackable decorators
- Advantages: More flexible than static inheritance, add/remove responsibilities at runtime, follows Open/Closed Principle, avoids feature-laden base classes
- Disadvantages: Can lead to many small objects, order of decorators can matter, hard to remove a specific decorator from the middle
- Use cases: I/O streams (buffering, compression, encryption), GUI components (borders, scrollbars), middleware pipelines, logging/caching layers
10. Facade Pattern¶
Intent: Provides a simplified, unified interface to a complex subsystem, making it easier to use.
Real-world analogy: A hotel concierge is a facade. Instead of you calling the restaurant, spa, taxi company, and tour operator separately, you just tell the concierge what you want, and they handle all the complex interactions behind the scenes.
How it works: The facade provides simple methods that orchestrate complex operations involving multiple subsystem classes. Clients interact with the facade instead of dealing with subsystem complexity directly. The subsystem classes remain accessible for advanced use cases.
# Complex subsystem classes
class VideoFile:
def __init__(self, filename: str):
self.filename = filename
self.codec = filename.split('.')[-1]
class CodecFactory:
@staticmethod
def extract(file: VideoFile) -> str:
print(f"CodecFactory: extracting {file.codec} codec...")
return file.codec
class BitrateReader:
@staticmethod
def read(filename: str, codec: str) -> str:
print(f"BitrateReader: reading bitrate of {filename}...")
return f"bitrate_data_{codec}"
@staticmethod
def convert(buffer: str, codec: str) -> str:
print(f"BitrateReader: converting to {codec}...")
return f"converted_{buffer}"
class AudioMixer:
@staticmethod
def fix(data: str) -> str:
print("AudioMixer: fixing audio...")
return f"fixed_audio_{data}"
class VideoProcessor:
@staticmethod
def process(video_data: str, audio_data: str) -> str:
print("VideoProcessor: processing video and audio...")
return f"processed_{video_data}_{audio_data}"
class FileWriter:
@staticmethod
def write(data: str, filename: str) -> None:
print(f"FileWriter: writing to {filename}")
# Facade - provides simple interface to complex video conversion
class VideoConverterFacade:
"""
Simple interface for video conversion.
Hides complexity of codec extraction, bitrate conversion,
audio mixing, and file writing.
"""
def convert(self, filename: str, target_format: str) -> str:
print(f"\n{'='*50}")
print(f"VideoConverter: Starting conversion of {filename} to {target_format}")
print('='*50)
# Complex subsystem interactions hidden from client
file = VideoFile(filename)
source_codec = CodecFactory.extract(file)
bitrate_data = BitrateReader.read(filename, source_codec)
converted_data = BitrateReader.convert(bitrate_data, target_format)
audio_data = AudioMixer.fix(converted_data)
final_data = VideoProcessor.process(converted_data, audio_data)
output_file = filename.replace(file.codec, target_format)
FileWriter.write(final_data, output_file)
print(f"VideoConverter: Conversion complete! Output: {output_file}\n")
return output_file
# Client code - simple and clean
converter = VideoConverterFacade()
converter.convert("birthday_party.avi", "mp4")
converter.convert("lecture.mov", "webm")
- Characteristics: Unified simple interface, subsystem encapsulation, reduces coupling, doesn't prevent direct subsystem access
- Advantages: Simplifies complex API usage, promotes loose coupling, improves code readability, provides entry point for complex operations
- Disadvantages: Can become a "god object" if overused, may hide too much functionality, can limit access to advanced features
- Use cases: Library APIs, complex initialization sequences, legacy system wrappers, microservice gateways, third-party integration
11. Flyweight Pattern¶
Intent: Minimizes memory usage by sharing common data between multiple objects instead of storing duplicate data in each object.
Real-world analogy: In a text editor, each character could be an object. But storing font, size, and color for every single character would waste memory. Instead, the Flyweight pattern shares this common formatting data (intrinsic state) while keeping character position (extrinsic state) separate.
How it works: The pattern separates object state into intrinsic (shared, immutable) and extrinsic (unique, context-dependent). A factory manages shared flyweight objects, creating them once and reusing them. Extrinsic state is passed to methods rather than stored.
from typing import Dict
# Flyweight - stores intrinsic (shared) state
class TreeType:
"""Shared state for tree rendering (name, color, texture)"""
def __init__(self, name: str, color: str, texture: str):
# Intrinsic state - shared between many trees
self.name = name
self.color = color
self.texture = texture
# Simulate loading heavy texture data
self._texture_data = f"[Heavy texture data for {texture}]"
def draw(self, x: int, y: int) -> None:
"""Draw tree at given position (extrinsic state passed in)"""
print(f"Drawing {self.name} tree ({self.color}) at ({x}, {y})")
# Flyweight Factory - ensures sharing of flyweights
class TreeFactory:
_tree_types: Dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, color: str, texture: str) -> TreeType:
key = f"{name}_{color}_{texture}"
if key not in cls._tree_types:
print(f"TreeFactory: Creating new TreeType for '{name}'")
cls._tree_types[key] = TreeType(name, color, texture)
return cls._tree_types[key]
@classmethod
def get_type_count(cls) -> int:
return len(cls._tree_types)
# Context - stores extrinsic state and references flyweight
class Tree:
"""Individual tree with unique position (extrinsic state)"""
def __init__(self, x: int, y: int, tree_type: TreeType):
# Extrinsic state - unique per tree
self.x = x
self.y = y
# Reference to shared flyweight
self._type = tree_type
def draw(self) -> None:
self._type.draw(self.x, self.y)
# Client - Forest that manages many trees
class Forest:
def __init__(self):
self._trees: list[Tree] = []
def plant_tree(self, x: int, y: int, name: str, color: str, texture: str) -> None:
tree_type = TreeFactory.get_tree_type(name, color, texture)
tree = Tree(x, y, tree_type)
self._trees.append(tree)
def draw(self) -> None:
for tree in self._trees:
tree.draw()
# Usage - create forest with many trees but few flyweights
forest = Forest()
# Plant many trees - but only 3 TreeType objects created
print("Planting trees...")
forest.plant_tree(10, 20, "Oak", "green", "oak_texture.png")
forest.plant_tree(30, 40, "Oak", "green", "oak_texture.png") # Reuses Oak
forest.plant_tree(50, 60, "Pine", "dark_green", "pine_texture.png")
forest.plant_tree(70, 80, "Pine", "dark_green", "pine_texture.png") # Reuses Pine
forest.plant_tree(90, 100, "Birch", "light_green", "birch_texture.png")
forest.plant_tree(15, 25, "Oak", "green", "oak_texture.png") # Reuses Oak
print(f"\nTotal trees: {len(forest._trees)}")
print(f"Unique TreeTypes (flyweights): {TreeFactory.get_type_count()}")
print("\nDrawing forest:")
forest.draw()
- Characteristics: Intrinsic/extrinsic state separation, shared immutable objects, factory for flyweight management, memory optimization
- Advantages: Dramatically reduces memory usage, improves cache performance, handles large numbers of similar objects efficiently
- Disadvantages: Increases code complexity, requires careful state separation, may introduce threading concerns for mutable intrinsic state
- Use cases: Text editors (character formatting), game development (bullets, particles, tiles), caching, symbol tables, network connections
12. Proxy Pattern¶
Intent: Provides a surrogate or placeholder for another object to control access, add functionality, or defer expensive operations.
Real-world analogy: A credit card is a proxy for your bank account. It provides the same interface (pay for things) but adds access control (PIN verification), logging (transaction history), and doesn't require carrying cash (lazy loading of actual funds).
How it works: The proxy implements the same interface as the real object and holds a reference to it. Depending on the type of proxy, it can add behavior before/after delegating to the real object, or even decide not to delegate at all.
from abc import ABC, abstractmethod
from time import sleep
from functools import lru_cache
# Subject interface
class Database(ABC):
@abstractmethod
def query(self, sql: str) -> list:
pass
@abstractmethod
def execute(self, sql: str) -> bool:
pass
# Real subject - expensive to create and use
class RealDatabase(Database):
def __init__(self, connection_string: str):
print(f"Connecting to database: {connection_string}")
sleep(0.5) # Simulate slow connection
self._connection = connection_string
print("Database connection established")
def query(self, sql: str) -> list:
print(f"Executing query: {sql}")
sleep(0.2) # Simulate query time
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
def execute(self, sql: str) -> bool:
print(f"Executing: {sql}")
sleep(0.1)
return True
# Virtual Proxy - lazy initialization
class LazyDatabaseProxy(Database):
"""Defers database connection until actually needed"""
def __init__(self, connection_string: str):
self._connection_string = connection_string
self._database = None
def _get_database(self) -> RealDatabase:
if self._database is None:
print("LazyProxy: Creating database on first use")
self._database = RealDatabase(self._connection_string)
return self._database
def query(self, sql: str) -> list:
return self._get_database().query(sql)
def execute(self, sql: str) -> bool:
return self._get_database().execute(sql)
# Protection Proxy - access control
class SecureDatabaseProxy(Database):
"""Adds authentication and authorization checks"""
def __init__(self, database: Database, user_role: str):
self._database = database
self._user_role = user_role
def query(self, sql: str) -> list:
print(f"SecureProxy: Checking read permissions for {self._user_role}")
return self._database.query(sql)
def execute(self, sql: str) -> bool:
if self._user_role != "admin":
print(f"SecureProxy: ACCESS DENIED - {self._user_role} cannot execute writes")
return False
print(f"SecureProxy: Admin access granted")
return self._database.execute(sql)
# Caching Proxy - performance optimization
class CachingDatabaseProxy(Database):
"""Caches query results to avoid repeated database calls"""
def __init__(self, database: Database):
self._database = database
self._cache: dict = {}
def query(self, sql: str) -> list:
if sql in self._cache:
print(f"CacheProxy: Returning cached result for '{sql}'")
return self._cache[sql]
print(f"CacheProxy: Cache miss, querying database")
result = self._database.query(sql)
self._cache[sql] = result
return result
def execute(self, sql: str) -> bool:
# Invalidate cache on writes
self._cache.clear()
print("CacheProxy: Cache cleared due to write operation")
return self._database.execute(sql)
# Usage examples
print("=== Lazy Proxy Demo ===")
lazy_db = LazyDatabaseProxy("postgresql://localhost/mydb")
print("Proxy created, but no connection yet...")
lazy_db.query("SELECT * FROM users") # Now it connects
print("\n=== Protection Proxy Demo ===")
real_db = RealDatabase("postgresql://localhost/mydb")
user_db = SecureDatabaseProxy(real_db, "viewer")
admin_db = SecureDatabaseProxy(real_db, "admin")
user_db.query("SELECT * FROM users") # Allowed
user_db.execute("DELETE FROM users") # Denied
admin_db.execute("DELETE FROM users") # Allowed
print("\n=== Caching Proxy Demo ===")
cached_db = CachingDatabaseProxy(RealDatabase("postgresql://localhost/mydb"))
cached_db.query("SELECT * FROM users") # Cache miss
cached_db.query("SELECT * FROM users") # Cache hit
- Characteristics: Same interface as real object, controls access to real object, can add behavior transparently, various proxy types (virtual, protection, remote, caching)
- Advantages: Controls object access without changing it, adds functionality transparently, enables lazy initialization, can protect sensitive resources
- Disadvantages: Adds indirection and latency, can complicate code, proxy behavior may be unexpected
- Use cases: Lazy loading (virtual proxy), access control (protection proxy), caching, logging, remote objects (RPC/RMI), smart references
Behavioral Patterns¶
Behavioral patterns are concerned with algorithms and object interactions, focusing on how objects communicate, distribute responsibilities, and encapsulate varying behavior.
13. Chain of Responsibility Pattern¶
Intent: Passes a request along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler.
Real-world analogy: Customer support escalation—your complaint goes to a support agent first. If they can't help, it escalates to a supervisor, then a manager, then corporate. Each level either handles your issue or passes it up the chain.
How it works: Handlers are linked in a chain. Each handler has a reference to the next handler. When a request comes in, a handler either processes it and stops the chain, or passes it to the next handler. The client only needs to send the request to the first handler.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum, auto
class Priority(Enum):
LOW = auto()
MEDIUM = auto()
HIGH = auto()
CRITICAL = auto()
@dataclass
class SupportTicket:
id: str
description: str
priority: Priority
customer_tier: str # "basic", "premium", "enterprise"
# Handler interface
class SupportHandler(ABC):
def __init__(self):
self._next_handler: SupportHandler | None = None
def set_next(self, handler: "SupportHandler") -> "SupportHandler":
self._next_handler = handler
return handler # Enable chaining
@abstractmethod
def handle(self, ticket: SupportTicket) -> str | None:
pass
def pass_to_next(self, ticket: SupportTicket) -> str | None:
if self._next_handler:
return self._next_handler.handle(ticket)
return None
# Concrete handlers
class AutomatedSupport(SupportHandler):
"""Handles simple, common issues automatically"""
KNOWN_ISSUES = ["password reset", "billing inquiry", "status check"]
def handle(self, ticket: SupportTicket) -> str | None:
if any(issue in ticket.description.lower() for issue in self.KNOWN_ISSUES):
return f"[AutoBot] Ticket {ticket.id}: Resolved automatically via knowledge base"
print(f"[AutoBot] Ticket {ticket.id}: Cannot handle, escalating...")
return self.pass_to_next(ticket)
class BasicSupport(SupportHandler):
"""Handles low-priority tickets from basic customers"""
def handle(self, ticket: SupportTicket) -> str | None:
if ticket.priority == Priority.LOW:
return f"[BasicSupport] Ticket {ticket.id}: Resolved by support agent"
print(f"[BasicSupport] Ticket {ticket.id}: Priority too high, escalating...")
return self.pass_to_next(ticket)
class SeniorSupport(SupportHandler):
"""Handles medium/high priority or premium customers"""
def handle(self, ticket: SupportTicket) -> str | None:
if ticket.priority in [Priority.MEDIUM, Priority.HIGH]:
return f"[SeniorSupport] Ticket {ticket.id}: Resolved by senior agent"
if ticket.customer_tier == "premium":
return f"[SeniorSupport] Ticket {ticket.id}: Premium customer handled"
print(f"[SeniorSupport] Ticket {ticket.id}: Critical issue, escalating to manager...")
return self.pass_to_next(ticket)
class ManagerSupport(SupportHandler):
"""Handles critical issues and enterprise customers"""
def handle(self, ticket: SupportTicket) -> str | None:
if ticket.priority == Priority.CRITICAL or ticket.customer_tier == "enterprise":
return f"[Manager] Ticket {ticket.id}: Escalated to management and resolved"
return f"[Manager] Ticket {ticket.id}: Handled by manager as fallback"
# Build the chain
auto_support = AutomatedSupport()
basic_support = BasicSupport()
senior_support = SeniorSupport()
manager_support = ManagerSupport()
auto_support.set_next(basic_support).set_next(senior_support).set_next(manager_support)
# Test tickets
tickets = [
SupportTicket("T001", "Password reset needed", Priority.LOW, "basic"),
SupportTicket("T002", "App crashing on startup", Priority.MEDIUM, "basic"),
SupportTicket("T003", "Data loss incident", Priority.CRITICAL, "premium"),
SupportTicket("T004", "Feature request", Priority.LOW, "enterprise"),
]
print("Processing support tickets:\n")
for ticket in tickets:
result = auto_support.handle(ticket)
print(f"Result: {result}\n")
- Characteristics: Linked handler chain, each handler decides to process or pass, decoupled sender and receiver, dynamic chain modification
- Advantages: Reduces coupling between sender and receivers, flexible request processing, easy to add/remove handlers, follows Single Responsibility
- Disadvantages: No guarantee request will be handled, can be hard to debug the chain, potential performance overhead
- Use cases: Event handling, middleware pipelines, authentication/authorization chains, logging levels, exception handling
14. Command Pattern¶
Intent: Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
Real-world analogy: A restaurant order—when you tell the waiter "I'll have the steak, medium-rare," they write it down (command object). The order sits in a queue, gets sent to the kitchen (receiver), and if needed, can be cancelled. The waiter doesn't cook—they just manage commands.
How it works: Commands encapsulate all request details (receiver, method, arguments) into an object. An invoker holds commands and triggers them. The receiver performs the actual work. This decoupling enables queuing, logging, and undo functionality.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List
# Command interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Receiver - the actual text editor
class TextEditor:
def __init__(self):
self._content: List[str] = []
self._cursor = 0
def insert(self, text: str, position: int) -> None:
self._content.insert(position, text)
print(f"Inserted '{text}' at position {position}")
def delete(self, position: int) -> str:
if 0 <= position < len(self._content):
removed = self._content.pop(position)
print(f"Deleted '{removed}' from position {position}")
return removed
return ""
def get_content(self) -> str:
return "".join(self._content)
# Concrete commands
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str, position: int):
self._editor = editor
self._text = text
self._position = position
def execute(self) -> None:
self._editor.insert(self._text, self._position)
def undo(self) -> None:
self._editor.delete(self._position)
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, position: int):
self._editor = editor
self._position = position
self._deleted_text = ""
def execute(self) -> None:
self._deleted_text = self._editor.delete(self._position)
def undo(self) -> None:
if self._deleted_text:
self._editor.insert(self._deleted_text, self._position)
# Macro command - composite of commands
class MacroCommand(Command):
def __init__(self, commands: List[Command]):
self._commands = commands
def execute(self) -> None:
for cmd in self._commands:
cmd.execute()
def undo(self) -> None:
for cmd in reversed(self._commands):
cmd.undo()
# Invoker - manages command history
class CommandManager:
def __init__(self):
self._history: List[Command] = []
self._redo_stack: List[Command] = []
def execute(self, command: Command) -> None:
command.execute()
self._history.append(command)
self._redo_stack.clear() # Clear redo on new command
def undo(self) -> None:
if self._history:
command = self._history.pop()
command.undo()
self._redo_stack.append(command)
print("Undo performed")
else:
print("Nothing to undo")
def redo(self) -> None:
if self._redo_stack:
command = self._redo_stack.pop()
command.execute()
self._history.append(command)
print("Redo performed")
else:
print("Nothing to redo")
# Usage
editor = TextEditor()
manager = CommandManager()
# Execute commands
manager.execute(InsertCommand(editor, "H", 0))
manager.execute(InsertCommand(editor, "e", 1))
manager.execute(InsertCommand(editor, "l", 2))
manager.execute(InsertCommand(editor, "l", 3))
manager.execute(InsertCommand(editor, "o", 4))
print(f"\nContent: '{editor.get_content()}'")
# Undo/Redo
manager.undo() # Remove 'o'
manager.undo() # Remove 'l'
print(f"After 2 undos: '{editor.get_content()}'")
manager.redo() # Add 'l' back
print(f"After redo: '{editor.get_content()}'")
# Macro command
print("\n--- Macro Command ---")
macro = MacroCommand([
InsertCommand(editor, "!", 4),
InsertCommand(editor, "!", 5),
InsertCommand(editor, "!", 6),
])
manager.execute(macro)
print(f"After macro: '{editor.get_content()}'")
manager.undo() # Undo entire macro
print(f"After undo macro: '{editor.get_content()}'")
- Characteristics: Encapsulated commands, decouples sender and receiver, supports undo/redo, enables command queuing and logging
- Advantages: Decouples invoker from receiver, supports undo/redo, commands can be queued/scheduled, easy to add new commands
- Disadvantages: Can increase number of classes significantly, adds indirection, simple operations become complex
- Use cases: GUI actions (buttons, menu items), transaction systems, task scheduling, macro recording, game action replay
15. Interpreter Pattern¶
Intent: Defines a representation for a language's grammar and provides an interpreter to evaluate sentences in that language.
Real-world analogy: A music score is a language with symbols for notes, duration, and dynamics. A musician (interpreter) reads this language and produces music. Similarly, the pattern defines grammar rules and interprets expressions according to those rules.
How it works: The pattern defines an abstract expression class with an interpret() method. Terminal expressions handle basic elements, while non-terminal expressions combine other expressions according to grammar rules. A context object may hold global information needed during interpretation.
from abc import ABC, abstractmethod
from typing import Dict
# Context - holds variable values
class Context:
def __init__(self):
self._variables: Dict[str, int] = {}
def set_variable(self, name: str, value: int) -> None:
self._variables[name] = value
def get_variable(self, name: str) -> int:
return self._variables.get(name, 0)
# Abstract expression
class Expression(ABC):
@abstractmethod
def interpret(self, context: Context) -> int:
pass
# Terminal expressions
class NumberExpression(Expression):
"""Literal number"""
def __init__(self, value: int):
self._value = value
def interpret(self, context: Context) -> int:
return self._value
def __repr__(self):
return str(self._value)
class VariableExpression(Expression):
"""Variable reference"""
def __init__(self, name: str):
self._name = name
def interpret(self, context: Context) -> int:
return context.get_variable(self._name)
def __repr__(self):
return self._name
# Non-terminal expressions (combine other expressions)
class AddExpression(Expression):
def __init__(self, left: Expression, right: Expression):
self._left = left
self._right = right
def interpret(self, context: Context) -> int:
return self._left.interpret(context) + self._right.interpret(context)
def __repr__(self):
return f"({self._left} + {self._right})"
class SubtractExpression(Expression):
def __init__(self, left: Expression, right: Expression):
self._left = left
self._right = right
def interpret(self, context: Context) -> int:
return self._left.interpret(context) - self._right.interpret(context)
def __repr__(self):
return f"({self._left} - {self._right})"
class MultiplyExpression(Expression):
def __init__(self, left: Expression, right: Expression):
self._left = left
self._right = right
def interpret(self, context: Context) -> int:
return self._left.interpret(context) * self._right.interpret(context)
def __repr__(self):
return f"({self._left} * {self._right})"
# Simple parser (for demonstration)
class ExpressionParser:
"""Parses simple expressions like 'x + 5 * y'"""
@staticmethod
def parse(expression: str) -> Expression:
tokens = expression.replace('(', '').replace(')', '').split()
return ExpressionParser._parse_tokens(tokens)
@staticmethod
def _parse_tokens(tokens: list) -> Expression:
if len(tokens) == 1:
token = tokens[0]
if token.isdigit() or (token[0] == '-' and token[1:].isdigit()):
return NumberExpression(int(token))
return VariableExpression(token)
# Find lowest precedence operator (+ or -)
for i in range(len(tokens) - 1, -1, -1):
if tokens[i] == '+':
left = ExpressionParser._parse_tokens(tokens[:i])
right = ExpressionParser._parse_tokens(tokens[i+1:])
return AddExpression(left, right)
elif tokens[i] == '-':
left = ExpressionParser._parse_tokens(tokens[:i])
right = ExpressionParser._parse_tokens(tokens[i+1:])
return SubtractExpression(left, right)
# Then multiplication
for i in range(len(tokens) - 1, -1, -1):
if tokens[i] == '*':
left = ExpressionParser._parse_tokens(tokens[:i])
right = ExpressionParser._parse_tokens(tokens[i+1:])
return MultiplyExpression(left, right)
return NumberExpression(0)
# Usage
context = Context()
context.set_variable("x", 10)
context.set_variable("y", 5)
context.set_variable("z", 2)
# Build expression tree manually: (x + y) * z
expr1 = MultiplyExpression(
AddExpression(VariableExpression("x"), VariableExpression("y")),
VariableExpression("z")
)
print(f"Expression: {expr1}")
print(f"Result (x=10, y=5, z=2): {expr1.interpret(context)}")
# Parse from string
expr2 = ExpressionParser.parse("x + y * z")
print(f"\nParsed expression: {expr2}")
print(f"Result: {expr2.interpret(context)}")
# Change context
context.set_variable("x", 100)
print(f"\nAfter x=100: {expr2.interpret(context)}")
- Characteristics: Grammar representation, recursive expression tree, terminal and non-terminal expressions, context for shared state
- Advantages: Easy to implement simple grammars, easy to extend with new expressions, grammar rules are explicit in code
- Disadvantages: Complex grammars lead to many classes, can be inefficient for large expressions, better alternatives exist (parser generators)
- Use cases: SQL parsing, regular expressions, mathematical expressions, configuration languages, template engines, domain-specific languages (DSLs)
16. Iterator Pattern¶
Intent: Provides a way to access elements of a collection sequentially without exposing its underlying representation.
Real-world analogy: A TV remote with channel buttons. You don't need to know how channels are stored or organized—you just press "next channel" to iterate through them. The iterator abstracts away the internal structure of the channel list.
How it works: The pattern defines an Iterator interface with methods like next() and has_next(). Collections provide a method to create iterators. This separates traversal logic from collection implementation, allowing multiple iteration strategies and simultaneous traversals.
from abc import ABC, abstractmethod
from typing import TypeVar, Generic, List, Iterator as TypingIterator
from dataclasses import dataclass
T = TypeVar('T')
# Iterator interface
class Iterator(ABC, Generic[T]):
@abstractmethod
def has_next(self) -> bool:
pass
@abstractmethod
def next(self) -> T:
pass
@abstractmethod
def current(self) -> T:
pass
# Aggregate interface
class IterableCollection(ABC, Generic[T]):
@abstractmethod
def create_iterator(self) -> Iterator[T]:
pass
# Concrete collection - Binary Search Tree
@dataclass
class TreeNode:
value: int
left: "TreeNode | None" = None
right: "TreeNode | None" = None
class BinarySearchTree(IterableCollection[int]):
def __init__(self):
self._root: TreeNode | None = None
def insert(self, value: int) -> None:
if not self._root:
self._root = TreeNode(value)
else:
self._insert_recursive(self._root, value)
def _insert_recursive(self, node: TreeNode, value: int) -> None:
if value < node.value:
if node.left is None:
node.left = TreeNode(value)
else:
self._insert_recursive(node.left, value)
else:
if node.right is None:
node.right = TreeNode(value)
else:
self._insert_recursive(node.right, value)
def create_iterator(self) -> "Iterator[int]":
return InOrderIterator(self._root)
def create_reverse_iterator(self) -> "Iterator[int]":
return ReverseInOrderIterator(self._root)
# Concrete iterators - different traversal strategies
class InOrderIterator(Iterator[int]):
"""Traverses tree in ascending order"""
def __init__(self, root: TreeNode | None):
self._stack: List[TreeNode] = []
self._push_left(root)
def _push_left(self, node: TreeNode | None) -> None:
while node:
self._stack.append(node)
node = node.left
def has_next(self) -> bool:
return len(self._stack) > 0
def current(self) -> int:
if not self._stack:
raise StopIteration()
return self._stack[-1].value
def next(self) -> int:
if not self.has_next():
raise StopIteration()
node = self._stack.pop()
self._push_left(node.right)
return node.value
class ReverseInOrderIterator(Iterator[int]):
"""Traverses tree in descending order"""
def __init__(self, root: TreeNode | None):
self._stack: List[TreeNode] = []
self._push_right(root)
def _push_right(self, node: TreeNode | None) -> None:
while node:
self._stack.append(node)
node = node.right
def has_next(self) -> bool:
return len(self._stack) > 0
def current(self) -> int:
if not self._stack:
raise StopIteration()
return self._stack[-1].value
def next(self) -> int:
if not self.has_next():
raise StopIteration()
node = self._stack.pop()
self._push_right(node.left)
return node.value
# Python-style iterator support (bonus)
class PythonIteratorAdapter(TypingIterator[int]):
def __init__(self, iterator: Iterator[int]):
self._iterator = iterator
def __next__(self) -> int:
if self._iterator.has_next():
return self._iterator.next()
raise StopIteration()
# Usage
tree = BinarySearchTree()
for value in [50, 30, 70, 20, 40, 60, 80]:
tree.insert(value)
print("In-order traversal (ascending):")
iterator = tree.create_iterator()
while iterator.has_next():
print(iterator.next(), end=" ")
print()
print("\nReverse in-order traversal (descending):")
reverse_iter = tree.create_reverse_iterator()
while reverse_iter.has_next():
print(reverse_iter.next(), end=" ")
print()
# Using Python's for loop with adapter
print("\nUsing Python for loop:")
for value in PythonIteratorAdapter(tree.create_iterator()):
print(value, end=" ")
print()
- Characteristics: Sequential access, hides collection structure, decouples traversal from collection, supports multiple simultaneous iterations
- Advantages: Simplifies client code, supports various traversal strategies, promotes encapsulation, follows Single Responsibility
- Disadvantages: Can be overkill for simple collections, additional classes needed, may be less efficient than direct access
- Use cases: Collection traversal, database cursors, file system navigation, social network friend lists, streaming data
17. Mediator Pattern¶
Intent: Defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other directly.
Real-world analogy: An air traffic control tower is a mediator. Pilots don't communicate directly with each other—they all talk to the tower, which coordinates takeoffs, landings, and routes. This prevents chaos and reduces the complexity of pilot-to-pilot communication.
How it works: Instead of components communicating directly (leading to a complex web of dependencies), they send messages through a mediator. The mediator knows about all components and routes messages appropriately. Components only know about the mediator, not each other.
from abc import ABC, abstractmethod
from typing import Dict, List
from dataclasses import dataclass
from enum import Enum
# Mediator interface
class ChatRoomMediator(ABC):
@abstractmethod
def register_user(self, user: "User") -> None:
pass
@abstractmethod
def send_message(self, message: str, sender: "User", recipient: str | None = None) -> None:
pass
@abstractmethod
def send_file(self, filename: str, sender: "User", recipient: str) -> None:
pass
# Colleague (Component) interface
class User(ABC):
def __init__(self, name: str, mediator: ChatRoomMediator):
self._name = name
self._mediator = mediator
self._mediator.register_user(self)
@property
def name(self) -> str:
return self._name
def send(self, message: str, to: str | None = None) -> None:
self._mediator.send_message(message, self, to)
def send_file(self, filename: str, to: str) -> None:
self._mediator.send_file(filename, self, to)
@abstractmethod
def receive(self, message: str, sender: str) -> None:
pass
@abstractmethod
def receive_file(self, filename: str, sender: str) -> None:
pass
# Concrete mediator
class ChatRoom(ChatRoomMediator):
def __init__(self, name: str):
self._name = name
self._users: Dict[str, User] = {}
self._message_history: List[str] = []
def register_user(self, user: User) -> None:
if user.name not in self._users:
self._users[user.name] = user
self._broadcast(f"*** {user.name} has joined the chat ***", None)
def send_message(self, message: str, sender: User, recipient: str | None = None) -> None:
if recipient:
# Private message
if recipient in self._users:
self._users[recipient].receive(f"[Private] {message}", sender.name)
self._log(f"[Private] {sender.name} -> {recipient}: {message}")
else:
sender.receive(f"User {recipient} not found", "System")
else:
# Broadcast to all
self._broadcast(message, sender)
def send_file(self, filename: str, sender: User, recipient: str) -> None:
if recipient in self._users:
self._users[recipient].receive_file(filename, sender.name)
self._log(f"[File] {sender.name} -> {recipient}: {filename}")
else:
sender.receive(f"User {recipient} not found", "System")
def _broadcast(self, message: str, sender: User | None) -> None:
sender_name = sender.name if sender else "System"
self._log(f"{sender_name}: {message}")
for name, user in self._users.items():
if sender is None or name != sender.name:
user.receive(message, sender_name)
def _log(self, message: str) -> None:
self._message_history.append(message)
def get_history(self) -> List[str]:
return self._message_history.copy()
# Concrete colleagues (different user types)
class RegularUser(User):
def receive(self, message: str, sender: str) -> None:
print(f" [{self._name}] received from {sender}: {message}")
def receive_file(self, filename: str, sender: str) -> None:
print(f" [{self._name}] received file '{filename}' from {sender}")
class PremiumUser(User):
"""Premium users get notifications and can see typing indicators"""
def receive(self, message: str, sender: str) -> None:
print(f" [{self._name}] 🌟 received from {sender}: {message}")
def receive_file(self, filename: str, sender: str) -> None:
print(f" [{self._name}] 🌟 received file '{filename}' from {sender} (priority download)")
# Usage
print("=== Chat Room Demo ===\n")
chat = ChatRoom("Developers Lounge")
# Users register with mediator
alice = PremiumUser("Alice", chat)
bob = RegularUser("Bob", chat)
charlie = RegularUser("Charlie", chat)
print("\n--- Conversation ---")
alice.send("Hello everyone!")
bob.send("Hi Alice!")
charlie.send("Hey team!")
print("\n--- Private Message ---")
alice.send("Can you review my PR?", to="Bob")
print("\n--- File Sharing ---")
bob.send_file("code_review.pdf", to="Alice")
print("\n--- New User Joins ---")
dave = RegularUser("Dave", chat)
dave.send("Hi, I'm new here!")
print("\n--- Chat History ---")
for msg in chat.get_history():
print(f" {msg}")
- Characteristics: Centralized communication, decoupled colleagues, mediator knows all participants, can become complex
- Advantages: Reduces coupling between components, centralizes control, simplifies object protocols, easy to add new components
- Disadvantages: Mediator can become a "god object," potential single point of failure, can be overly complex
- Use cases: GUI dialog coordination, chat rooms, air traffic control, workflow orchestration, event aggregation
18. Memento Pattern¶
Intent: Captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored to that state later.
Real-world analogy: A video game save system. When you save, the game captures your current state (position, health, inventory). Later, you can load that save to restore exactly where you were. The save file (memento) doesn't expose the game's internal implementation.
How it works: The Originator creates a Memento containing a snapshot of its state. A Caretaker stores mementos but cannot access their contents. Only the Originator can restore state from a Memento. This preserves encapsulation while enabling state restoration.
from dataclasses import dataclass, field
from typing import List, Dict, Any
from datetime import datetime
from copy import deepcopy
# Memento - stores state snapshot
@dataclass(frozen=True)
class EditorMemento:
"""Immutable snapshot of editor state"""
_state: Dict[str, Any]
_timestamp: datetime
@property
def timestamp(self) -> datetime:
return self._timestamp
@property
def description(self) -> str:
return f"Snapshot at {self._timestamp.strftime('%H:%M:%S')}"
def get_state(self) -> Dict[str, Any]:
"""Only originator should call this"""
return deepcopy(self._state)
# Originator - creates and restores from mementos
class TextEditor:
def __init__(self):
self._content: str = ""
self._cursor_position: int = 0
self._selection_start: int | None = None
self._selection_end: int | None = None
self._font: str = "Arial"
self._font_size: int = 12
def type_text(self, text: str) -> None:
before = self._content[:self._cursor_position]
after = self._content[self._cursor_position:]
self._content = before + text + after
self._cursor_position += len(text)
def delete(self, count: int = 1) -> None:
if self._cursor_position > 0:
delete_from = max(0, self._cursor_position - count)
self._content = self._content[:delete_from] + self._content[self._cursor_position:]
self._cursor_position = delete_from
def set_font(self, font: str, size: int) -> None:
self._font = font
self._font_size = size
def move_cursor(self, position: int) -> None:
self._cursor_position = max(0, min(position, len(self._content)))
def save(self) -> EditorMemento:
"""Create memento with current state"""
state = {
"content": self._content,
"cursor_position": self._cursor_position,
"selection_start": self._selection_start,
"selection_end": self._selection_end,
"font": self._font,
"font_size": self._font_size,
}
return EditorMemento(state, datetime.now())
def restore(self, memento: EditorMemento) -> None:
"""Restore state from memento"""
state = memento.get_state()
self._content = state["content"]
self._cursor_position = state["cursor_position"]
self._selection_start = state["selection_start"]
self._selection_end = state["selection_end"]
self._font = state["font"]
self._font_size = state["font_size"]
def __str__(self) -> str:
cursor_line = " " * self._cursor_position + "|"
return f"Content: '{self._content}'\n {cursor_line} (cursor at {self._cursor_position})\nFont: {self._font} {self._font_size}pt"
# Caretaker - manages memento history
class EditorHistory:
def __init__(self, editor: TextEditor):
self._editor = editor
self._undo_stack: List[EditorMemento] = []
self._redo_stack: List[EditorMemento] = []
# Save initial state
self._undo_stack.append(editor.save())
def save(self) -> None:
"""Save current state for undo"""
self._undo_stack.append(self._editor.save())
self._redo_stack.clear() # Clear redo on new action
def undo(self) -> bool:
if len(self._undo_stack) > 1: # Keep at least initial state
current = self._undo_stack.pop()
self._redo_stack.append(current)
self._editor.restore(self._undo_stack[-1])
return True
return False
def redo(self) -> bool:
if self._redo_stack:
memento = self._redo_stack.pop()
self._undo_stack.append(memento)
self._editor.restore(memento)
return True
return False
def show_history(self) -> None:
print(f"Undo stack: {len(self._undo_stack)} states")
print(f"Redo stack: {len(self._redo_stack)} states")
# Usage
print("=== Text Editor with Undo/Redo ===\n")
editor = TextEditor()
history = EditorHistory(editor)
# Type some text
editor.type_text("Hello")
history.save()
print("After typing 'Hello':")
print(editor)
editor.type_text(" World")
history.save()
print("\nAfter typing ' World':")
print(editor)
editor.set_font("Times New Roman", 14)
history.save()
print("\nAfter changing font:")
print(editor)
editor.type_text("!")
history.save()
print("\nAfter typing '!':")
print(editor)
# Undo operations
print("\n--- Undo Operations ---")
history.undo()
print("After undo:")
print(editor)
history.undo()
print("\nAfter another undo:")
print(editor)
# Redo
print("\n--- Redo Operation ---")
history.redo()
print("After redo:")
print(editor)
history.show_history()
- Characteristics: State snapshot, encapsulation preserved, originator-controlled restoration, caretaker stores but doesn't examine
- Advantages: Provides state rollback without exposing internals, clean separation of concerns, supports multiple undo levels
- Disadvantages: Can be memory-intensive with frequent snapshots, may require deep copying, caretaker lifetime management
- Use cases: Undo/redo functionality, database transactions, game save states, configuration snapshots, version control
19. Observer Pattern¶
Intent: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Real-world analogy: A newspaper subscription. You (observer) subscribe to a newspaper (subject). When a new edition is published, all subscribers automatically receive it. You can subscribe or unsubscribe at any time without affecting other subscribers.
How it works: The Subject maintains a list of observers and notifies them of state changes. Observers register interest with subjects and implement an update method. This creates a loosely coupled relationship where subjects and observers can vary independently.
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any) -> None:
pass
# Subject interface
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer) -> None:
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
pass
@abstractmethod
def notify(self, event: str, data: Any) -> None:
pass
# Concrete Subject - Stock Market
@dataclass
class StockPrice:
symbol: str
price: float
change: float
change_percent: float
class StockMarket(Subject):
def __init__(self):
self._observers: Dict[str, List[Observer]] = {}
self._stocks: Dict[str, StockPrice] = {}
def attach(self, observer: Observer, event: str = "*") -> None:
"""Subscribe to specific event or all events (*)"""
if event not in self._observers:
self._observers[event] = []
if observer not in self._observers[event]:
self._observers[event].append(observer)
def detach(self, observer: Observer, event: str = "*") -> None:
if event in self._observers and observer in self._observers[event]:
self._observers[event].remove(observer)
def notify(self, event: str, data: Any) -> None:
# Notify specific event subscribers
if event in self._observers:
for observer in self._observers[event]:
observer.update(event, data)
# Notify wildcard subscribers
if "*" in self._observers:
for observer in self._observers["*"]:
observer.update(event, data)
def update_stock(self, symbol: str, new_price: float) -> None:
old_price = self._stocks.get(symbol, StockPrice(symbol, new_price, 0, 0)).price
change = new_price - old_price
change_percent = (change / old_price * 100) if old_price > 0 else 0
stock = StockPrice(symbol, new_price, change, change_percent)
self._stocks[symbol] = stock
# Notify with appropriate events
self.notify(f"price_update:{symbol}", stock)
if change_percent >= 5:
self.notify("significant_gain", stock)
elif change_percent <= -5:
self.notify("significant_loss", stock)
# Concrete Observers
class StockTicker(Observer):
"""Displays real-time stock prices"""
def __init__(self, name: str):
self._name = name
def update(self, event: str, data: StockPrice) -> None:
direction = "📈" if data.change >= 0 else "📉"
print(f"[{self._name}] {direction} {data.symbol}: ${data.price:.2f} "
f"({data.change:+.2f}, {data.change_percent:+.1f}%)")
class AlertSystem(Observer):
"""Sends alerts for significant market movements"""
def update(self, event: str, data: StockPrice) -> None:
if event == "significant_gain":
print(f"🚨 ALERT: {data.symbol} surged {data.change_percent:+.1f}%!")
elif event == "significant_loss":
print(f"🚨 ALERT: {data.symbol} dropped {data.change_percent:+.1f}%!")
class PortfolioTracker(Observer):
"""Tracks specific stocks in a portfolio"""
def __init__(self, holdings: Dict[str, int]):
self._holdings = holdings # symbol -> shares
def update(self, event: str, data: StockPrice) -> None:
if data.symbol in self._holdings:
shares = self._holdings[data.symbol]
value = shares * data.price
daily_change = shares * data.change
print(f"📊 Portfolio: {shares} shares of {data.symbol} = ${value:.2f} "
f"(today: {daily_change:+.2f})")
class TradingBot(Observer):
"""Automated trading based on price movements"""
def __init__(self, threshold: float = 3.0):
self._threshold = threshold
def update(self, event: str, data: StockPrice) -> None:
if abs(data.change_percent) >= self._threshold:
action = "BUY" if data.change_percent < 0 else "SELL"
print(f"🤖 TradingBot: {action} signal for {data.symbol} "
f"(change: {data.change_percent:+.1f}%)")
# Usage
print("=== Stock Market Observer Demo ===\n")
market = StockMarket()
# Create observers
ticker = StockTicker("Main Display")
alerts = AlertSystem()
portfolio = PortfolioTracker({"AAPL": 100, "GOOGL": 50})
bot = TradingBot(threshold=3.0)
# Subscribe to events
market.attach(ticker, "*") # All updates
market.attach(alerts, "significant_gain")
market.attach(alerts, "significant_loss")
market.attach(portfolio, "price_update:AAPL")
market.attach(portfolio, "price_update:GOOGL")
market.attach(bot, "*")
# Simulate market activity
print("--- Market Opens ---")
market.update_stock("AAPL", 150.00)
market.update_stock("GOOGL", 140.00)
market.update_stock("MSFT", 380.00)
print("\n--- Price Changes ---")
market.update_stock("AAPL", 154.50) # +3% gain
market.update_stock("GOOGL", 131.60) # -6% loss (triggers alert)
market.update_stock("MSFT", 385.00) # small change
print("\n--- Unsubscribe ticker ---")
market.detach(ticker, "*")
market.update_stock("AAPL", 158.00) # Ticker won't show this
- Characteristics: One-to-many dependency, automatic notification, loose coupling, dynamic subscription
- Advantages: Open/Closed Principle compliant, supports broadcast communication, runtime relationship changes
- Disadvantages: Unexpected updates possible, memory leaks if not unsubscribed, update order not guaranteed, can cause cascading updates
- Use cases: Event handling systems, GUI frameworks, data binding, notification services, pub/sub messaging, reactive programming
20. State Pattern¶
Intent: Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
Real-world analogy: A vending machine behaves differently based on its state: waiting for money, has money inserted, dispensing product, or out of stock. Each state changes how the machine responds to the same buttons.
How it works: State-specific behavior is encapsulated in separate state classes. The context object holds a reference to its current state and delegates behavior to it. State transitions are handled either by the context or by state objects themselves.
from abc import ABC, abstractmethod
from typing import Optional
# State interface
class DocumentState(ABC):
@abstractmethod
def edit(self, doc: "Document") -> None:
pass
@abstractmethod
def review(self, doc: "Document") -> None:
pass
@abstractmethod
def approve(self, doc: "Document") -> None:
pass
@abstractmethod
def reject(self, doc: "Document") -> None:
pass
@abstractmethod
def publish(self, doc: "Document") -> None:
pass
@property
@abstractmethod
def name(self) -> str:
pass
# Concrete States
class DraftState(DocumentState):
@property
def name(self) -> str:
return "Draft"
def edit(self, doc: "Document") -> None:
print(f"✏️ Editing document '{doc.title}'...")
doc.content += " [edited]"
def review(self, doc: "Document") -> None:
if len(doc.content) > 10:
print(f"📝 Submitting '{doc.title}' for review...")
doc.change_state(PendingReviewState())
else:
print("❌ Document too short to submit for review")
def approve(self, doc: "Document") -> None:
print("❌ Cannot approve: document is still a draft")
def reject(self, doc: "Document") -> None:
print("❌ Cannot reject: document is still a draft")
def publish(self, doc: "Document") -> None:
print("❌ Cannot publish: document must be approved first")
class PendingReviewState(DocumentState):
@property
def name(self) -> str:
return "Pending Review"
def edit(self, doc: "Document") -> None:
print("❌ Cannot edit: document is under review")
def review(self, doc: "Document") -> None:
print("ℹ️ Document is already under review")
def approve(self, doc: "Document") -> None:
print(f"✅ Document '{doc.title}' approved!")
doc.change_state(ApprovedState())
def reject(self, doc: "Document") -> None:
print(f"🔙 Document '{doc.title}' rejected, returning to draft...")
doc.change_state(DraftState())
def publish(self, doc: "Document") -> None:
print("❌ Cannot publish: document must be approved first")
class ApprovedState(DocumentState):
@property
def name(self) -> str:
return "Approved"
def edit(self, doc: "Document") -> None:
print(f"⚠️ Editing approved document - returning to draft...")
doc.content += " [edited]"
doc.change_state(DraftState())
def review(self, doc: "Document") -> None:
print("ℹ️ Document already reviewed and approved")
def approve(self, doc: "Document") -> None:
print("ℹ️ Document is already approved")
def reject(self, doc: "Document") -> None:
print(f"🔙 Revoking approval, returning '{doc.title}' to draft...")
doc.change_state(DraftState())
def publish(self, doc: "Document") -> None:
print(f"🎉 Publishing '{doc.title}'!")
doc.change_state(PublishedState())
class PublishedState(DocumentState):
@property
def name(self) -> str:
return "Published"
def edit(self, doc: "Document") -> None:
print("❌ Cannot edit: document is published. Create a new version instead.")
def review(self, doc: "Document") -> None:
print("ℹ️ Document is already published")
def approve(self, doc: "Document") -> None:
print("ℹ️ Document is already published")
def reject(self, doc: "Document") -> None:
print(f"📥 Unpublishing '{doc.title}'...")
doc.change_state(DraftState())
def publish(self, doc: "Document") -> None:
print("ℹ️ Document is already published")
# Context
class Document:
def __init__(self, title: str, content: str = ""):
self.title = title
self.content = content
self._state: DocumentState = DraftState()
def change_state(self, state: DocumentState) -> None:
print(f" State: {self._state.name} → {state.name}")
self._state = state
@property
def state_name(self) -> str:
return self._state.name
def edit(self) -> None:
self._state.edit(self)
def submit_for_review(self) -> None:
self._state.review(self)
def approve(self) -> None:
self._state.approve(self)
def reject(self) -> None:
self._state.reject(self)
def publish(self) -> None:
self._state.publish(self)
def __str__(self) -> str:
return f"Document('{self.title}', state={self.state_name}, content='{self.content[:30]}...')"
# Usage
print("=== Document Workflow Demo ===\n")
doc = Document("Design Patterns Guide", "Initial content about design patterns")
print(f"Created: {doc}\n")
# Try to publish directly (should fail)
print("--- Attempt to publish draft ---")
doc.publish()
# Normal workflow
print("\n--- Edit and submit for review ---")
doc.edit()
doc.submit_for_review()
# Try to edit while under review
print("\n--- Attempt to edit under review ---")
doc.edit()
# Reject and re-edit
print("\n--- Reject and revise ---")
doc.reject()
doc.edit()
doc.submit_for_review()
# Approve and publish
print("\n--- Approve and publish ---")
doc.approve()
doc.publish()
# Try to edit published document
print("\n--- Attempt to edit published ---")
doc.edit()
print(f"\nFinal: {doc}")
- Characteristics: State objects encapsulate behavior, context delegates to current state, state transitions explicit, eliminates conditionals
- Advantages: Localizes state-specific behavior, makes state transitions explicit, eliminates bulky conditionals, follows Open/Closed Principle
- Disadvantages: Can lead to many state classes, state transitions can be complex, overkill for simple state machines
- Use cases: Workflow management, UI element states, game character behavior, connection states, order processing, vending machines
21. Strategy Pattern¶
Intent: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Real-world analogy: Transportation to the airport—you can take a taxi, bus, or bike. Each is a different "strategy" with the same goal (get to airport) but different trade-offs (cost, speed, convenience). You choose the strategy based on context.
How it works: The pattern extracts algorithms into separate strategy classes with a common interface. The context holds a reference to a strategy and delegates the work to it. Strategies can be swapped at runtime without changing the context.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
from enum import Enum
# Strategy interface
class PricingStrategy(ABC):
@abstractmethod
def calculate_price(self, base_price: float, quantity: int) -> float:
pass
@property
@abstractmethod
def name(self) -> str:
pass
# Concrete strategies
class RegularPricing(PricingStrategy):
@property
def name(self) -> str:
return "Regular"
def calculate_price(self, base_price: float, quantity: int) -> float:
return base_price * quantity
class BulkDiscountPricing(PricingStrategy):
"""10% discount for 10+ items, 20% for 50+"""
@property
def name(self) -> str:
return "Bulk Discount"
def calculate_price(self, base_price: float, quantity: int) -> float:
total = base_price * quantity
if quantity >= 50:
return total * 0.80 # 20% off
elif quantity >= 10:
return total * 0.90 # 10% off
return total
class HappyHourPricing(PricingStrategy):
"""50% off during happy hour"""
@property
def name(self) -> str:
return "Happy Hour"
def calculate_price(self, base_price: float, quantity: int) -> float:
return base_price * quantity * 0.50
class LoyaltyPricing(PricingStrategy):
"""Discount based on loyalty points"""
def __init__(self, loyalty_points: int):
self._points = loyalty_points
@property
def name(self) -> str:
return f"Loyalty ({self._points} pts)"
def calculate_price(self, base_price: float, quantity: int) -> float:
total = base_price * quantity
# 1% discount per 100 points, max 30%
discount_percent = min(self._points // 100, 30)
return total * (1 - discount_percent / 100)
class PromotionalPricing(PricingStrategy):
"""Buy N, get M free"""
def __init__(self, buy: int, get_free: int):
self._buy = buy
self._free = get_free
@property
def name(self) -> str:
return f"Buy {self._buy} Get {self._free} Free"
def calculate_price(self, base_price: float, quantity: int) -> float:
sets = quantity // (self._buy + self._free)
remainder = quantity % (self._buy + self._free)
paid_items = sets * self._buy + min(remainder, self._buy)
return base_price * paid_items
# Context
@dataclass
class CartItem:
name: str
base_price: float
quantity: int
class ShoppingCart:
def __init__(self):
self._items: List[CartItem] = []
self._pricing_strategy: PricingStrategy = RegularPricing()
def set_pricing_strategy(self, strategy: PricingStrategy) -> None:
print(f"💰 Pricing strategy changed to: {strategy.name}")
self._pricing_strategy = strategy
def add_item(self, name: str, price: float, quantity: int) -> None:
self._items.append(CartItem(name, price, quantity))
def calculate_total(self) -> float:
return sum(
self._pricing_strategy.calculate_price(item.base_price, item.quantity)
for item in self._items
)
def display(self) -> None:
print(f"\n{'='*50}")
print(f"Shopping Cart (Strategy: {self._pricing_strategy.name})")
print('='*50)
for item in self._items:
item_price = self._pricing_strategy.calculate_price(
item.base_price, item.quantity
)
print(f" {item.name}: {item.quantity} x ${item.base_price:.2f} = ${item_price:.2f}")
print(f"{'─'*50}")
print(f" TOTAL: ${self.calculate_total():.2f}")
print()
# Usage
print("=== Pricing Strategy Demo ===")
cart = ShoppingCart()
cart.add_item("Widget", 10.00, 5)
cart.add_item("Gadget", 25.00, 3)
cart.add_item("Gizmo", 15.00, 12)
# Regular pricing
cart.display()
# Switch to bulk discount
cart.set_pricing_strategy(BulkDiscountPricing())
cart.display()
# Happy hour!
cart.set_pricing_strategy(HappyHourPricing())
cart.display()
# Loyalty customer
cart.set_pricing_strategy(LoyaltyPricing(loyalty_points=1500))
cart.display()
# Promotional pricing
cart.set_pricing_strategy(PromotionalPricing(buy=2, get_free=1))
cart.display()
# Strategies can be selected based on context
print("--- Dynamic Strategy Selection ---")
import datetime
current_hour = datetime.datetime.now().hour
if 16 <= current_hour <= 18:
cart.set_pricing_strategy(HappyHourPricing())
else:
cart.set_pricing_strategy(RegularPricing())
cart.display()
- Characteristics: Encapsulated algorithms, interchangeable strategies, composition over inheritance, runtime algorithm selection
- Advantages: Algorithms vary independently, eliminates conditional statements, easy to add new strategies, follows Open/Closed Principle
- Disadvantages: Clients must be aware of different strategies, increased number of objects, can be overkill for few algorithms
- Use cases: Sorting algorithms, payment processing, compression algorithms, route calculation, validation rules, authentication methods
22. Template Method Pattern¶
Intent: Defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure.
Real-world analogy: A recipe template—all cookies follow the same steps (mix ingredients, form dough, bake, cool), but different cookies override specific steps (chocolate chip adds chips, oatmeal adds oats). The overall process remains the same.
How it works: The base class defines a template method that outlines the algorithm's steps, calling abstract or hook methods. Subclasses override these methods to customize behavior while the algorithm structure stays fixed in the base class.
from abc import ABC, abstractmethod
from typing import Dict, Any
import json
# Abstract class with template method
class DataExporter(ABC):
"""Template for exporting data to various formats"""
def export(self, data: list[Dict[str, Any]], filename: str) -> None:
"""
Template method - defines the algorithm skeleton.
Subclasses customize by overriding hook methods.
"""
print(f"\n{'='*50}")
print(f"Exporting to {filename}")
print('='*50)
# Step 1: Validate data (can be overridden)
if not self.validate(data):
print("❌ Validation failed, aborting export")
return
# Step 2: Transform data (can be overridden)
transformed = self.transform(data)
# Step 3: Format data (must be implemented by subclasses)
formatted = self.format_data(transformed)
# Step 4: Add header (hook - optional override)
header = self.create_header(data)
# Step 5: Add footer (hook - optional override)
footer = self.create_footer(data)
# Step 6: Combine and write
content = self.combine(header, formatted, footer)
self.write_file(filename, content)
# Step 7: Post-processing hook
self.post_process(filename)
print(f"✅ Export complete: {filename}")
# Hooks with default implementations (can be overridden)
def validate(self, data: list[Dict[str, Any]]) -> bool:
"""Hook: validates data before processing"""
print(" Validating data...")
return len(data) > 0
def transform(self, data: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
"""Hook: transforms data before formatting"""
print(" Transforming data...")
return data
def create_header(self, data: list[Dict[str, Any]]) -> str:
"""Hook: creates optional header"""
return ""
def create_footer(self, data: list[Dict[str, Any]]) -> str:
"""Hook: creates optional footer"""
return ""
def combine(self, header: str, body: str, footer: str) -> str:
"""Hook: combines parts into final content"""
parts = [p for p in [header, body, footer] if p]
return "\n".join(parts)
def write_file(self, filename: str, content: str) -> None:
"""Hook: writes content to file"""
print(f" Writing to {filename}...")
# In real implementation: write to actual file
print(f" Content preview:\n{content[:200]}...")
def post_process(self, filename: str) -> None:
"""Hook: optional post-processing"""
pass
# Abstract method - must be implemented by subclasses
@abstractmethod
def format_data(self, data: list[Dict[str, Any]]) -> str:
"""Format data to specific output format"""
pass
# Concrete implementations
class CSVExporter(DataExporter):
"""Exports data to CSV format"""
def format_data(self, data: list[Dict[str, Any]]) -> str:
print(" Formatting as CSV...")
if not data:
return ""
headers = ",".join(data[0].keys())
rows = [",".join(str(v) for v in row.values()) for row in data]
return "\n".join([headers] + rows)
def create_header(self, data: list[Dict[str, Any]]) -> str:
return f"# Exported {len(data)} records"
class JSONExporter(DataExporter):
"""Exports data to JSON format"""
def __init__(self, pretty: bool = True):
self._pretty = pretty
def format_data(self, data: list[Dict[str, Any]]) -> str:
print(" Formatting as JSON...")
if self._pretty:
return json.dumps(data, indent=2)
return json.dumps(data)
class XMLExporter(DataExporter):
"""Exports data to XML format"""
def format_data(self, data: list[Dict[str, Any]]) -> str:
print(" Formatting as XML...")
lines = ['<?xml version="1.0" encoding="UTF-8"?>', "<records>"]
for record in data:
lines.append(" <record>")
for key, value in record.items():
lines.append(f" <{key}>{value}</{key}>")
lines.append(" </record>")
lines.append("</records>")
return "\n".join(lines)
def create_header(self, data: list[Dict[str, Any]]) -> str:
return f"<!-- Generated: {len(data)} records -->"
class HTMLExporter(DataExporter):
"""Exports data to HTML table format"""
def validate(self, data: list[Dict[str, Any]]) -> bool:
# Custom validation - also check for consistent keys
if not super().validate(data):
return False
if len(data) > 1:
keys = set(data[0].keys())
return all(set(row.keys()) == keys for row in data)
return True
def transform(self, data: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
# Escape HTML entities
print(" Escaping HTML entities...")
transformed = []
for row in data:
transformed.append({
k: str(v).replace("<", "<").replace(">", ">")
for k, v in row.items()
})
return transformed
def format_data(self, data: list[Dict[str, Any]]) -> str:
print(" Formatting as HTML table...")
if not data:
return "<table></table>"
headers = "".join(f"<th>{h}</th>" for h in data[0].keys())
rows = []
for record in data:
cells = "".join(f"<td>{v}</td>" for v in record.values())
rows.append(f" <tr>{cells}</tr>")
return f"<table>\n <tr>{headers}</tr>\n" + "\n".join(rows) + "\n</table>"
def create_header(self, data: list[Dict[str, Any]]) -> str:
return "<html>\n<body>"
def create_footer(self, data: list[Dict[str, Any]]) -> str:
return "</body>\n</html>"
def post_process(self, filename: str) -> None:
print(f" Opening {filename} in browser...")
# Usage
sample_data = [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
{"id": 3, "name": "Charlie", "email": "charlie@example.com"},
]
print("=== Template Method Demo: Data Exporters ===")
# Same data, different formats using same algorithm structure
exporters = [
CSVExporter(),
JSONExporter(pretty=True),
XMLExporter(),
HTMLExporter(),
]
for exporter in exporters:
exporter.export(sample_data, f"output.{exporter.__class__.__name__[:3].lower()}")
- Characteristics: Algorithm skeleton in base class, hooks and abstract methods for customization, inheritance-based, inversion of control
- Advantages: Promotes code reuse, enforces algorithm structure, localizes common behavior, follows Hollywood Principle ("don't call us, we'll call you")
- Disadvantages: Limits flexibility (inheritance), subclasses tightly coupled to base, can lead to complex hierarchies
- Use cases: Data processing pipelines, report generation, build processes, test frameworks, game loops, parsing templates
23. Visitor Pattern¶
Intent: Separates algorithms from the objects on which they operate, allowing new operations to be added without modifying the object structure.
Real-world analogy: A building inspector visits different rooms (kitchen, bedroom, bathroom) and performs inspections. Each room accepts the visitor and lets them do their job. Adding a new type of inspection (fire safety, energy audit) doesn't require changing the room classes.
How it works: Objects in a structure implement an accept(visitor) method that calls back to the visitor. The visitor has a visit method for each type of object. This double-dispatch mechanism lets you add new operations by creating new visitor classes.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
# Element interface
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor: "DocumentVisitor") -> None:
pass
# Concrete elements
@dataclass
class Heading(DocumentElement):
level: int
text: str
def accept(self, visitor: "DocumentVisitor") -> None:
visitor.visit_heading(self)
@dataclass
class Paragraph(DocumentElement):
text: str
def accept(self, visitor: "DocumentVisitor") -> None:
visitor.visit_paragraph(self)
@dataclass
class Image(DocumentElement):
src: str
alt: str
width: int
height: int
def accept(self, visitor: "DocumentVisitor") -> None:
visitor.visit_image(self)
@dataclass
class CodeBlock(DocumentElement):
code: str
language: str
def accept(self, visitor: "DocumentVisitor") -> None:
visitor.visit_code_block(self)
@dataclass
class Link(DocumentElement):
url: str
text: str
def accept(self, visitor: "DocumentVisitor") -> None:
visitor.visit_link(self)
# Document (composite structure)
class Document:
def __init__(self, title: str):
self.title = title
self._elements: List[DocumentElement] = []
def add(self, element: DocumentElement) -> None:
self._elements.append(element)
def accept(self, visitor: "DocumentVisitor") -> None:
for element in self._elements:
element.accept(visitor)
# Visitor interface
class DocumentVisitor(ABC):
@abstractmethod
def visit_heading(self, heading: Heading) -> None:
pass
@abstractmethod
def visit_paragraph(self, paragraph: Paragraph) -> None:
pass
@abstractmethod
def visit_image(self, image: Image) -> None:
pass
@abstractmethod
def visit_code_block(self, code_block: CodeBlock) -> None:
pass
@abstractmethod
def visit_link(self, link: Link) -> None:
pass
# Concrete visitors - each implements a different operation
class HTMLExportVisitor(DocumentVisitor):
"""Exports document to HTML"""
def __init__(self):
self._html_parts: List[str] = []
def visit_heading(self, heading: Heading) -> None:
self._html_parts.append(f"<h{heading.level}>{heading.text}</h{heading.level}>")
def visit_paragraph(self, paragraph: Paragraph) -> None:
self._html_parts.append(f"<p>{paragraph.text}</p>")
def visit_image(self, image: Image) -> None:
self._html_parts.append(
f'<img src="{image.src}" alt="{image.alt}" '
f'width="{image.width}" height="{image.height}">'
)
def visit_code_block(self, code_block: CodeBlock) -> None:
self._html_parts.append(
f'<pre><code class="language-{code_block.language}">'
f'{code_block.code}</code></pre>'
)
def visit_link(self, link: Link) -> None:
self._html_parts.append(f'<a href="{link.url}">{link.text}</a>')
def get_html(self) -> str:
return "\n".join(self._html_parts)
class MarkdownExportVisitor(DocumentVisitor):
"""Exports document to Markdown"""
def __init__(self):
self._md_parts: List[str] = []
def visit_heading(self, heading: Heading) -> None:
self._md_parts.append(f"{'#' * heading.level} {heading.text}")
def visit_paragraph(self, paragraph: Paragraph) -> None:
self._md_parts.append(f"\n{paragraph.text}\n")
def visit_image(self, image: Image) -> None:
self._md_parts.append(f"")
def visit_code_block(self, code_block: CodeBlock) -> None:
self._md_parts.append(f"```{code_block.language}\n{code_block.code}\n```")
def visit_link(self, link: Link) -> None:
self._md_parts.append(f"[{link.text}]({link.url})")
def get_markdown(self) -> str:
return "\n".join(self._md_parts)
class WordCountVisitor(DocumentVisitor):
"""Counts words in document"""
def __init__(self):
self._word_count = 0
self._code_lines = 0
self._image_count = 0
self._link_count = 0
def visit_heading(self, heading: Heading) -> None:
self._word_count += len(heading.text.split())
def visit_paragraph(self, paragraph: Paragraph) -> None:
self._word_count += len(paragraph.text.split())
def visit_image(self, image: Image) -> None:
self._image_count += 1
self._word_count += len(image.alt.split())
def visit_code_block(self, code_block: CodeBlock) -> None:
self._code_lines += code_block.code.count('\n') + 1
def visit_link(self, link: Link) -> None:
self._link_count += 1
self._word_count += len(link.text.split())
def get_stats(self) -> dict:
return {
"words": self._word_count,
"code_lines": self._code_lines,
"images": self._image_count,
"links": self._link_count,
}
class SEOAnalyzerVisitor(DocumentVisitor):
"""Analyzes document for SEO issues"""
def __init__(self):
self._issues: List[str] = []
self._h1_count = 0
self._images_without_alt = 0
def visit_heading(self, heading: Heading) -> None:
if heading.level == 1:
self._h1_count += 1
if self._h1_count > 1:
self._issues.append("Multiple H1 headings found (should have exactly one)")
if len(heading.text) > 60:
self._issues.append(f"Heading too long: '{heading.text[:30]}...'")
def visit_paragraph(self, paragraph: Paragraph) -> None:
if len(paragraph.text) < 50:
self._issues.append("Short paragraph detected (consider expanding)")
def visit_image(self, image: Image) -> None:
if not image.alt or len(image.alt) < 5:
self._issues.append(f"Image missing proper alt text: {image.src}")
def visit_code_block(self, code_block: CodeBlock) -> None:
pass # Code blocks don't affect SEO
def visit_link(self, link: Link) -> None:
if link.text.lower() in ["click here", "read more", "link"]:
self._issues.append(f"Non-descriptive link text: '{link.text}'")
def get_issues(self) -> List[str]:
if self._h1_count == 0:
self._issues.insert(0, "Missing H1 heading")
return self._issues
# Usage
print("=== Visitor Pattern Demo: Document Processing ===\n")
# Create document structure
doc = Document("Design Patterns Tutorial")
doc.add(Heading(1, "Understanding the Visitor Pattern"))
doc.add(Paragraph("The Visitor pattern separates algorithms from object structures."))
doc.add(Heading(2, "Implementation Example"))
doc.add(CodeBlock("def accept(self, visitor):\n visitor.visit(self)", "python"))
doc.add(Image("visitor-uml.png", "UML diagram", 400, 300))
doc.add(Paragraph("See the full code on GitHub."))
doc.add(Link("https://github.com/example", "click here"))
# Apply different visitors to same document structure
print("--- HTML Export ---")
html_visitor = HTMLExportVisitor()
doc.accept(html_visitor)
print(html_visitor.get_html())
print("\n--- Markdown Export ---")
md_visitor = MarkdownExportVisitor()
doc.accept(md_visitor)
print(md_visitor.get_markdown())
print("\n--- Word Count Stats ---")
stats_visitor = WordCountVisitor()
doc.accept(stats_visitor)
for key, value in stats_visitor.get_stats().items():
print(f" {key}: {value}")
print("\n--- SEO Analysis ---")
seo_visitor = SEOAnalyzerVisitor()
doc.accept(seo_visitor)
issues = seo_visitor.get_issues()
if issues:
for issue in issues:
print(f" ⚠️ {issue}")
else:
print(" ✅ No SEO issues found")
- Characteristics: Double dispatch, separates operations from structure, accumulates state during traversal, operations in visitor classes
- Advantages: Easy to add new operations without modifying elements, gathers related operations in one class, can accumulate state across elements
- Disadvantages: Adding new element types requires updating all visitors, breaks encapsulation (visitors access element internals), can be complex
- Use cases: Compilers (AST traversal), document processing, serialization, reporting, validation across object hierarchies, static analysis tools