Guide contents

Lesson 63 of 94

Behavioral Patterns

Behavioral patterns describe how objects divide work and talk to each other. In class-based designs, most of them are built from small interfaces and a class per variant: a Strategy interface with a class per strategy, a State interface with a class per state, a Visitor with one visit method per node type.

Rust shrinks many of them, because two features cover what those class hierarchies were for:

  • Closures replace one-method interfaces. A strategy, a command, or an observer is often just a closure.
  • Enums replace small closed hierarchies. A state machine or a visitor over a fixed set of node types is a match.

Traits and trait objects are still there for the cases that need them: several methods per variant, or variants that other code must be able to add.

Pattern Rust
Strategy a closure (impl Fn), or a trait for multi-method strategies
Observer Vec<Box<dyn Fn(&Event)>>, a subscription handle, or channels across threads
Command an enum of commands, or boxed closures
State an enum state machine, Box<dyn State>, or typestate (states as types)
Template method trait default methods
Visitor match on an enum, or a visitor trait with default walk methods
Chain of responsibility a list of trait objects tried in order
Iterator the Iterator trait
Mediator one owner that routes messages between members, addressed by id
Memento a Cloned snapshot with private fields
Null object Option, or a no-op implementation

Part 1: Strategy and observer

1. Strategy: just pass a closure

A classic strategy is an interface with one method and a class per variant. In Rust, a closure is the strategy.

Rust
fn sort_users(users: &mut Vec<(&str, u32)>, strategy: impl Fn(&(&str, u32), &(&str, u32)) -> std::cmp::Ordering) {
    users.sort_by(strategy);
}

fn main() {
    let mut users = vec![("cy", 29), ("ana", 34), ("ben", 29)];

    sort_users(&mut users, |a, b| a.0.cmp(b.0));
    println!("by name: {users:?}");

    sort_users(&mut users, |a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    println!("by age desc: {users:?}");

    let strategies: Vec<(&str, Box<dyn Fn(u32) -> u32>)> = vec![("flat", Box::new(|_| 500)), ("per-kg", Box::new(|kg| kg * 120)), ("free", Box::new(|_| 0))];
    for (name, shipping) in &strategies {
        println!("{name:<6} shipping for 7 kg: {}", shipping(7));
    }
}
Output
by name: [("ana", 34), ("ben", 29), ("cy", 29)]
by age desc: [("ana", 34), ("ben", 29), ("cy", 29)]
flat   shipping for 7 kg: 500
per-kg shipping for 7 kg: 840
free   shipping for 7 kg: 0

2. Strategy as a trait

When a strategy has several related methods, a name, or configuration of its own, a trait is clearer than a bundle of closures. A generic parameter fixes the strategy at compile time; Box<dyn Trait> lets you choose it at runtime.

Rust
trait Pricing {
    fn name(&self) -> String;
    fn unit_price(&self, base_cents: u32, qty: u32) -> u32;

    fn total(&self, base_cents: u32, qty: u32) -> u32 {
        self.unit_price(base_cents, qty) * qty
    }
}

struct ListPrice;

struct VolumeDiscount {
    min_qty: u32,
    percent_off: u32,
}

impl Pricing for ListPrice {
    fn name(&self) -> String {
        String::from("list price")
    }
    fn unit_price(&self, base_cents: u32, _qty: u32) -> u32 {
        base_cents
    }
}

impl Pricing for VolumeDiscount {
    fn name(&self) -> String {
        format!("{}% off from {} units", self.percent_off, self.min_qty)
    }
    fn unit_price(&self, base_cents: u32, qty: u32) -> u32 {
        if qty >= self.min_qty { base_cents * (100 - self.percent_off) / 100 } else { base_cents }
    }
}

// Chosen at compile time: one copy of `quote` per strategy type.
fn quote(pricing: &impl Pricing, qty: u32) -> String {
    format!("{qty:>3} x 250 with {}: {}", pricing.name(), pricing.total(250, qty))
}

fn pricing_for(customer_tier: &str) -> Box<dyn Pricing> {
    match customer_tier {
        "wholesale" => Box::new(VolumeDiscount { min_qty: 50, percent_off: 20 }),
        _ => Box::new(ListPrice),
    }
}

fn main() {
    println!("{}", quote(&ListPrice, 10));
    println!("{}", quote(&VolumeDiscount { min_qty: 50, percent_off: 20 }, 60));

    for tier in ["retail", "wholesale"] {
        let pricing = pricing_for(tier); // chosen at runtime
        println!("{tier}: {}", pricing.total(250, 100));
    }
}
Output
 10 x 250 with list price: 2500
 60 x 250 with 20% off from 50 units: 12000
retail: 25000
wholesale: 20000

3. Observer with closures

Observers subscribe to a subject and get notified of events. A Vec of boxed closures is the simplest form.

Rust
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug, Clone)]
enum StockEvent {
    PriceChanged { symbol: String, price: f64 },
}

#[derive(Default)]
struct Ticker {
    observers: Vec<Box<dyn Fn(&StockEvent)>>,
}

impl Ticker {
    fn subscribe(&mut self, f: impl Fn(&StockEvent) + 'static) {
        self.observers.push(Box::new(f));
    }

    fn publish(&self, e: StockEvent) {
        for o in &self.observers {
            o(&e);
        }
    }
}

fn main() {
    let alerts = Rc::new(RefCell::new(Vec::new()));
    let mut ticker = Ticker::default();

    ticker.subscribe(|e| println!("log: {e:?}"));
    let a = Rc::clone(&alerts);
    ticker.subscribe(move |StockEvent::PriceChanged { symbol, price }| {
        if *price > 100.0 {
            a.borrow_mut().push(format!("{symbol} crossed 100 at {price}"));
        }
    });

    ticker.publish(StockEvent::PriceChanged { symbol: "ACME".into(), price: 98.5 });
    ticker.publish(StockEvent::PriceChanged { symbol: "ACME".into(), price: 101.25 });
    println!("alerts: {:?}", alerts.borrow());
}
Output
log: PriceChanged { symbol: "ACME", price: 98.5 }
log: PriceChanged { symbol: "ACME", price: 101.25 }
alerts: ["ACME crossed 100 at 101.25"]

The second observer needs to change alerts from inside a closure while main keeps reading it, so it's shared through Rc<RefCell<...>>. For observers on other threads or async tasks, channels are the better tool; they come later, in Channels and Async Channels & Locks.

4. Unsubscribing by dropping a handle

Forgetting to unsubscribe is a classic memory leak. Here subscribe returns a Subscription handle, and dropping the handle unsubscribes. The handle holds only a Weak reference to the listener list, so it doesn't keep the bus alive either (Rc & Weak shows the related trick of observers held weakly).

Rust
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};

type Listeners = RefCell<Vec<(u64, Box<dyn Fn(&str)>)>>;

#[derive(Default)]
struct EventBus {
    listeners: Rc<Listeners>,
    next_id: Cell<u64>,
}

struct Subscription {
    id: u64,
    listeners: Weak<Listeners>,
}

impl Drop for Subscription {
    fn drop(&mut self) {
        if let Some(list) = self.listeners.upgrade() {
            list.borrow_mut().retain(|(id, _)| *id != self.id);
        }
    }
}

impl EventBus {
    fn subscribe(&self, f: impl Fn(&str) + 'static) -> Subscription {
        let id = self.next_id.get();
        self.next_id.set(id + 1);
        self.listeners.borrow_mut().push((id, Box::new(f)));
        Subscription { id, listeners: Rc::downgrade(&self.listeners) }
    }

    fn publish(&self, event: &str) {
        for (_, listener) in self.listeners.borrow().iter() {
            listener(event);
        }
    }
}

fn main() {
    let bus = EventBus::default();
    let _audit = bus.subscribe(|e| println!("audit: {e}"));
    {
        let _popup = bus.subscribe(|e| println!("popup: {e}"));
        bus.publish("file saved");
    } // `_popup` is dropped here, which unsubscribes it
    bus.publish("file closed");
    println!("{} listener(s) left", bus.listeners.borrow().len());
}
Output
audit: file saved
popup: file saved
audit: file closed
1 listener(s) left

Note the names: let _audit = ... keeps the handle alive until the end of main. Writing let _ = ... instead would drop it immediately and unsubscribe on the spot, because _ is not a variable. One limitation: a listener that subscribes or unsubscribes while publish is running would hit the RefCell borrow check and panic; collect such changes and apply them after publishing.

Part 2: Command and state

5. Command with undo

Each command is a value describing an operation, so it can be stored, replayed, or undone. An enum lists the commands; applying and undoing are two matches.

Rust
#[derive(Debug, Clone)]
enum Command {
    Insert { pos: usize, text: String },
    Delete { pos: usize, len: usize, removed: Option<String> },
}

struct Editor {
    text: String,
    history: Vec<Command>,
}

impl Editor {
    fn execute(&mut self, mut cmd: Command) {
        match &mut cmd {
            Command::Insert { pos, text } => self.text.insert_str(*pos, text),
            Command::Delete { pos, len, removed } => {
                *removed = Some(self.text.drain(*pos..*pos + *len).collect());
            }
        }
        self.history.push(cmd);
    }

    fn undo(&mut self) {
        match self.history.pop() {
            Some(Command::Insert { pos, text }) => {
                self.text.replace_range(pos..pos + text.len(), "");
            }
            Some(Command::Delete { pos, removed: Some(r), .. }) => self.text.insert_str(pos, &r),
            _ => {}
        }
    }
}

fn main() {
    let mut ed = Editor { text: String::from("Hello world"), history: Vec::new() };
    ed.execute(Command::Insert { pos: 5, text: String::from(",") });
    ed.execute(Command::Delete { pos: 7, len: 5, removed: None });
    ed.execute(Command::Insert { pos: 7, text: String::from("Rust") });
    println!("{:?}", ed.text);
    ed.undo();
    println!("{:?}", ed.text);
    ed.undo();
    println!("{:?}", ed.text);
    println!("{} command(s) left in history", ed.history.len());
}
Output
"Hello, Rust"
"Hello, "
"Hello, world"
1 command(s) left in history

6. Commands as queued closures

When commands only need to run, not be inspected or undone, a boxed closure is enough. FnOnce closures may consume what they captured, so each one can take ownership of its own data and run exactly once.

Rust
use std::collections::VecDeque;

type Task = Box<dyn FnOnce() -> Result<String, String>>;

#[derive(Default)]
struct TaskQueue {
    tasks: VecDeque<(String, Task)>,
}

impl TaskQueue {
    fn push(&mut self, name: &str, task: impl FnOnce() -> Result<String, String> + 'static) {
        self.tasks.push_back((name.to_string(), Box::new(task)));
    }

    fn run_all(&mut self) {
        while let Some((name, task)) = self.tasks.pop_front() {
            match task() {
                Ok(msg) => println!("{name}: {msg}"),
                Err(e) => println!("{name} FAILED: {e}"),
            }
        }
    }
}

fn main() {
    let mut queue = TaskQueue::default();

    let amounts = vec![1_250, 4_990, 300];
    queue.push("sum", move || Ok(format!("total {}", amounts.iter().sum::<u32>()))); // `amounts` moves into the task

    let email = String::from("ana@example.com");
    queue.push("mail", move || if email.contains('@') { Ok(format!("sent to {email}")) } else { Err(String::from("bad address")) });

    queue.push("parse", || "12x".parse::<u32>().map(|n| n.to_string()).map_err(|e| e.to_string()));

    println!("{} tasks queued", queue.tasks.len());
    queue.run_all();
}
Output
3 tasks queued
sum: total 6540
mail: sent to ana@example.com
parse FAILED: invalid digit found in string

7. State: an enum state machine

The GoF state pattern uses one class per state, each implementing the transitions. An enum with a transition function is shorter and makes every state/event combination visible in one match.

Rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum Vending {
    Idle,
    HasCoins(u32),
    Dispensing,
}

#[derive(Debug, Clone, Copy)]
enum Input {
    Coin(u32),
    Select { price: u32 },
    Done,
}

fn next(state: Vending, input: Input) -> (Vending, String) {
    match (state, input) {
        (Vending::Idle, Input::Coin(c)) => (Vending::HasCoins(c), format!("credit {c}")),
        (Vending::HasCoins(have), Input::Coin(c)) => (Vending::HasCoins(have + c), format!("credit {}", have + c)),
        (Vending::HasCoins(have), Input::Select { price }) if have >= price => {
            (Vending::Dispensing, format!("dispensing, change {}", have - price))
        }
        (Vending::HasCoins(have), Input::Select { price }) => (state, format!("need {} more", price - have)),
        (Vending::Dispensing, Input::Done) => (Vending::Idle, String::from("ready")),
        (s, i) => (s, format!("ignored {i:?} in {s:?}")),
    }
}

fn main() {
    let mut state = Vending::Idle;
    for input in [Input::Select { price: 150 }, Input::Coin(100), Input::Select { price: 150 }, Input::Coin(100), Input::Select { price: 150 }, Input::Done] {
        let (s, msg) = next(state, input);
        state = s;
        println!("{msg:<28} -> {state:?}");
    }
}
Output
ignored Select { price: 150 } in Idle -> Idle
credit 100                   -> HasCoins(100)
need 50 more                 -> HasCoins(100)
credit 200                   -> HasCoins(200)
dispensing, change 50        -> Dispensing
ready                        -> Idle

8. State with trait objects

The direct translation of the GoF pattern: each state is its own type implementing a State trait, and each transition method returns the next state. Default methods mean "this event is ignored in this state", so each state only implements the events it reacts to. New states can be added without touching a central match, which is the trade-off against example 7.

Rust
trait State {
    fn name(&self) -> String;

    // `None` means: stay in the current state.
    fn submit(&self) -> Option<Box<dyn State>> {
        None
    }
    fn approve(&self) -> Option<Box<dyn State>> {
        None
    }
    fn reject(&self) -> Option<Box<dyn State>> {
        None
    }
    fn is_public(&self) -> bool {
        false
    }
}

struct Draft;
struct InReview {
    approvals: u32,
}
struct Published;

impl State for Draft {
    fn name(&self) -> String {
        String::from("draft")
    }
    fn submit(&self) -> Option<Box<dyn State>> {
        Some(Box::new(InReview { approvals: 0 }))
    }
}

impl State for InReview {
    fn name(&self) -> String {
        format!("in review ({}/2 approvals)", self.approvals)
    }
    fn approve(&self) -> Option<Box<dyn State>> {
        if self.approvals + 1 >= 2 { Some(Box::new(Published)) } else { Some(Box::new(InReview { approvals: self.approvals + 1 })) }
    }
    fn reject(&self) -> Option<Box<dyn State>> {
        Some(Box::new(Draft))
    }
}

impl State for Published {
    fn name(&self) -> String {
        String::from("published")
    }
    fn is_public(&self) -> bool {
        true
    }
}

struct Post {
    text: String,
    state: Box<dyn State>,
}

impl Post {
    fn apply(&mut self, event: &str) {
        let next = match event {
            "submit" => self.state.submit(),
            "approve" => self.state.approve(),
            "reject" => self.state.reject(),
            _ => None,
        };
        match next {
            Some(state) => self.state = state,
            None => println!("  ({event} ignored while {})", self.state.name()),
        }
    }

    fn visible_text(&self) -> &str {
        if self.state.is_public() { &self.text } else { "" }
    }
}

fn main() {
    let mut post = Post { text: String::from("Rust for Java devs"), state: Box::new(Draft) };
    for event in ["approve", "submit", "approve", "reject", "submit", "approve", "approve", "submit"] {
        post.apply(event);
        println!("{event:<8} -> {:<27} visible: {:?}", post.state.name(), post.visible_text());
    }
}
Output
  (approve ignored while draft)
approve  -> draft                       visible: ""
submit   -> in review (0/2 approvals)   visible: ""
approve  -> in review (1/2 approvals)   visible: ""
reject   -> draft                       visible: ""
submit   -> in review (0/2 approvals)   visible: ""
approve  -> in review (1/2 approvals)   visible: ""
approve  -> published                   visible: "Rust for Java devs"
  (submit ignored while published)
submit   -> published                   visible: "Rust for Java devs"

9. Typestate: invalid transitions don't compile

When the order of steps is fixed, you can make each state a separate type. Each transition consumes the old value and returns the new one, and methods exist only on the states where they're valid. Calling one in the wrong state isn't a runtime error: it doesn't compile.

Rust
struct DraftPost {
    text: String,
}

struct PendingPost {
    text: String,
}

struct PublishedPost {
    text: String,
}

impl DraftPost {
    fn new(text: &str) -> DraftPost {
        DraftPost { text: text.to_string() }
    }
    fn edit(&mut self, more: &str) {
        self.text.push_str(more);
    }
    fn submit(self) -> PendingPost {
        PendingPost { text: self.text }
    }
}

impl PendingPost {
    fn approve(self) -> PublishedPost {
        PublishedPost { text: self.text }
    }
    fn reject(self) -> DraftPost {
        DraftPost { text: self.text }
    }
}

impl PublishedPost {
    fn content(&self) -> &str {
        &self.text // only published posts can be read
    }
}

fn main() {
    let mut draft = DraftPost::new("Typestate");
    draft.edit(" in Rust");
    let rejected = draft.submit().reject();
    let published = rejected.submit().approve();
    println!("{}", published.content());
}
Output
Typestate in Rust

Reading a draft is a compile error, found before the program runs:

Rust✗ does not compile · E0599
struct DraftPost {
    text: String,
}

struct PublishedPost {
    text: String,
}

impl PublishedPost {
    fn content(&self) -> &str {
        &self.text
    }
}

fn main() {
    let draft = DraftPost { text: String::from("secret plans") };
    println!("{}", draft.content());
}
Output
error[E0599]: no method named `content` found for struct `DraftPost` in the current scope
  --> src/main.rs:17:26
   |
 1 | struct DraftPost {
   | ---------------- method `content` not found for this struct
...
17 |     println!("{}", draft.content());
   |                          ^^^^^^^ method not found in `DraftPost`

Because submit(self) takes ownership, the old DraftPost can't be used after submitting either. API Design Idioms, later in the course, applies the same technique to builders.

Part 3: Algorithms over structures

10. Template method: trait default methods

The template method pattern defines an algorithm's skeleton in a base class and lets subclasses fill in steps. A trait with a default method that calls required methods is exactly that, without a base class.

Rust
trait ReportGenerator {
    fn title(&self) -> String;
    fn rows(&self) -> Vec<(String, f64)>;

    fn footer(&self, total: f64) -> String {
        format!("TOTAL {total:.2}") // an optional hook with a default
    }

    // The template: fixed skeleton, customizable steps.
    fn generate(&self) -> String {
        let rows = self.rows();
        let total: f64 = rows.iter().map(|(_, v)| v).sum();
        let mut out = vec![self.title().to_uppercase()];
        out.extend(rows.iter().map(|(k, v)| format!("  {k:<8} {v:>8.2}")));
        out.push(self.footer(total));
        out.join("\n")
    }
}

struct SalesReport;
struct ExpenseReport;

impl ReportGenerator for SalesReport {
    fn title(&self) -> String {
        String::from("Q3 sales")
    }
    fn rows(&self) -> Vec<(String, f64)> {
        vec![("north".into(), 1200.5), ("south".into(), 980.0)]
    }
}

impl ReportGenerator for ExpenseReport {
    fn title(&self) -> String {
        String::from("Q3 expenses")
    }
    fn rows(&self) -> Vec<(String, f64)> {
        vec![("travel".into(), 310.0), ("software".into(), 89.99)]
    }
    fn footer(&self, total: f64) -> String {
        format!("SPENT {total:.2} (budget 500.00)")
    }
}

fn main() {
    println!("{}\n", SalesReport.generate());
    println!("{}", ExpenseReport.generate());
}
Output
Q3 SALES
  north     1200.50
  south      980.00
TOTAL 2180.50

Q3 EXPENSES
  travel     310.00
  software    89.99
SPENT 399.99 (budget 500.00)

11. Visitor: match on an enum

The visitor pattern exists because adding an operation to a class hierarchy otherwise means editing every class. With an enum, a new operation is just a new function with a match, and the compiler checks it covers every variant.

Rust
enum Expr {
    Num(f64),
    Var(&'static str),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

use Expr::*;

// "Visitor" 1: evaluate
fn eval(e: &Expr, x: f64) -> f64 {
    match e {
        Num(n) => *n,
        Var(_) => x,
        Add(a, b) => eval(a, x) + eval(b, x),
        Mul(a, b) => eval(a, x) * eval(b, x),
    }
}

// "Visitor" 2: pretty-print
fn show(e: &Expr) -> String {
    match e {
        Num(n) => n.to_string(),
        Var(v) => v.to_string(),
        Add(a, b) => format!("({} + {})", show(a), show(b)),
        Mul(a, b) => format!("{} * {}", show(a), show(b)),
    }
}

// "Visitor" 3: count variables
fn vars(e: &Expr) -> usize {
    match e {
        Num(_) => 0,
        Var(_) => 1,
        Add(a, b) | Mul(a, b) => vars(a) + vars(b),
    }
}

fn main() {
    // 3 * (x + 2)
    let e = Mul(Box::new(Num(3.0)), Box::new(Add(Box::new(Var("x")), Box::new(Num(2.0)))));
    println!("{} = {} at x = 4", show(&e), eval(&e, 4.0));
    println!("variables used: {}", vars(&e));
}
Output
3 * (x + 2) = 18 at x = 4
variables used: 1

The trade-off flips compared to a class hierarchy: adding an operation is easy (a new function), adding a variant means updating every match, which the compiler lists for you.

12. A visitor trait with default walking

With many node types, writing a full match for every small analysis gets repetitive. A visitor trait whose default methods walk the whole tree lets each visitor override only the nodes it cares about. Parsers and linters in Rust (the syn crate, for example) are built this way.

Rust
use std::collections::BTreeMap;

enum Expr {
    Num(f64),
    Var(String),
    Add(Box<Expr>, Box<Expr>),
    Call(String, Vec<Expr>),
}

trait Visitor {
    fn visit_expr(&mut self, e: &Expr) {
        walk(self, e) // default: just keep walking
    }
    fn visit_var(&mut self, _name: &str) {}
    fn visit_call(&mut self, _name: &str, _args: &[Expr]) {}
}

// Visits every child, calling the hooks along the way.
fn walk<V: Visitor + ?Sized>(v: &mut V, e: &Expr) {
    match e {
        Expr::Num(_) => {}
        Expr::Var(name) => v.visit_var(name),
        Expr::Add(a, b) => {
            v.visit_expr(a);
            v.visit_expr(b);
        }
        Expr::Call(name, args) => {
            v.visit_call(name, args);
            for arg in args {
                v.visit_expr(arg);
            }
        }
    }
}

// Collects variable names; ignores everything else.
struct Vars(Vec<String>);

impl Visitor for Vars {
    fn visit_var(&mut self, name: &str) {
        if !self.0.iter().any(|v| v == name) {
            self.0.push(name.to_string());
        }
    }
}

// Counts function calls and their arguments.
struct Calls(BTreeMap<String, Vec<usize>>);

impl Visitor for Calls {
    fn visit_call(&mut self, name: &str, args: &[Expr]) {
        self.0.entry(name.to_string()).or_default().push(args.len());
    }
}

fn var(n: &str) -> Expr {
    Expr::Var(n.to_string())
}

fn call(n: &str, args: Vec<Expr>) -> Expr {
    Expr::Call(n.to_string(), args)
}

fn main() {
    // max(x, y + 1) + min(x, abs(z), 0)
    let e = Expr::Add(
        Box::new(call("max", vec![var("x"), Expr::Add(Box::new(var("y")), Box::new(Expr::Num(1.0)))])),
        Box::new(call("min", vec![var("x"), call("abs", vec![var("z")]), Expr::Num(0.0)])),
    );

    let mut vars = Vars(Vec::new());
    vars.visit_expr(&e);
    println!("variables: {:?}", vars.0);

    let mut calls = Calls(BTreeMap::new());
    calls.visit_expr(&e);
    println!("calls (arg counts): {:?}", calls.0);
}
Output
variables: ["x", "y", "z"]
calls (arg counts): {"abs": [1], "max": [2], "min": [3]}

13. Chain of responsibility: middleware

Each handler either processes a request or passes it on. A list of trait objects, tried in order, is the usual shape, and it's how web frameworks structure middleware.

Rust
struct Request {
    path: String,
    token: Option<String>,
}

enum Outcome {
    Continue(Request),
    Respond(u16, String),
}

trait Middleware {
    fn handle(&self, req: Request) -> Outcome;
}

struct Auth;
struct RateLimit {
    blocked_paths: Vec<&'static str>,
}
struct Router;

impl Middleware for Auth {
    fn handle(&self, req: Request) -> Outcome {
        match &req.token {
            Some(t) if t == "valid" => Outcome::Continue(req),
            _ if req.path == "/health" => Outcome::Continue(req),
            _ => Outcome::Respond(401, String::from("unauthorized")),
        }
    }
}

impl Middleware for RateLimit {
    fn handle(&self, req: Request) -> Outcome {
        if self.blocked_paths.contains(&req.path.as_str()) {
            Outcome::Respond(429, String::from("too many requests"))
        } else {
            Outcome::Continue(req)
        }
    }
}

impl Middleware for Router {
    fn handle(&self, req: Request) -> Outcome {
        Outcome::Respond(200, format!("content of {}", req.path))
    }
}

fn run(chain: &[Box<dyn Middleware>], mut req: Request) -> (u16, String) {
    for m in chain {
        match m.handle(req) {
            Outcome::Continue(r) => req = r,
            Outcome::Respond(code, body) => return (code, body),
        }
    }
    (404, String::from("no handler"))
}

fn main() {
    let chain: Vec<Box<dyn Middleware>> = vec![Box::new(Auth), Box::new(RateLimit { blocked_paths: vec!["/export"] }), Box::new(Router)];
    for (path, token) in [("/health", None), ("/orders", Some("valid")), ("/orders", None), ("/export", Some("valid"))] {
        let req = Request { path: path.into(), token: token.map(String::from) };
        println!("{path:<8} {:?}", run(&chain, req));
    }
}
Output
/health  (200, "content of /health")
/orders  (200, "content of /orders")
/orders  (401, "unauthorized")
/export  (429, "too many requests")

Each handler takes the Request by value and hands it back inside Continue, so ownership of the request moves down the chain and no handler can hold on to it by accident.

14. Iterator

The iterator pattern is built into the language: implement Iterator (one method, next) and you get map, filter, zip, sum, and dozens more for free (Custom Iterators).

Rust
struct Countdown {
    from: u32,
}

impl Iterator for Countdown {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.from == 0 {
            return None;
        }
        self.from -= 1;
        Some(self.from + 1)
    }
}

fn main() {
    let launch: Vec<String> = Countdown { from: 5 }.map(|n| n.to_string()).chain(std::iter::once(String::from("liftoff"))).collect();
    println!("{}", launch.join(" "));
    println!("sum of evens: {}", Countdown { from: 10 }.filter(|n| n % 2 == 0).sum::<u32>());
}
Output
5 4 3 2 1 liftoff
sum of evens: 30

Part 4: Coordinating objects

15. Mediator: one owner routes the messages

In the mediator pattern, objects don't talk to each other directly; they go through a central mediator. It's a way to reduce coupling, and in Rust it's often the natural design anyway: objects holding references to each other fight the borrow checker, while one owner that holds everything and addresses members by id doesn't.

Rust
#[derive(Default)]
struct Member {
    name: String,
    inbox: Vec<String>,
    muted: bool,
}

#[derive(Default)]
struct ChatRoom {
    members: Vec<Member>,
}

type MemberId = usize;

impl ChatRoom {
    fn join(&mut self, name: &str) -> MemberId {
        self.members.push(Member { name: name.to_string(), ..Default::default() });
        self.members.len() - 1
    }

    fn mute(&mut self, who: MemberId) {
        self.members[who].muted = true;
    }

    // `to: None` broadcasts to everyone except the sender.
    fn send(&mut self, from: MemberId, to: Option<MemberId>, text: &str) {
        let sender = self.members[from].name.clone();
        for (id, member) in self.members.iter_mut().enumerate() {
            let addressed = match to {
                Some(target) => target == id,
                None => id != from,
            };
            if addressed && !member.muted {
                member.inbox.push(format!("{sender}: {text}"));
            }
        }
    }
}

fn main() {
    let mut room = ChatRoom::default();
    let ana = room.join("ana");
    let ben = room.join("ben");
    let cy = room.join("cy");

    room.send(ana, None, "standup in 5 minutes");
    room.mute(cy);
    room.send(ben, Some(ana), "running late");
    room.send(ben, None, "starting without cy?");

    for m in &room.members {
        println!("{:<4} {:?}", m.name, m.inbox);
    }
}
Output
ana  ["ben: running late", "ben: starting without cy?"]
ben  ["ana: standup in 5 minutes"]
cy   ["ana: standup in 5 minutes"]

The ids are plain indexes, the same approach as the index-based graph in Cell & RefCell.

16. Memento: snapshots you can restore

A memento captures an object's state so it can be restored later, without exposing that state to whoever stores the snapshots. Clone makes the snapshot; private fields keep it opaque outside the module.

Rust
mod editor {
    #[derive(Clone)]
    pub struct Snapshot {
        text: String, // private: the history can store it, but not look inside
        cursor: usize,
    }

    pub struct Editor {
        text: String,
        cursor: usize,
    }

    impl Editor {
        pub fn new() -> Editor {
            Editor { text: String::new(), cursor: 0 }
        }

        pub fn type_text(&mut self, s: &str) {
            self.text.insert_str(self.cursor, s);
            self.cursor += s.len();
        }

        pub fn snapshot(&self) -> Snapshot {
            Snapshot { text: self.text.clone(), cursor: self.cursor }
        }

        pub fn restore(&mut self, s: Snapshot) {
            self.text = s.text;
            self.cursor = s.cursor;
        }

        pub fn show(&self) -> String {
            format!("{}|{}", &self.text[..self.cursor], &self.text[self.cursor..])
        }
    }
}

fn main() {
    let mut ed = editor::Editor::new();
    let mut history = Vec::new();

    for word in ["Dear team,", " the release", " is delayed"] {
        history.push(ed.snapshot());
        ed.type_text(word);
        println!("{}", ed.show());
    }
    history.pop(); // throw away the newest snapshot
    ed.restore(history.pop().unwrap()); // go back two steps
    println!("restored: {}", ed.show());
}
Output
Dear team,|
Dear team, the release|
Dear team, the release is delayed|
restored: Dear team,|

17. Null object: Option and Default

The null object pattern avoids null checks by substituting a do-nothing implementation. Rust has no null, so the need mostly disappears: absence is Option. When you do want a do-nothing implementation, a unit struct implementing the trait plays the part.

Rust
trait Logger {
    fn log(&self, msg: &str);
}

struct ConsoleLogger;
struct NoopLogger; // the null object

impl Logger for ConsoleLogger {
    fn log(&self, msg: &str) {
        println!("[log] {msg}");
    }
}

impl Logger for NoopLogger {
    fn log(&self, _msg: &str) {}
}

fn process(items: &[&str], logger: &dyn Logger) -> usize {
    for i in items {
        logger.log(&format!("processing {i}"));
    }
    items.len()
}

fn discount_for(code: Option<&str>) -> u32 {
    code.map(|c| if c == "VIP" { 20 } else { 5 }).unwrap_or(0) // Option instead of a null check
}

fn main() {
    println!("done: {}", process(&["a", "b"], &ConsoleLogger));
    println!("done: {}", process(&["c"], &NoopLogger));
    println!("{} {} {}", discount_for(Some("VIP")), discount_for(Some("WELCOME")), discount_for(None));
}
Output
[log] processing a
[log] processing b
done: 2
done: 1
20 5 0

18. Rust's own patterns

Some patterns come from Rust's own features and have no Gang of Four name. You've met most of them already:

Pattern What it does Lesson
RAII guard cleanup in Drop (locks, files, transactions, subscriptions) Drop, Deref & Cow
Newtype a distinct type around another for type safety or the orphan rule Structural Patterns
Typestate states as types; wrong-order calls don't compile example 9, and API Design Idioms
Extension trait add methods to types you don't own Structural Patterns
Interior mutability mutate through &self with Cell/RefCell/Mutex Cell & RefCell
Sealed trait a public trait others can't implement Modules & Visibility
Actor a thread or task owning state, driven by messages Channels, later in the course

19. Realistic example: a plugin-based order pipeline

A final example combining several patterns: a factory building steps from configuration, steps sharing one trait (strategy and chain of responsibility), a decorator reporting each step, and an observer collecting the events.

Rust
use std::cell::RefCell;

#[derive(Debug, Clone)]
struct Order {
    id: u32,
    total_cents: u32,
    country: &'static str,
    notes: Vec<String>,
}

trait Step {
    fn name(&self) -> &str;
    fn apply(&self, order: Order) -> Result<Order, String>;
}

struct Validate;
struct AddTax {
    rate_percent: u32,
}
struct FraudCheck {
    limit_cents: u32,
}

impl Step for Validate {
    fn name(&self) -> &str {
        "validate"
    }
    fn apply(&self, o: Order) -> Result<Order, String> {
        if o.total_cents == 0 { Err(format!("order {} is empty", o.id)) } else { Ok(o) }
    }
}

impl Step for AddTax {
    fn name(&self) -> &str {
        "tax"
    }
    fn apply(&self, mut o: Order) -> Result<Order, String> {
        let tax = o.total_cents * self.rate_percent / 100;
        o.total_cents += tax;
        o.notes.push(format!("tax {tax}"));
        Ok(o)
    }
}

impl Step for FraudCheck {
    fn name(&self) -> &str {
        "fraud"
    }
    fn apply(&self, o: Order) -> Result<Order, String> {
        if o.total_cents > self.limit_cents && o.country != "DE" {
            Err(format!("order {} held for review", o.id))
        } else {
            Ok(o)
        }
    }
}

// Decorator: reports each step to an observer.
struct Observed<'a> {
    inner: Box<dyn Step>,
    events: &'a RefCell<Vec<String>>,
}

impl Step for Observed<'_> {
    fn name(&self) -> &str {
        self.inner.name()
    }
    fn apply(&self, o: Order) -> Result<Order, String> {
        let id = o.id;
        let result = self.inner.apply(o);
        self.events.borrow_mut().push(format!("order {id}: {} {}", self.name(), if result.is_ok() { "ok" } else { "FAILED" }));
        result
    }
}

// Factory: build the pipeline from configuration.
fn build_pipeline<'a>(config: &[&str], events: &'a RefCell<Vec<String>>) -> Vec<Box<dyn Step + 'a>> {
    config
        .iter()
        .filter_map(|name| -> Option<Box<dyn Step>> {
            match *name {
                "validate" => Some(Box::new(Validate)),
                "tax" => Some(Box::new(AddTax { rate_percent: 19 })),
                "fraud" => Some(Box::new(FraudCheck { limit_cents: 50_000 })),
                _ => None,
            }
        })
        .map(|inner| Box::new(Observed { inner, events }) as Box<dyn Step + 'a>)
        .collect()
}

fn main() {
    let events = RefCell::new(Vec::new());
    let pipeline = build_pipeline(&["validate", "tax", "fraud", "unknown"], &events);

    let orders = [
        Order { id: 1, total_cents: 10_000, country: "DE", notes: vec![] },
        Order { id: 2, total_cents: 0, country: "FR", notes: vec![] },
        Order { id: 3, total_cents: 60_000, country: "US", notes: vec![] },
    ];
    for order in orders {
        let result = pipeline.iter().try_fold(order, |o, step| step.apply(o));
        match result {
            Ok(o) => println!("order {}: total {} {:?}", o.id, o.total_cents, o.notes),
            Err(e) => println!("{e}"),
        }
    }
    println!("{:#?}", events.borrow());
}
Output
order 1: total 11900 ["tax 1900"]
order 2 is empty
order 3 held for review
[
    "order 1: validate ok",
    "order 1: tax ok",
    "order 1: fraud ok",
    "order 2: validate FAILED",
    "order 3: validate ok",
    "order 3: tax ok",
    "order 3: fraud FAILED",
]

Summary

Pattern Rust
Strategy a closure; a trait when it has several methods, generic or Box<dyn>
Observer Vec<Box<dyn Fn(&Event)>>; a handle that unsubscribes on Drop; channels across threads
Command an enum (inspectable, undoable) or Box<dyn FnOnce()> (just runnable)
State an enum + match (closed), Box<dyn State> (open), or typestate (compile-time)
Template method trait default methods calling required methods
Visitor functions with match; a visitor trait with default walk methods for large trees
Chain of responsibility a slice of trait objects, passing the request by value
Iterator impl Iterator
Mediator one owner holding all members, addressed by id
Memento a Cloned snapshot with private fields
Null object Option, or a no-op implementation

This completes the OOP & Design Patterns section. Next: Threads, the start of Concurrency.