Guide contents

Lesson 62 of 94

Structural Patterns

Structural patterns are about combining types: wrapping an object to change its interface (adapter), add behavior (decorator), control access (proxy), or hide complexity (facade), and building trees where one node and a group of nodes look the same (composite).

In Rust, almost all of them take the same shape: a wrapper struct that implements a trait, usually by calling the value it wraps. Generics let wrappers stack at compile time with no overhead; Box<dyn Trait> lets them stack at runtime.

Pattern Rust
Adapter a wrapper (newtype) implementing your trait; a blanket impl for closures
Decorator a generic wrapper Logged<T: Trait>, or Box<dyn Trait> layers
Composite a recursive enum, or a trait object holding Vec<Box<dyn Trait>>
Facade a module with a few public functions
Proxy a wrapper with the same trait; OnceCell for lazy, RefCell<HashMap> for caching
Flyweight Rc<T> / Arc<T> sharing
Bridge pass the implementation as a trait parameter
Extension methods extension traits

1. Adapter

An adapter makes an existing type fit an interface it wasn't written for. Implement the trait for a thin wrapper around the existing type.

Rust
// Our application's interface.
trait TemperatureSensor {
    fn celsius(&self) -> f64;
}

// A third-party type with a different API (imagine it's from another crate).
struct LegacyFahrenheitProbe {
    raw_reading: i32, // tenths of a degree Fahrenheit
}

impl LegacyFahrenheitProbe {
    fn read_tenths_f(&self) -> i32 {
        self.raw_reading
    }
}

// The adapter.
struct ProbeAdapter(LegacyFahrenheitProbe);

impl TemperatureSensor for ProbeAdapter {
    fn celsius(&self) -> f64 {
        (self.0.read_tenths_f() as f64 / 10.0 - 32.0) * 5.0 / 9.0
    }
}

struct ModernSensor(f64);

impl TemperatureSensor for ModernSensor {
    fn celsius(&self) -> f64 {
        self.0
    }
}

fn main() {
    let sensors: Vec<Box<dyn TemperatureSensor>> = vec![Box::new(ModernSensor(21.5)), Box::new(ProbeAdapter(LegacyFahrenheitProbe { raw_reading: 986 }))];
    for s in &sensors {
        println!("{:.1} C", s.celsius());
    }
}
Output
21.5 C
37.0 C

When the trait is yours, you could also implement it directly for LegacyFahrenheitProbe, with no wrapper at all. The wrapper is needed when both the trait and the type come from other crates, as example 3 shows.

2. Adapting closures

In Rust, a blanket implementation lets closures act as implementations of a trait: implement your trait for every closure with the right signature, and closures can be used wherever the trait is expected, side by side with structs.

Rust
trait Validator {
    fn validate(&self, input: &str) -> Result<(), String>;
}

struct MinLength(usize);

impl Validator for MinLength {
    fn validate(&self, input: &str) -> Result<(), String> {
        if input.chars().count() >= self.0 { Ok(()) } else { Err(format!("must be at least {} characters", self.0)) }
    }
}

// Every closure with the right signature is a Validator too.
impl<F> Validator for F
where
    F: Fn(&str) -> Result<(), String>,
{
    fn validate(&self, input: &str) -> Result<(), String> {
        self(input)
    }
}

fn problems(input: &str, rules: &[Box<dyn Validator>]) -> Vec<String> {
    rules.iter().filter_map(|rule| rule.validate(input).err()).collect()
}

fn main() {
    let rules: Vec<Box<dyn Validator>> = vec![
        Box::new(MinLength(8)),
        Box::new(|s: &str| if s.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err(String::from("needs a digit")) }),
        Box::new(|s: &str| if s.to_lowercase().contains("password") { Err(String::from("too obvious")) } else { Ok(()) }),
    ];
    for pw in ["hunter2", "MyPassword1", "correct-horse-7"] {
        println!("{pw:<16} {:?}", problems(pw, &rules));
    }
}
Output
hunter2          ["must be at least 8 characters"]
MyPassword1      ["too obvious"]
correct-horse-7  []

3. The orphan rule, and newtypes as adapters

You may implement a trait for a type only if the trait or the type is defined in your crate. This "orphan rule" stops two crates from providing conflicting implementations. So you can't implement the standard library's Display for the standard library's Vec:

Rust✗ does not compile · E0117
use std::fmt;

impl fmt::Display for Vec<String> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.join(", "))
    }
}

fn main() {}
Output
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
 --> src/main.rs:3:1
  |
3 | impl fmt::Display for Vec<String> {
  | ^^^^^^^^^^^^^^^^^^^^^^-----------
  |                       |
  |                       `Vec` is not defined in the current crate
  |
  = note: impl doesn't have any local type before any uncovered type parameters
  = note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
  = note: define and implement a trait or new type instead

The fix is a newtype: a one-field tuple struct that is your own type, wrapping the foreign one. It costs nothing at runtime.

Rust
use std::fmt;

struct Tags(Vec<String>);

impl fmt::Display for Tags {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.0.as_slice() {
            [] => write!(f, "(no tags)"),
            [first, rest @ ..] => {
                write!(f, "#{first}")?;
                for tag in rest {
                    write!(f, " #{tag}")?;
                }
                Ok(())
            }
        }
    }
}

fn main() {
    let posts = [Tags(vec![]), Tags(vec![String::from("rust")]), Tags(vec![String::from("rust"), String::from("java"), String::from("jvm")])];
    for tags in &posts {
        println!("{tags}");
    }
    println!("{} tags on the last post", posts[2].0.len()); // the Vec is still inside
}
Output
(no tags)
#rust
#rust #java #jvm
3 tags on the last post

4. Decorator at compile time

A decorator wraps an object with the same interface and adds behavior. Generic decorators (Uppercase<D>) stack at compile time: the whole chain is one concrete type, with no allocation and no dynamic dispatch.

Rust
trait DataSource {
    fn read(&self) -> String;
}

struct FileSource(String);

impl DataSource for FileSource {
    fn read(&self) -> String {
        self.0.clone()
    }
}

struct Uppercase<D: DataSource>(D);
struct Trimmed<D: DataSource>(D);
struct Counted<D: DataSource>(D);

impl<D: DataSource> DataSource for Uppercase<D> {
    fn read(&self) -> String {
        self.0.read().to_uppercase()
    }
}

impl<D: DataSource> DataSource for Trimmed<D> {
    fn read(&self) -> String {
        self.0.read().trim().to_string()
    }
}

impl<D: DataSource> DataSource for Counted<D> {
    fn read(&self) -> String {
        let s = self.0.read();
        format!("{s} ({} chars)", s.chars().count())
    }
}

fn main() {
    let raw = FileSource(String::from("   hello decorators   "));
    let decorated = Counted(Uppercase(Trimmed(raw)));
    println!("{}", decorated.read());
}
Output
HELLO DECORATORS (16 chars)

5. Decorators stacked at runtime

When the layers come from configuration, each decorator holds a Box<dyn Trait> and the chain is built in a loop. This is the textbook notifier example: a base email notifier, with SMS and Slack layers added on request.

Rust
trait Notifier {
    fn send(&self, message: &str) -> Vec<String>;
}

struct Email {
    to: String,
}

impl Notifier for Email {
    fn send(&self, message: &str) -> Vec<String> {
        vec![format!("email to {}: {message}", self.to)]
    }
}

// Each decorator wraps "some notifier" and adds a channel.
struct Sms {
    inner: Box<dyn Notifier>,
    phone: String,
}

struct Slack {
    inner: Box<dyn Notifier>,
    channel: String,
}

impl Notifier for Sms {
    fn send(&self, message: &str) -> Vec<String> {
        let mut sent = self.inner.send(message);
        sent.push(format!("sms to {}: {message}", self.phone));
        sent
    }
}

impl Notifier for Slack {
    fn send(&self, message: &str) -> Vec<String> {
        let mut sent = self.inner.send(message);
        sent.push(format!("slack {}: {message}", self.channel));
        sent
    }
}

fn build_notifier(channels: &[&str]) -> Box<dyn Notifier> {
    let mut notifier: Box<dyn Notifier> = Box::new(Email { to: String::from("ops@example.com") });
    for channel in channels {
        notifier = match *channel {
            "sms" => Box::new(Sms { inner: notifier, phone: String::from("+49 170 1234567") }),
            "slack" => Box::new(Slack { inner: notifier, channel: String::from("#alerts") }),
            _ => notifier,
        };
    }
    notifier
}

fn main() {
    let configs: [&[&str]; 3] = [&[], &["slack"], &["sms", "slack"]];
    for config in configs {
        println!("{config:?}:");
        for line in build_notifier(config).send("disk 90% full") {
            println!("  {line}");
        }
    }
}
Output
[]:
  email to ops@example.com: disk 90% full
["slack"]:
  email to ops@example.com: disk 90% full
  slack #alerts: disk 90% full
["sms", "slack"]:
  email to ops@example.com: disk 90% full
  sms to +49 170 1234567: disk 90% full
  slack #alerts: disk 90% full

6. Decorating functions

For single functions, a decorator is a function that takes a closure and returns (or runs) a wrapped version. logged returns a new closure that prints each call; retry runs a fallible operation until it succeeds or runs out of attempts.

Rust
use std::fmt::{Debug, Display};

// Returns a new function that behaves like `f` and logs each call.
fn logged<A: Debug, R: Debug>(name: &'static str, f: impl Fn(A) -> R) -> impl Fn(A) -> R {
    move |arg| {
        println!("-> {name}({arg:?})");
        let result = f(arg);
        println!("<- {name} = {result:?}");
        result
    }
}

// Runs `op` until it succeeds, at most `attempts` times.
fn retry<T, E: Display>(attempts: u32, mut op: impl FnMut() -> Result<T, E>) -> Result<T, E> {
    let mut attempt = 1;
    loop {
        match op() {
            Err(e) if attempt < attempts => {
                println!("attempt {attempt} failed: {e}; retrying");
                attempt += 1;
            }
            result => return result,
        }
    }
}

fn main() {
    let area = logged("area", |(w, h): (u32, u32)| w * h);
    let total = area((3, 4)) + area((5, 5));
    println!("total area: {total}");

    let mut calls = 0;
    let result = retry(5, || {
        calls += 1;
        if calls < 3 { Err(format!("timeout #{calls}")) } else { Ok("payload") }
    });
    println!("{result:?} after {calls} calls");
    println!("{:?}", retry(2, || Err::<(), _>("still down")));
}
Output
-> area((3, 4))
<- area = 12
-> area((5, 5))
<- area = 25
total area: 37
attempt 1 failed: timeout #1; retrying
attempt 2 failed: timeout #2; retrying
Ok("payload") after 3 calls
attempt 1 failed: still down; retrying
Err("still down")

7. Composite: an enum tree

The composite pattern treats individual objects and groups uniformly. A recursive enum expresses it directly: a node is either a leaf or a group of nodes, and one match handles both.

Rust
enum Node {
    File { name: String, bytes: u64 },
    Dir { name: String, children: Vec<Node> },
}

impl Node {
    fn size(&self) -> u64 {
        match self {
            Node::File { bytes, .. } => *bytes,
            Node::Dir { children, .. } => children.iter().map(Node::size).sum(),
        }
    }

    fn render(&self, depth: usize, out: &mut Vec<String>) {
        let indent = "  ".repeat(depth);
        match self {
            Node::File { name, bytes } => out.push(format!("{indent}{name} ({bytes} B)")),
            Node::Dir { name, children } => {
                out.push(format!("{indent}{name}/ ({} B)", self.size()));
                for c in children {
                    c.render(depth + 1, out);
                }
            }
        }
    }
}

fn file(name: &str, bytes: u64) -> Node {
    Node::File { name: name.into(), bytes }
}

fn main() {
    let tree = Node::Dir {
        name: "project".into(),
        children: vec![
            file("Cargo.toml", 420),
            Node::Dir { name: "src".into(), children: vec![file("main.rs", 2_100), file("lib.rs", 5_300)] },
            Node::Dir { name: "docs".into(), children: vec![] },
        ],
    };
    let mut lines = Vec::new();
    tree.render(0, &mut lines);
    println!("{}", lines.join("\n"));
}
Output
project/ (7820 B)
  Cargo.toml (420 B)
  src/ (7400 B)
    main.rs (2100 B)
    lib.rs (5300 B)
  docs/ (0 B)

8. Composite with trait objects

When other code must be able to add new kinds of nodes, as with widgets in a UI toolkit, make the node a trait. A container is just one more implementation that holds Vec<Box<dyn Widget>> and delegates to its children.

Rust
trait Widget {
    fn render(&self, indent: usize) -> Vec<String>;

    fn count(&self) -> usize {
        1
    }
}

struct Text(String);

struct Button {
    label: String,
}

struct Panel {
    title: String,
    children: Vec<Box<dyn Widget>>,
}

impl Widget for Text {
    fn render(&self, indent: usize) -> Vec<String> {
        vec![format!("{}\"{}\"", " ".repeat(indent), self.0)]
    }
}

impl Widget for Button {
    fn render(&self, indent: usize) -> Vec<String> {
        vec![format!("{}[{}]", " ".repeat(indent), self.label)]
    }
}

impl Widget for Panel {
    fn render(&self, indent: usize) -> Vec<String> {
        let mut lines = vec![format!("{}{}:", " ".repeat(indent), self.title)];
        for child in &self.children {
            lines.extend(child.render(indent + 2));
        }
        lines
    }

    fn count(&self) -> usize {
        1 + self.children.iter().map(|c| c.count()).sum::<usize>()
    }
}

fn main() {
    let form = Panel {
        title: String::from("Login"),
        children: vec![
            Box::new(Text(String::from("Welcome back"))),
            Box::new(Panel {
                title: String::from("Credentials"),
                children: vec![Box::new(Text(String::from("user name"))), Box::new(Text(String::from("password")))],
            }),
            Box::new(Button { label: String::from("Sign in") }),
        ],
    };
    println!("{}", form.render(0).join("\n"));
    println!("{} widgets", form.count());
}
Output
Login:
  "Welcome back"
  Credentials:
    "user name"
    "password"
  [Sign in]
6 widgets

9. Facade

A facade offers one simple entry point over a complicated subsystem. In Rust, a module with a few public functions over private submodules is a facade, as the Modules lesson showed with pub use.

Rust
mod video {
    mod decoder {
        pub fn decode(file: &str) -> Vec<u8> {
            file.bytes().collect()
        }
    }
    mod scaler {
        pub fn scale(frames: Vec<u8>, width: u32) -> Vec<u8> {
            frames.into_iter().take(width as usize).collect()
        }
    }
    mod encoder {
        pub fn encode(frames: &[u8], codec: &str) -> String {
            format!("{} frames encoded as {codec}", frames.len())
        }
    }

    // The facade: one call hides three subsystems and their ordering.
    pub fn convert_for_mobile(file: &str) -> String {
        let frames = decoder::decode(file);
        let small = scaler::scale(frames, 8);
        encoder::encode(&small, "h264")
    }
}

fn main() {
    println!("{}", video::convert_for_mobile("holiday_video.mov"));
}
Output
8 frames encoded as h264

10. Proxy: lazy loading

A proxy stands in for another object with the same interface. A virtual proxy delays creating an expensive object until it's needed. OnceCell (or OnceLock for thread-safe use) makes one a few lines long.

Rust
use std::cell::OnceCell;

struct HighResImage {
    pixels: Vec<u8>,
}

impl HighResImage {
    fn load(path: &str) -> HighResImage {
        println!("  (loading {path} from disk)");
        HighResImage { pixels: vec![0; 4_000_000] }
    }
}

struct ImageProxy {
    path: String,
    image: OnceCell<HighResImage>,
}

impl ImageProxy {
    fn new(path: &str) -> Self {
        ImageProxy { path: path.to_string(), image: OnceCell::new() }
    }

    fn file_name(&self) -> &str {
        &self.path // doesn't need the image
    }

    fn pixel_count(&self) -> usize {
        self.image.get_or_init(|| HighResImage::load(&self.path)).pixels.len()
    }
}

fn main() {
    let gallery: Vec<ImageProxy> = ["a.png", "b.png", "c.png"].map(ImageProxy::new).into_iter().collect();
    println!("thumbnails: {:?}", gallery.iter().map(|i| i.file_name()).collect::<Vec<_>>());
    println!("opened b.png: {} pixels", gallery[1].pixel_count());
    println!("opened b.png again: {} pixels", gallery[1].pixel_count());
}
Output
thumbnails: ["a.png", "b.png", "c.png"]
  (loading b.png from disk)
opened b.png: 4000000 pixels
opened b.png again: 4000000 pixels

11. Caching proxy

A caching proxy implements the same trait as a slow service and answers repeated requests from memory. The trait method takes &self, so the cache lives in a RefCell. Callers written against the trait can't tell the difference.

Rust
use std::cell::{Cell, RefCell};
use std::collections::HashMap;

trait WeatherService {
    fn forecast(&self, city: &str) -> String;
}

struct RemoteWeather {
    calls: Cell<u32>,
}

impl WeatherService for RemoteWeather {
    fn forecast(&self, city: &str) -> String {
        self.calls.set(self.calls.get() + 1); // pretend this is a slow HTTP call
        format!("{city}: {} C", city.bytes().map(u32::from).sum::<u32>() % 30)
    }
}

struct CachedWeather<S: WeatherService> {
    inner: S,
    cache: RefCell<HashMap<String, String>>,
}

impl<S: WeatherService> WeatherService for CachedWeather<S> {
    fn forecast(&self, city: &str) -> String {
        if let Some(hit) = self.cache.borrow().get(city) {
            return hit.clone();
        }
        let fresh = self.inner.forecast(city);
        self.cache.borrow_mut().insert(city.to_string(), fresh.clone());
        fresh
    }
}

fn dashboard(service: &impl WeatherService, cities: &[&str]) {
    for city in cities {
        println!("{}", service.forecast(city));
    }
}

fn main() {
    let service = CachedWeather { inner: RemoteWeather { calls: Cell::new(0) }, cache: RefCell::new(HashMap::new()) };
    dashboard(&service, &["Oslo", "Lima", "Oslo", "Oslo", "Lima"]);
    println!("remote calls: {}", service.inner.calls.get());
}
Output
Oslo: 23 C
Lima: 27 C
Oslo: 23 C
Oslo: 23 C
Lima: 27 C
remote calls: 2

A real cache also needs an expiry time and a size limit. Crates such as moka provide them; the structure stays the same.

12. Protection proxy

A protection proxy checks permissions before forwarding each call. Deriving PartialOrd on the Role enum orders the variants as they're declared, so "at least an editor" is a plain comparison.

Rust
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
enum Role {
    Viewer,
    Editor,
    Admin,
}

trait Documents {
    fn read(&self, id: u32) -> Result<String, String>;
    fn update(&mut self, id: u32, text: &str) -> Result<(), String>;
    fn delete(&mut self, id: u32) -> Result<(), String>;
}

struct Store {
    docs: Vec<(u32, String)>,
}

impl Documents for Store {
    fn read(&self, id: u32) -> Result<String, String> {
        self.docs.iter().find(|(d, _)| *d == id).map(|(_, text)| text.clone()).ok_or(format!("no document {id}"))
    }
    fn update(&mut self, id: u32, text: &str) -> Result<(), String> {
        let doc = self.docs.iter_mut().find(|(d, _)| *d == id).ok_or(format!("no document {id}"))?;
        doc.1 = text.to_string();
        Ok(())
    }
    fn delete(&mut self, id: u32) -> Result<(), String> {
        let before = self.docs.len();
        self.docs.retain(|(d, _)| *d != id);
        if self.docs.len() < before { Ok(()) } else { Err(format!("no document {id}")) }
    }
}

// The proxy: same interface, checks first.
struct Guarded<D: Documents> {
    inner: D,
    role: Role,
}

impl<D: Documents> Guarded<D> {
    fn require(&self, needed: Role, action: &str) -> Result<(), String> {
        if self.role >= needed { Ok(()) } else { Err(format!("{:?} may not {action}", self.role)) }
    }
}

impl<D: Documents> Documents for Guarded<D> {
    fn read(&self, id: u32) -> Result<String, String> {
        self.require(Role::Viewer, "read")?;
        self.inner.read(id)
    }
    fn update(&mut self, id: u32, text: &str) -> Result<(), String> {
        self.require(Role::Editor, "update")?;
        self.inner.update(id, text)
    }
    fn delete(&mut self, id: u32) -> Result<(), String> {
        self.require(Role::Admin, "delete")?;
        self.inner.delete(id)
    }
}

fn main() {
    for role in [Role::Viewer, Role::Editor, Role::Admin] {
        let store = Store { docs: vec![(1, String::from("draft")), (2, String::from("notes"))] };
        let mut docs = Guarded { inner: store, role };
        println!("{role:?}:");
        println!("  read   {:?}", docs.read(1));
        println!("  update {:?}", docs.update(1, "final"));
        println!("  delete {:?}", docs.delete(2));
        println!("  now    {:?} {:?}", docs.read(1), docs.read(2));
    }
}
Output
Viewer:
  read   Ok("draft")
  update Err("Viewer may not update")
  delete Err("Viewer may not delete")
  now    Ok("draft") Ok("notes")
Editor:
  read   Ok("draft")
  update Ok(())
  delete Err("Editor may not delete")
  now    Ok("final") Ok("notes")
Admin:
  read   Ok("draft")
  update Ok(())
  delete Ok(())
  now    Ok("final") Err("no document 2")

13. Flyweight: shared immutable data

Flyweights share heavy immutable data among many objects. Rc<str> (or Arc<str> across threads) shares one allocation; cloning it copies a pointer (Rc & Weak).

Rust
use std::collections::HashMap;
use std::rc::Rc;

struct Tree {
    x: u32,
    y: u32,
    species: Rc<str>, // shared, not copied per tree
}

#[derive(Default)]
struct SpeciesPool(HashMap<String, Rc<str>>);

impl SpeciesPool {
    fn get(&mut self, name: &str) -> Rc<str> {
        Rc::clone(self.0.entry(name.to_string()).or_insert_with(|| Rc::from(name)))
    }
}

fn main() {
    let mut pool = SpeciesPool::default();
    let forest: Vec<Tree> = (0..1000).map(|i| Tree { x: i % 40, y: i / 40, species: pool.get(if i % 3 == 0 { "oak" } else { "birch" }) }).collect();

    let oak = pool.get("oak");
    println!("{} trees, {} species strings stored", forest.len(), pool.0.len());
    println!("oak is shared by {} handles", Rc::strong_count(&oak) - 2); // minus the pool's and ours
    println!("tree 999 at ({}, {}) is {}", forest[999].x, forest[999].y, forest[999].species);
}
Output
1000 trees, 2 species strings stored
oak is shared by 334 handles
tree 999 at (39, 24) is oak

14. Bridge: two dimensions that vary independently

The bridge pattern separates what is drawn from how it's drawn, so that shapes and renderers can each grow without a subclass for every combination (SvgCircle, SvgSquare, CanvasCircle, ...). In Rust, the "how" is a trait that the "what" receives as a parameter.

Rust
trait Renderer {
    fn circle(&self, x: f64, y: f64, r: f64) -> String;
    fn rect(&self, x: f64, y: f64, w: f64, h: f64) -> String;
}

struct Svg;
struct Canvas2d;

impl Renderer for Svg {
    fn circle(&self, x: f64, y: f64, r: f64) -> String {
        format!(r#"<circle cx="{x}" cy="{y}" r="{r}"/>"#)
    }
    fn rect(&self, x: f64, y: f64, w: f64, h: f64) -> String {
        format!(r#"<rect x="{x}" y="{y}" width="{w}" height="{h}"/>"#)
    }
}

impl Renderer for Canvas2d {
    fn circle(&self, x: f64, y: f64, r: f64) -> String {
        format!("ctx.arc({x}, {y}, {r}, 0, 2 * Math.PI)")
    }
    fn rect(&self, x: f64, y: f64, w: f64, h: f64) -> String {
        format!("ctx.rect({x}, {y}, {w}, {h})")
    }
}

trait Shape {
    fn draw(&self, renderer: &dyn Renderer) -> Vec<String>;
}

struct Dot {
    x: f64,
    y: f64,
}

struct Badge {
    x: f64,
    y: f64,
    size: f64,
}

impl Shape for Dot {
    fn draw(&self, renderer: &dyn Renderer) -> Vec<String> {
        vec![renderer.circle(self.x, self.y, 1.0)]
    }
}

impl Shape for Badge {
    fn draw(&self, renderer: &dyn Renderer) -> Vec<String> {
        let half = self.size / 2.0;
        vec![renderer.rect(self.x, self.y, self.size, self.size), renderer.circle(self.x + half, self.y + half, half / 2.0)]
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Dot { x: 5.0, y: 5.0 }), Box::new(Badge { x: 10.0, y: 0.0, size: 8.0 })];
    let renderers: [(&str, &dyn Renderer); 2] = [("svg", &Svg), ("canvas", &Canvas2d)];
    for (name, renderer) in renderers {
        println!("{name}:");
        for shape in &shapes {
            for line in shape.draw(renderer) {
                println!("  {line}");
            }
        }
    }
}
Output
svg:
  <circle cx="5" cy="5" r="1"/>
  <rect x="10" y="0" width="8" height="8"/>
  <circle cx="14" cy="4" r="2"/>
canvas:
  ctx.arc(5, 5, 1, 0, 2 * Math.PI)
  ctx.rect(10, 0, 8, 8)
  ctx.arc(14, 4, 2, 0, 2 * Math.PI)

Two shapes and two renderers take four impl blocks, and a third renderer is one more, not one more per shape.

15. Extension traits: methods on types you don't own

Rust can add methods to types you don't own, such as String or Duration, with an extension trait: define a trait with the new methods and implement it for the existing type. The methods are available wherever the trait is in scope.

Rust
use std::time::Duration;

trait StrExt {
    fn is_blank(&self) -> bool;
    fn truncate_with_ellipsis(&self, max_chars: usize) -> String;
}

impl StrExt for str {
    fn is_blank(&self) -> bool {
        self.trim().is_empty()
    }

    fn truncate_with_ellipsis(&self, max_chars: usize) -> String {
        if self.chars().count() <= max_chars {
            return self.to_string();
        }
        let kept: String = self.chars().take(max_chars.saturating_sub(3)).collect();
        format!("{kept}...")
    }
}

trait DurationExt {
    fn human(&self) -> String;
}

impl DurationExt for Duration {
    fn human(&self) -> String {
        let s = self.as_secs();
        match s {
            0..=59 => format!("{s}s"),
            60..=3_599 => format!("{}m {}s", s / 60, s % 60),
            _ => format!("{}h {}m", s / 3_600, s % 3_600 / 60),
        }
    }
}

fn main() {
    println!("{} {}", "   ".is_blank(), " x ".is_blank());
    let title = String::from("Rust for Java developers: the complete course");
    println!("{}", title.truncate_with_ellipsis(24)); // works on String too, through deref
    for secs in [42, 754, 7_380] {
        println!("{}", Duration::from_secs(secs).human());
    }
}
Output
true false
Rust for Java develop...
42s
12m 34s
2h 3m

This is how crates extend standard types: itertools::Itertools adds dozens of iterator methods, and async crates add StreamExt methods to streams. Because the trait must be imported, nobody gets the new methods by surprise.

16. Realistic example: a layered repository

A user directory built from four structural patterns: an adapter makes a legacy CSV source look like a UserRepo, a caching proxy avoids repeated lookups, a logging decorator records each call, and a facade hides the whole stack behind one method.

Rust
use std::cell::RefCell;
use std::collections::HashMap;

#[derive(Debug, Clone)]
struct User {
    id: u32,
    name: String,
    team: String,
}

trait UserRepo {
    fn find(&self, id: u32) -> Option<User>;
}

// A legacy source with an API of its own: raw CSV lines.
struct LegacyCsv {
    lines: Vec<&'static str>,
}

impl LegacyCsv {
    fn line_for(&self, id: u32) -> Option<&'static str> {
        self.lines.iter().copied().find(|line| line.split(',').next().and_then(|f| f.parse::<u32>().ok()) == Some(id))
    }
}

// Adapter: CSV lines become `User`s.
struct CsvRepo(LegacyCsv);

impl UserRepo for CsvRepo {
    fn find(&self, id: u32) -> Option<User> {
        println!("    (scanning CSV for {id})");
        let fields: Vec<&str> = self.0.line_for(id)?.split(',').collect();
        Some(User { id, name: fields.get(1)?.to_string(), team: fields.get(2)?.to_string() })
    }
}

// Proxy: remembers answers, including "not found".
struct Cached<R: UserRepo> {
    inner: R,
    cache: RefCell<HashMap<u32, Option<User>>>,
}

impl<R: UserRepo> UserRepo for Cached<R> {
    fn find(&self, id: u32) -> Option<User> {
        if let Some(answer) = self.cache.borrow().get(&id) {
            return answer.clone();
        }
        let answer = self.inner.find(id);
        self.cache.borrow_mut().insert(id, answer.clone());
        answer
    }
}

// Decorator: logs every call, whatever is underneath.
struct Logged<R: UserRepo>(R);

impl<R: UserRepo> UserRepo for Logged<R> {
    fn find(&self, id: u32) -> Option<User> {
        let result = self.0.find(id);
        println!("  find({id}) -> {}", if result.is_some() { "found" } else { "none" });
        result
    }
}

// Facade: callers see one type with one method.
struct UserDirectory {
    repo: Box<dyn UserRepo>,
}

impl UserDirectory {
    fn from_csv(lines: Vec<&'static str>) -> UserDirectory {
        let stack = Logged(Cached { inner: CsvRepo(LegacyCsv { lines }), cache: RefCell::new(HashMap::new()) });
        UserDirectory { repo: Box::new(stack) }
    }

    fn display_name(&self, id: u32) -> String {
        match self.repo.find(id) {
            Some(u) => format!("{} ({}, #{})", u.name, u.team, u.id),
            None => format!("unknown user #{id}"),
        }
    }
}

fn main() {
    let directory = UserDirectory::from_csv(vec!["1,Ana Lima,payments", "2,Ben Okafor,search", "3,Cy Park,infra"]);
    for id in [2, 2, 9, 9, 1] {
        println!("{}", directory.display_name(id));
    }
}
Output
    (scanning CSV for 2)
  find(2) -> found
Ben Okafor (search, #2)
  find(2) -> found
Ben Okafor (search, #2)
    (scanning CSV for 9)
  find(9) -> none
unknown user #9
  find(9) -> none
unknown user #9
    (scanning CSV for 1)
  find(1) -> found
Ana Lima (payments, #1)

Summary

Pattern Rust
Adapter a newtype implementing your trait; a blanket impl<F: Fn(...)> to accept closures
Orphan rule workaround wrap the foreign type in a newtype
Decorator generic wrappers for compile-time stacks, Box<dyn Trait> layers for runtime stacks, higher-order functions for single functions
Composite a recursive enum (closed set) or Vec<Box<dyn Trait>> children (open set)
Facade a module or struct with a small public API
Proxy a wrapper implementing the same trait: OnceCell (lazy), RefCell<HashMap> (cache), checks before forwarding (protection)
Flyweight Rc<T> / Arc<T>
Bridge the implementation passed in as a trait object or generic parameter
Extension methods extension traits

Next: Behavioral Patterns: strategy, observer, command, state, visitor, and the rest, often replaced by a closure or an enum.