Skip to content

Behavioral Patterns: Strategy, Observer, Command

Behavioral patterns describe how objects talk to each other and share work. This note covers three classic patterns from the Gang of Four book.

Strategy

Idea: Replace a long chain of conditions (if / elif / match) with a set of interchangeable algorithms.

from abc import ABC, abstractmethod
from dataclasses import dataclass


class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list[int]) -> list[int]: ...


class QuickSort(SortStrategy):
    def sort(self, data: list[int]) -> list[int]:
        return sorted(data)  # simplified


class RadixSort(SortStrategy):
    def sort(self, data: list[int]) -> list[int]:
        return sorted(data)  # simplified


@dataclass
class Sorter:
    strategy: SortStrategy

    def sort(self, data: list[int]) -> list[int]:
        return self.strategy.sort(data)

Where it helps: Payment providers (Stripe, PayPal, bank transfer). Compression libraries. Report export (PDF, CSV, Excel). Shipping cost rules.

When to choose it: You have several ways to do the same job, and you want to pick the way at runtime without giant if trees.

Observer

Idea: One object (the subject) tells many listeners (observers) when something important happens.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class EventObserver(ABC):
    @abstractmethod
    def update(self, event: str, data: dict) -> None: ...


@dataclass
class EventBus:
    _observers: list[EventObserver] = field(default_factory=list)

    def subscribe(self, observer: EventObserver) -> None:
        self._observers.append(observer)

    def publish(self, event: str, data: dict) -> None:
        for obs in self._observers:
            obs.update(event, data)


class EmailObserver(EventObserver):
    def update(self, event: str, data: dict) -> None:
        if event == "order.placed":
            print(f"Email: order {data['order_id']} confirmation sent")


class AnalyticsObserver(EventObserver):
    def update(self, event: str, data: dict) -> None:
        print(f"Analytics: tracking {event} for order {data.get('order_id')}")


bus = EventBus()
bus.subscribe(EmailObserver())
bus.subscribe(AnalyticsObserver())
bus.publish("order.placed", {"order_id": "ORD-42"})

Where it helps: UI events (click, scroll). Domain events (order placed → email, stock, analytics). Reactive streams.

Risks: Forgetting to unsubscribe can leak memory. Do not depend on the order of update calls unless you define it yourself.

Command

Idea: Wrap a request in an object. You can queue it, log it, and often undo it.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class Command(ABC):
    @abstractmethod
    def execute(self) -> None: ...

    @abstractmethod
    def undo(self) -> None: ...


@dataclass
class TextEditor:
    _text: str = ""
    _history: list[Command] = field(default_factory=list)

    def run(self, cmd: Command) -> None:
        cmd.execute()
        self._history.append(cmd)

    def undo(self) -> None:
        if self._history:
            self._history.pop().undo()


@dataclass
class InsertText(Command):
    editor: TextEditor
    text: str

    def execute(self) -> None:
        self.editor._text += self.text

    def undo(self) -> None:
        self.editor._text = self.editor._text[: -len(self.text)]

Where it helps: Transaction logs. Task queues (each job is a command). Undo/redo in editors. Retries with full request data stored on the command object.

Note: Renamed execute on TextEditor to run so it does not clash with Command.execute.