SWDev Patterns

SWDev Patterns: Behavioral

strategy, observer, command

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
LanguageIdiomatic 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
LanguageIdiomatic 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
LanguageIdiomatic 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
ResourceDescription
Refactoring Guru - Strategy Strategy pattern with diagrams and examples in multiple languages.
Refactoring Guru - Observer Observer pattern with subscribe/unsubscribe examples.
Refactoring Guru - Command Command pattern with undo history and macro recording examples.
Rust Book - Trait Objects Dynamic dispatch in Rust - the foundation for trait-based behavioral patterns.