SWDev Impl Patterns

SWDev Impl Patterns: Behavior and Dispatch

command-query separation, polymorphism, delegation, template method

3.0 Behavior and Dispatch

The four patterns in this chapter address how methods communicate their intent, how conditional logic is eliminated through types, and how behavior is varied without subclassing. They operate at the boundary between individual method contracts and class design.

3.1 Command-Query Separation

Problem: a method both changes state and returns a value. Callers cannot reason about which calls are safe to repeat, reorder, or skip. Reading code that queries state through calls with side effects requires tracking implicit mutations. Solution: separate every method into one of two kinds. A command changes state and returns nothing (or only a status). A query returns a value and causes no observable side effects. Meyer: asking a question should not change the answer. Beck applies this at the implementation level: name commands as imperatives (add_item, reset), name queries as nouns or predicates (count, is_empty).
// Violation: pop() both removes an item and returns it
fn pop(&mut self) -> Option<T>

// CQS-compliant separation
fn peek(&self) -> Option<&T>    // query:   returns top item, no mutation
fn remove_top(&mut self)         // command: removes top item, returns nothing

// In Rust, the standard library violates CQS intentionally for ergonomics:
// Vec::pop() is a well-known, documented exception.
// The principle guides new API design; it does not forbid pragmatic exceptions.
Command-Query Separation - Notes
Notes
Benefit Queries are referentially transparent - their result depends only on state, not on how many times they are called. This makes code easier to test, reason about, and optimize (queries can be cached or reordered).
Pragmatic exceptions Some operations are inherently atomic (dequeue an item from a concurrent queue). CQS may be violated where the alternative requires holding a lock across two calls. Document the exception; do not make it a habit.
Naming signal If a method name contains a verb and returns a value, it is probably violating CQS. get_and_clear(), fetch_next(), update_and_return() are signals to review.

3.2 Replace Conditional with Polymorphism

Problem: the same type-checking conditional - if type == A ... else if type == B ... or a switch on a type tag - appears in multiple methods. Adding a new type requires finding and updating every copy. Solution: define a method on a shared interface and implement it differently for each type. The caller dispatches through the interface; the conditional disappears into the dispatch mechanism. Fowler: move each leg of the conditional into an overriding method in a subclass (or trait implementor). Make the original method abstract.
// Before: type-dispatching conditional repeated in every rendering method
fn render_shape(shape: &Shape, canvas: &mut Canvas) {
    match shape.kind {
        ShapeKind::Circle    => render_circle(shape, canvas),
        ShapeKind::Rectangle => render_rectangle(shape, canvas),
        ShapeKind::Triangle  => render_triangle(shape, canvas),
    }
}
fn bounding_box(shape: &Shape) -> Rect {
    match shape.kind { ... }  // same match, again
}

// After: each type carries its own behavior
trait Shape {
    fn render(&self, canvas: &mut Canvas);
    fn bounding_box(&self) -> Rect;
}

struct Circle    { center: Point, radius: f64 }
struct Rectangle { origin: Point, size: Size }
struct Triangle  { vertices: [Point; 3] }

impl Shape for Circle    { fn render(..) { .. } fn bounding_box(..) { .. } }
impl Shape for Rectangle { fn render(..) { .. } fn bounding_box(..) { .. } }
impl Shape for Triangle  { fn render(..) { .. } fn bounding_box(..) { .. } }
Adding a Polygon type now requires only a new impl Shape for Polygon. No existing code changes. The match statements that scattered logic across the codebase are gone.
Replace Conditional - Notes
Notes
When to apply When the same conditional on a type tag or enum appears in three or more methods. A single conditional on an enum is normal; the same conditional in many places is a sign that behavior belongs on the type.
Rust enums Rust enums with match are often the right choice when the set of variants is closed and all variants are known at compile time. The compiler enforces exhaustive matching. Use trait objects when new types must be added without recompilation.
Limit Not every conditional needs polymorphism. A single boolean flag that changes one behavior in one method does not justify an interface split.

3.3 Delegation over Inheritance

Problem: a subclass inherits a large interface from a parent but only needs a small part of it. The subclass overrides most inherited behavior to no-ops or errors. Changes to the parent break subclasses that depend on inherited methods. Testing the subclass requires instantiating the parent. Solution: hold a reference to a collaborator and delegate the needed operations to it. The class exposes only the methods it actually needs; it is not bound to the parent's full interface. The GoF principle: favor object composition over class inheritance.
// Inheritance: Stack inherits from List and inherits all list operations
// - includes remove_at(), insert_at(), etc. that violate stack semantics
class Stack : public List { ... }

// Delegation: Stack holds a List and exposes only push/pop/peek
struct Stack<T> {
    storage: Vec<T>     // holds a Vec but does not inherit from it
}
impl<T> Stack<T> {
    fn push(&mut self, item: T)  { self.storage.push(item) }
    fn pop(&mut self) -> Option<T> { self.storage.pop() }
    fn peek(&self) -> Option<&T>   { self.storage.last() }
    fn is_empty(&self) -> bool      { self.storage.is_empty() }
}
The delegation version exposes exactly the stack interface. Callers cannot call list operations on a stack. Changing the internal storage to a LinkedList requires one field change - no interface impact.
Delegation - Notes
Notes
When inheritance is right True is-a relationships where the subtype genuinely satisfies the Liskov Substitution Principle - it can be used wherever the base type is used without surprising the caller. If you would have to override methods to prevent their use, delegation is better.
Rust Rust has no class inheritance. Delegation through held fields is the only option; the language enforces the pattern by design. Trait implementations on wrapper types provide the needed interface.
C# / Python Forwarding properties or __getattr__ reduce the boilerplate of delegation when many methods need to be forwarded unchanged.

3.4 Template Method

Problem: several procedures share the same overall structure but differ in specific steps. Duplicating the skeleton in each procedure means that changes to the overall flow must be made in every copy. Solution: define the skeleton once in a template method that calls abstract or overridable steps. Each variant implements only the steps that differ. Beck applies this at the implementation level: the steps can be virtual methods, injected callables, or trait implementations - the key is that the skeleton is written once.
// Template method using injected steps (functional style)
struct ReportPipeline {
    fetch:    Box<dyn Fn() -> Vec<Row>>,
    filter:   Box<dyn Fn(Vec<Row>) -> Vec<Row>>,
    render:   Box<dyn Fn(Vec<Row>) -> String>,
}

impl ReportPipeline {
    fn run(&self) -> String {
        let data     = (self.fetch)();
        let filtered = (self.filter)(data);
        (self.render)(filtered)
    }
}

// Variant A: CSV output
ReportPipeline { fetch: db_fetch, filter: active_only, render: csv_render }

// Variant B: HTML output
ReportPipeline { fetch: db_fetch, filter: active_only, render: html_render }
The pipeline structure - fetch, filter, render - is written once. Variants differ only in the step implementations they supply. Adding an HTML variant requires no changes to the skeleton.
Template Method - Notes
Notes
Inheritance vs. injection The classic GoF form uses virtual method overrides in subclasses. The modern idiomatic form injects step implementations as closures, callbacks, or trait implementations - avoiding a subclass hierarchy for what is essentially a parameterization.
vs. Strategy Strategy replaces the entire algorithm; Template Method replaces individual steps within a fixed skeleton. Use Template Method when the skeleton is stable and only specific steps vary; use Strategy when the algorithm as a whole is interchangeable.
Invariants The template method can enforce invariants around the injected steps - for example, always logging before and after each step, or wrapping the computation in a transaction - regardless of what the steps do.

3.5 References

Behavior and Dispatch References
ResourceDescription
Fowler - Command Query Separation Fowler's summary of Meyer's CQS principle with notes on when to violate it.
Refactoring Guru - Replace Conditional with Polymorphism Step-by-step transformation from type-switching conditional to polymorphic dispatch.
Refactoring Guru - Template Method Template Method pattern with skeleton/step examples and language variants.
Fowler - Composed Method Fowler's discussion of Beck's Composed Method and function length guidance.