Design Patterns: Strategy and Observer

Design Patterns: Strategy and Observer

Design patterns are reusable solutions to common software design problems. The Gang of Four book (1994) cataloged 23 design patterns, and many remain essential tools in modern software development. This article focuses on two of the most widely used behavioral patterns: Strategy and Observer. Both patterns promote loose coupling and adhere to the Open/Closed Principle—they make code extensible without modification.

The Strategy Pattern

The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable at runtime. Instead of writing a massive if-else chain to handle different behaviors, you define a strategy interface and implement concrete strategies for each variant. The context class delegates to a strategy object, allowing the algorithm to be swapped without changing the context code. This pattern is ideal for payment processing (credit card vs PayPal vs crypto), sorting algorithms, compression methods, and validation rules.

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool: ...

class CreditCardStrategy(PaymentStrategy):
    def __init__(self, card_number: str, cvv: str):
        self.card_number = card_number
        self.cvv = cvv
    def pay(self, amount: float) -> bool:
        print(f"Charging {amount} to card {self.card_number[-4:]}")
        return True

class PayPalStrategy(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email
    def pay(self, amount: float) -> bool:
        print(f"Charging {amount} via PayPal account {self.email}")
        return True

class ShoppingCart:
    def __init__(self, strategy: PaymentStrategy):
        self.items = []
        self.strategy = strategy
    def checkout(self) -> bool:
        total = sum(item.price for item in self.items)
        return self.strategy.pay(total)

# Usage
cart = ShoppingCart(PayPalStrategy("user@example.com"))
cart.checkout()  # Uses PayPal; can switch to CreditCardStrategy later

The Observer Pattern

The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are notified automatically. This is the foundation of event-driven programming and pub-sub systems. In Python, the Observer pattern is used extensively in GUI frameworks (button click events), Django signals, and asyncio event loops. Unlike Strategy (which is about algorithm selection), Observer is about notification and propagation of state changes.

class Subject:
    def __init__(self):
        self._observers = []
    def attach(self, observer):
        self._observers.append(observer)
    def detach(self, observer):
        self._observers.remove(observer)
    def notify(self, **kwargs):
        for observer in self._observers:
            observer.update(**kwargs)

class Observer(ABC):
    @abstractmethod
    def update(self, **kwargs): ...

class EmailNotifier(Observer):
    def update(self, **kwargs):
        print(f"Email: Order {kwargs.get('order_id')} is now {kwargs.get('status')}")

class Logger(Observer):
    def update(self, **kwargs):
        print(f"Log: Order {kwargs.get('order_id')} → {kwargs.get('status')} at {kwargs.get('timestamp')}")

order_system = Subject()
order_system.attach(EmailNotifier())
order_system.attach(Logger())
order_system.notify(order_id=123, status="shipped", timestamp="2026-07-09T10:00:00Z")

When to Use Each Pattern

Use Strategy when you need to select an algorithm at runtime and want to avoid conditionals, or when you have multiple variants of the same behavior that should be independently testable. Use Observer when a change in one object requires updating others, but you don’t know how many objects need updating ahead of time. Both patterns are often combined: an event system (Observer) can dispatch events to different handlers that each use a Strategy to process the event differently depending on its type.

Observer Pattern in Modern Frameworks

Modern frameworks have abstracted the Observer pattern into reactive programming libraries. RxPY (ReactiveX for Python) provides Observable streams that emit values over time, with operators for filtering, transforming, and combining streams. The Observer pattern is also the foundation of publish-subscribe systems like Redis Pub/Sub, Apache Kafka, and WebSocket-based event buses. In frontend frameworks like React, the virtual DOM diffing algorithm is essentially an Observer that re-renders components when their state changes. Understanding the raw Observer pattern helps you debug these higher-level abstractions when things go wrong—the same principles of subscription management, backpressure, and error propagation apply at every abstraction level.

from rx import from_list
from rx.operators import filter, map

numbers = from_list([1, 2, 3, 4, 5, 6])
numbers.pipe(
    filter(lambda x: x % 2 == 0),
    map(lambda x: x ** 2)
).subscribe(
    on_next=lambda x: print(f"Got: {x}"),
    on_error=lambda e: print(f"Error: {e}"),
    on_completed=lambda: print("Done!")
)

State Pattern as a Strategy Variant

The State pattern is closely related to Strategy but with a key difference: in Strategy, the client chooses and sets the strategy; in State, the object’s internal state determines its behavior automatically. A document editor has states (Draft, Review, Published) that determine which operations are allowed. State transitions are defined in the state machine. The State pattern is implemented identically to Strategy at the code level but differs in intent. In game development, state machines control character behavior, AI decision making, and UI screens. Adding a new state requires creating a new class without modifying existing states or the context.

Strategy Pattern in Functional Programming

In functional programming languages, the Strategy pattern becomes trivial: strategies are just functions passed as arguments. Instead of defining a Strategy interface and concrete classes, you pass a function directly. Python’s first-class functions make this natural: sort(key=len) passes a strategy for extracting sort keys. The strategy could be a lambda, a named function, or a callable class. The functools.partial function creates pre-configured strategies by binding some arguments. Libraries like attrs and dataclasses with field(validator=…) use this pattern for validation strategies. In Java, functional interfaces and lambda expressions (added in Java 8) eliminated the boilerplate of anonymous Strategy classes. The modern version of the Strategy pattern is dependency injection: your function or class receives its strategy as a parameter instead of implementing a fixed behavior.

Leave a Reply

Your email address will not be published. Required fields are marked *