86.4 Behavioral Patterns: Observer, Strategy, Command, State, Template Method
Right, let’s get into the good stuff. Behavioral patterns are where we stop just building structures and start giving them actual brains. They’re about how objects talk to each other, who’s responsible for what, and how you manage complex flows of control without creating a spaghetti-code monster. These patterns are the difference between a codebase that works and one you can actually change without having a full-blown existential crisis.
Observer: Stop Manually Checking, Start Getting Updates
Ever found yourself writing a while True: loop that just checks and re-checks if some value has changed yet? You’re better than that. The Observer pattern is your way out. It sets up a one-to-many relationship: when one object (the “subject”) changes state, all its dependents (“observers”) are notified and updated automatically. It’s the core of event-driven programming.
Think of it like subscribing to a newsletter. You don’t refresh the news website every five minutes; you give them your email, and they send you the news when it’s ready. The subject is the newsletter company, and you are the observer.
Here’s how we do it without overcomplicating things. Notice I’m using abc for the base classes. This isn’t just pedantic OOP—it forces a clean contract.
from abc import ABC, abstractmethod
class Subject(ABC):
"""The one who has the news."""
@abstractmethod
def attach(self, observer):
"""Subscribe an observer."""
pass
@abstractmethod
def detach(self, observer):
"""Unsubscribe an observer."""
pass
@abstractmethod
def notify(self):
"""Send out the update to all subscribers."""
pass
class ConcreteSubject(Subject):
"""The actual thing you're watching."""
def __init__(self):
self._observers = set()
self._state = None # This is the valuable data
def attach(self, observer):
self._observers.add(observer)
def detach(self, observer):
self._observers.discard(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify() # This is the magic. State changes? Everyone knows.
class Observer(ABC):
"""The subscriber."""
@abstractmethod
def update(self, subject):
"""Receive the update. The subject is passed for context."""
pass
class ConcreteObserver(Observer):
"""An actual subscriber who does something with the news."""
def update(self, subject):
print(f"Observer got the news! New state is: {subject.state}")
# Let's see it in action
if __name__ == "__main__":
subject = ConcreteSubject()
observer_a = ConcreteObserver()
observer_b = ConcreteObserver()
subject.attach(observer_a)
subject.attach(observer_b)
subject.state = "First Update" # Both observers will print
subject.detach(observer_b)
subject.state = "Second Update" # Only observer_a prints
Pitfall Alert: The order of notification is non-deterministic. If your observers have dependencies on each other, you’ve just created a race condition. Also, a poorly written observer that throws an exception can break the entire notification chain. Always wrap your observer.update() calls in try-catch blocks in a real system.
Strategy: Swap Algorithms Like Socks
This pattern is so stupidly simple and useful I’m almost angry I didn’t think of it first. The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the client that uses it.
Translation: Stop using mile-long if/elif/else chains to pick behavior. Just define a common interface for your strategies and pass the one you want to the client.
from abc import ABC, abstractmethod
from typing import List
class SortStrategy(ABC):
"""The interface that all sorting strategies must implement."""
@abstractmethod
def sort(self, data: List) -> List:
pass
class QuickSortStrategy(SortStrategy):
def sort(self, data):
# Let's pretend this is a real quicksort. You get the idea.
return sorted(data) # Python's sorted is Timsort, but for demo...
class ReverseSortStrategy(SortStrategy):
def sort(self, data):
return sorted(data, reverse=True)
class Sorter:
"""The Context class. It doesn't care which strategy it uses."""
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
"""Allows changing the strategy at runtime."""
self._strategy = strategy
def execute_sort(self, data):
"""Delegates the work to the strategy object."""
return self._strategy.sort(data)
# Usage
data = [5, 2, 8, 1]
sorter = Sorter(QuickSortStrategy())
print(sorter.execute_sort(data)) # [1, 2, 5, 8]
sorter.set_strategy(ReverseSortStrategy())
print(sorter.execute_sort(data)) # [8, 5, 2, 1]
Why this rules: You’ve just closed the Sorter class for modification (Open/Closed Principle). To add a new sorting algorithm, you create a new class. You don’t touch a single line of the Sorter code. This is how you avoid creating a 5000-line God class full of switches.
Command: Encapsulate All The Things
The Command pattern turns a request into a stand-alone object that contains all information about the request. This lets you parameterize methods with different requests, delay or queue a request’s execution, and support undoable operations.
It’s the patron saint of “I’ll get to it later.” You’re essentially creating a to-do list of command objects that can be executed whenever you feel like it.
from abc import ABC, abstractmethod
class Command(ABC):
"""The interface for all commands."""
@abstractmethod
def execute(self):
pass
# Optional but crucial for undo
@abstractmethod
def undo(self):
pass
class LightOnCommand(Command):
"""A concrete command to turn a light on."""
def __init__(self, light):
self.light = light
self.previous_state = None
def execute(self):
self.previous_state = self.light.state
self.light.turn_on()
def undo(self):
if self.previous_state == "on":
self.light.turn_on()
else:
self.light.turn_off()
class Light:
"""The Receiver class that knows how to do the actual work."""
def __init__(self):
self.state = "off"
def turn_on(self):
self.state = "on"
print("Light is ON")
def turn_off(self):
self.state = "off"
print("Light is OFF")
class RemoteControl:
"""The Invoker class. It holds a command and triggers it."""
def __init__(self):
self._command = None
self.history = [] # For undo functionality
def set_command(self, command):
self._command = command
def press_button(self):
if self._command:
self._command.execute()
self.history.append(self._command)
def press_undo(self):
if self.history:
last_command = self.history.pop()
last_command.undo()
# Usage
light = Light()
remote = RemoteControl()
on_command = LightOnCommand(light)
remote.set_command(on_command)
remote.press_button() # Light is ON
remote.press_undo() # Light is OFF
The real power here is the Invoker (the remote) knows nothing about the Receiver (the light). It just knows how to call execute(). This decoupling is what allows you to build complex macro systems, queues, and undo stacks.
State: If Elif Elif Elif Elif…
You’ve seen it: a class with a state attribute and a method that’s just a horrifying chain of if self.state == 'A': ... elif self.state == 'B': .... The State pattern is the antidote. It allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
You represent each state as a separate class, and the context object delegates all state-specific behavior to the current state object.
from abc import ABC, abstractmethod
class VendingMachineState(ABC):
"""Abstract state."""
@abstractmethod
def insert_money(self, amount):
pass
@abstractmethod
def select_drink(self, drink):
pass
@abstractmethod
def dispense(self):
pass
class IdleState(VendingMachineState):
def __init__(self, machine):
self.machine = machine
def insert_money(self, amount):
print(f"${amount} inserted.")
self.machine.balance = amount
self.machine.set_state(HasMoneyState(self.machine))
def select_drink(self, drink):
print("Please insert money first.")
def dispense(self):
print("Please insert money first.")
class HasMoneyState(VendingMachineState):
def __init__(self, machine):
self.machine = machine
def insert_money(self, amount):
self.machine.balance += amount
print(f"Balance: ${self.machine.balance}")
def select_drink(self, drink):
if drink.price <= self.machine.balance:
print(f"Selected {drink.name}")
self.machine.selected_drink = drink
self.machine.set_state(DispenseState(self.machine))
else:
print("Not enough money.")
def dispense(self):
print("Please select a drink first.")
class DispenseState(VendingMachineState):
def __init__(self, machine):
self.machine = machine
def insert_money(self, amount):
print("Please wait, dispensing your drink.")
def select_drink(self, drink):
print("Please wait, dispensing your drink.")
def dispense(self):
print(f"Dispensing {self.machine.selected_drink.name}... Enjoy!")
self.machine.balance -= self.machine.selected_drink.price
# Give change, then return to idle
self.machine.set_state(IdleState(self.machine))
class VendingMachine:
"""The Context."""
def __init__(self):
self.balance = 0
self.selected_drink = None
self._state = IdleState(self)
def set_state(self, state):
self._state = state
def insert_money(self, amount):
self._state.insert_money(amount)
def select_drink(self, drink):
self._state.select_drink(drink)
def dispense(self):
self._state.dispense()
# Usage
machine = VendingMachine()
machine.select_drink("Cola") # "Please insert money first."
machine.insert_money(2.00) # "$2.00 inserted."
machine.select_drink("Cola") # "Selected Cola" (assuming Cola costs <= $2)
machine.dispense() # "Dispensing Cola... Enjoy!"
The beauty is that adding a new state, like an OutOfOrderState, is trivial. You never have to touch the logic of the other states. You just create a new class and make the context transition to it. It turns a tangled web of conditional logic into a clean, organized set of objects.
Template Method: The Skeleton Key
This one is about keeping your DRY principle intact. The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
It’s the “fill-in-the-blanks” approach to design. The parent class says “here’s the order things happen in, you handle the specifics.”
from abc import ABC, abstractmethod
class DataExporter(ABC):
"""This class defines the algorithm's skeleton."""
def export(self, data):
"""The Template Method. It's final for a reason."""
self.validate_data(data)
cleaned_data = self.clean_data(data)
formatted_data = self.format_data(cleaned_data)
return self.send_data(formatted_data)
def validate_data(self, data):
# A common step with a default implementation
if not data:
raise ValueError("Data cannot be empty.")
def clean_data(self, data):
# Another common step
return [item for item in data if item is not None]
@abstractmethod
def format_data(self, data):
"""Subclasses MUST implement this."""
pass
@abstractmethod
def send_data(self, data):
"""Subclasses MUST implement this."""
pass
class CSVExporter(DataExporter):
def format_data(self, data):
return ",".join(map(str, data))
def send_data(self, data):
print(f"Pretending to write '{data}' to a CSV file.")
return True
class JSONExporter(DataExporter):
def format_data(self, data):
import json
return json.dumps(data)
def send_data(self, data):
print(f"Pretending to POST '{data}' to a web API.")
return True
# Usage
data = [1, 2, 3, None, 4]
csv_exporter = CSVExporter()
csv_exporter.export(data) # Validates, cleans, formats as CSV, "sends"
json_exporter = JSONExporter()
json_exporter.export(data) # Validates, cleans, formats as JSON, "sends"
The key here is the non-abstract methods in the base class. They provide common, reusable functionality that all exporters share. The subclasses only worry about the bits that are actually different. This is how you prevent copy-pasting the validate_data and clean_data logic into every single exporter class. It centralizes the algorithm’s flow and maximizes code reuse. It’s relentlessly pragmatic.