1.0 Behavioral Patterns
Behavioral patterns address how objects communicate, distribute responsibility,
and coordinate action. They answer the question: who does what, and how
does the caller know what happened?
The three patterns in this chapter - Strategy, Observer, and Command - are the
most widely encountered in application design. Strategy decouples an algorithm from
the object that invokes it. Observer decouples event producers from consumers.
Command decouples a request from the code that executes it.
1.1 Strategy
Intent: define a family of interchangeable algorithms, encapsulate
each one, and make them substitutable without changing the context that uses them.
The Context holds a reference to a Strategy. It delegates algorithm execution to
the strategy, knowing only the strategy's interface - not its implementation.
Strategies can be swapped at construction time or at runtime.
classDiagram
class TextSearch {
-matcher : Box~dyn Matcher~
+search(file, query) Vec~String~
}
class Matcher {
<<interface>>
+matches(line, query) bool
}
class ExactMatcher {
+matches(line, query) bool
}
class RegexMatcher {
-compiled : Regex
+matches(line, query) bool
}
class CaseFoldMatcher {
+matches(line, query) bool
}
TextSearch o--> Matcher : delegates to
Matcher <|.. ExactMatcher : implements
Matcher <|.. RegexMatcher : implements
Matcher <|.. CaseFoldMatcher : implements
interface Matcher {
fn matches(line: &str, query: &str) -> bool
}
struct TextSearch {
matcher: Box<dyn Matcher>
}
impl TextSearch {
fn search(&self, file: &Path, query: &str) -> Vec<String> {
read_lines(file)
.filter(|line| self.matcher.matches(line, query))
.collect()
}
}
// Concrete strategies:
struct ExactMatcher;
struct RegexMatcher { compiled: Regex }
struct CaseFoldMatcher;
Swapping from exact matching to regex matching requires only constructing
TextSearch with a different strategy. No change to the search loop itself.
Strategy - Language Idioms
| Language | Idiomatic form |
| Rust |
Box<dyn Trait> for owned strategies; closures
(Box<dyn Fn(...)>) when the strategy is a single operation
with no state.
|
| C++ |
Abstract base class with pure virtual execute() and
std::unique_ptr<Strategy> in the context; or a template
parameter for compile-time strategy selection with zero overhead.
|
| C# |
Interface or delegate; Func<T, TResult> replaces a
single-method interface in most cases. LINQ is built on the strategy pattern.
|
| Python |
Pass a plain function as the strategy - no wrapper class needed. Duck typing
means any callable with the right signature qualifies.
|
Strategy - Pros and Cons
| Notes |
| Pros |
Context is closed to modification when new algorithms are added; each strategy
is independently testable; strategies are composable.
|
| Cons |
Client must know which strategy to select; slight indirection overhead on
each call; proliferates types when strategies are numerous and stateless.
|
| Best for |
Any context where the algorithm varies independently from the data it operates
on - sorting comparators, serialization formats, search matching rules.
|
1.2 Observer
Intent: define a one-to-many dependency so that when a subject
changes state, all registered observers are notified and updated automatically.
The Subject maintains a list of Observer references. When state changes, it iterates
the list and calls each observer's update method. Observers subscribe and unsubscribe
at runtime. The subject has no knowledge of what observers do with the notification.
classDiagram
class FileWatcher {
-path : Path
-observers : Vec~Box~dyn Observer~~
+subscribe(obs)
+unsubscribe(id)
+poll()
}
class Observer {
<<interface>>
+on_event(event)
}
class LogObserver {
+on_event(event)
}
class CacheObserver {
+on_event(event)
}
class NotifyObserver {
+on_event(event)
}
FileWatcher o--> "0..*" Observer : notifies
Observer <|.. LogObserver : implements
Observer <|.. CacheObserver : implements
Observer <|.. NotifyObserver : implements
interface Observer {
fn on_event(&self, event: Event)
}
struct FileWatcher {
path: Path,
observers: Vec<Box<dyn Observer>>
}
impl FileWatcher {
fn subscribe(&mut self, obs: Box<dyn Observer>)
fn unsubscribe(&mut self, id: ObserverId)
fn poll(&self) {
if file_changed(self.path) {
let event = Event::new(self.path);
for obs in &self.observers {
obs.on_event(&event);
}
}
}
}
Each observer reacts independently: one may log the change, another may reload a
cache, a third may send a notification. None of them know about each other.
Observer - Language Idioms
| Language | Idiomatic form |
| Rust |
Channels (std::sync::mpsc) for decoupled single-consumer
notification; Arc<Mutex<Vec<Box<dyn Fn(...)>>>>
for multi-observer callback lists. Ownership rules prevent dangling observers.
|
| C++ |
std::vector<std::function<void(Event)>> as the
subscriber list; use std::weak_ptr for observer references to
avoid lifetime issues when observers are destroyed before the subject.
|
| C# |
Events and delegates are the native language form. Declare
event EventHandler<T> Changed; subscribers use
+= and -=. The runtime manages the invocation list.
|
| Python |
A list of callables; blinker or PyDispatcher libraries
provide signal/slot infrastructure. Django signals are an application of this pattern.
|
Observer - Pros and Cons
| Notes |
| Pros |
Subject and observers are loosely coupled; observers can be added and removed
at runtime without changing the subject; supports broadcast notification
to an arbitrary number of consumers.
|
| Cons |
Notification order is not guaranteed; a misbehaving observer can stall the
subject; cascading updates are possible when observers themselves notify
other subjects; object lifetime management requires care in C++ and Rust.
|
| Best for |
UI event handling, model-view synchronization, pub/sub messaging within a
process, and any situation where one state change drives multiple reactions.
|
1.3 Command
Intent: encapsulate a request as an object so that requests can
be parameterized, queued, logged, and undone.
A Command object bundles the action and its parameters. An Invoker queues and
executes commands without knowing what they do. The Receiver carries out the
actual work. Storing executed commands in a history stack enables undo.
classDiagram
class Editor {
-history : Vec~Box~dyn Command~~
+do_command(cmd)
+undo()
}
class Command {
<<interface>>
+execute()
+undo()
}
class InsertText {
-pos : usize
-text : String
+execute()
+undo()
}
class DeleteText {
-pos : usize
-len : usize
-deleted : String
+execute()
+undo()
}
Editor o--> "0..*" Command : executes
Command <|.. InsertText : implements
Command <|.. DeleteText : implements
interface Command {
fn execute(&mut self)
fn undo(&mut self)
}
struct InsertText { pos: usize, text: String, buffer: Rc<RefCell<Buffer>> }
struct DeleteText { pos: usize, len: usize, deleted: String, buffer: Rc<RefCell<Buffer>> }
impl Command for InsertText {
fn execute(&mut self) { self.buffer.borrow_mut().insert(self.pos, &self.text) }
fn undo(&mut self) { self.buffer.borrow_mut().delete(self.pos, self.text.len()) }
}
struct Editor {
history: Vec<Box<dyn Command>>
}
impl Editor {
fn do_command(&mut self, mut cmd: Box<dyn Command>) {
cmd.execute();
self.history.push(cmd);
}
fn undo(&mut self) {
if let Some(mut cmd) = self.history.pop() { cmd.undo(); }
}
}
The Editor does not know whether it is inserting, deleting, or moving text. It only
knows how to execute and undo. Adding a new operation means adding a new Command
type - no changes to Editor.
Command - Language Idioms
| Language | Idiomatic form |
| Rust |
An enum whose variants carry their parameters is the most idiomatic form;
match on the variant in execute() and undo(). Trait
objects work when commands come from external sources or plugins.
|
| C++ |
Abstract base class with virtual execute() and undo();
store std::unique_ptr<Command> in the history stack.
Lambdas with captures work for commands that need no undo.
|
| C# |
ICommand interface with Execute() and CanExecute()
is used directly by WPF/MVVM infrastructure. Implementing undo requires a
separate history stack not provided by the framework.
|
| Python |
A callable object or a namedtuple carrying parameters; the history list
stores them. For simple command queues without undo, a list of lambdas suffices.
|
Command - Pros and Cons
| Notes |
| Pros |
Decouples the caller from the action; undo/redo via history stack; commands
are composable into macros; the queue is serializable for logging or remote
execution.
|
| Cons |
One type per command grows the codebase; capturing enough state for undo can
be expensive; serializing commands for persistence requires additional effort.
|
| Best for |
Text and graphics editors, transactional systems requiring rollback, task
queues, and any system where user actions must be reversible.
|
1.4 References
Behavioral Patterns References