Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Patterns and Antipatterns

A pattern is a shape that recurs because it solves a recurring problem. The Gang of Four catalogued twenty-three of them for object-oriented design, and a good half of those exist to work around the absence of first-class functions or sum types. Rust has both, so several of the classic patterns dissolve: Strategy is a closure, Command is a closure, Visitor is a match, Iterator is in the standard library, and Singleton is usually a mistake.

What is left is a smaller set of shapes that are genuinely Rust’s, plus a list of habits that produce code which compiles and fights you. This chapter is both, written to be recognized on sight rather than read through.

Patterns

Newtype

A single-field tuple struct wrapping another type, to make a distinction the compiler enforces.

pub struct Sanitized(String);

impl Sanitized {
    pub fn new(raw: &str) -> Sanitized {
        Sanitized(raw.replace('<', "&lt;").replace('>', "&gt;"))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn render(content: &Sanitized) -> String {
    format!("<p>{}</p>", content.as_str())
}

fn main() {
    println!("{}", render(&Sanitized::new("<script>alert(1)</script>")));
}

Use it for identifiers, units, validated values, and anything where two values of the same underlying type mean different things. It costs nothing at runtime, and it is also the way past the orphan rule when you need to implement a foreign trait for a foreign type.

Builder

A separate type that accumulates configuration and produces the real value, solving the problem that Rust has neither optional parameters nor overloading.

pub struct Server {
    host: String,
    port: u16,
    threads: usize,
}

pub struct ServerBuilder {
    host: String,
    port: u16,
    threads: usize,
}

impl ServerBuilder {
    pub fn new() -> ServerBuilder {
        ServerBuilder { host: String::from("localhost"), port: 8080, threads: 4 }
    }

    pub fn host(mut self, host: &str) -> ServerBuilder {
        self.host = String::from(host);
        self
    }

    pub fn port(mut self, port: u16) -> ServerBuilder {
        self.port = port;
        self
    }

    pub fn build(self) -> Server {
        Server { host: self.host, port: self.port, threads: self.threads }
    }
}

fn main() {
    let server = ServerBuilder::new().host("0.0.0.0").port(443).build();
    println!("{}:{} on {} threads", server.host, server.port, server.threads);
}

Each setter takes self and returns it, which is what makes the calls chain. Reach for it when a type has more than three or four construction parameters, or when most of them have sensible defaults. For the simpler case where the defaults are all you need, #[derive(Default)] plus struct update syntax — Server { port: 443, ..Default::default() } — does the job with no builder at all.

Typestate

A state machine encoded as separate types, with transitions that consume the previous state. Chapter 6 developed this at length. The signature is a method taking self by value and returning a different type, and the payoff is that calling an operation in the wrong state is a compile error rather than a runtime one.

The RAII guard

A value whose destruction performs an action, so that the action cannot be skipped.

struct Timer {
    label: &'static str,
}

impl Timer {
    fn start(label: &'static str) -> Timer {
        println!("[{label}] start");
        Timer { label }
    }
}

impl Drop for Timer {
    fn drop(&mut self) {
        println!("[{}] done", self.label);
    }
}

fn process() {
    let _timer = Timer::start("process");
    println!("working");
}

fn main() {
    process();
}

MutexGuard, Ref, File and TempDir are all instances. Write one whenever “finished” means an action rather than merely freeing memory: releasing a lock, committing or rolling back, restoring a terminal, removing a temporary file, closing a span in a trace. Bind it to a name — let _guard = ..., not let _ = ..., which drops it immediately.

The extension trait

A trait defined solely to add methods to a type you do not own.

trait Truncate {
    fn ellipsize(&self, width: usize) -> String;
}

impl Truncate for str {
    fn ellipsize(&self, width: usize) -> String {
        if self.chars().count() <= width {
            self.to_string()
        } else {
            let kept: String = self.chars().take(width.saturating_sub(1)).collect();
            format!("{kept}…")
        }
    }
}

fn main() {
    println!("{}", "the rust thought process".ellipsize(12));
}

Name them ...Ext by convention when they are part of a public API. It is how crates add capability to standard types without wrappers.

Sans-IO

Separate the logic that decides from the code that performs input and output. A parser that takes bytes and returns events, a protocol state machine that takes messages and returns messages to send, a rules engine that takes facts and returns decisions — none of them touching a socket or a file.

The Rust-specific reason this pays is that pure logic needs no async, no lifetimes tied to a connection, and no mocking framework. It is testable with ordinary values, it works under any runtime, and the I/O shell around it is small enough to review. The Policy trait in chapter 16 is a small instance of the same instinct.

Identity over pointers

When two things refer to each other, give one an identifier and let the other hold it. Chapter 2 introduced this and chapter 12 developed it into the arena. Recognize it as a pattern rather than a workaround: identifiers serialize, compare, hash, cross threads, and survive the thing they name being moved in memory. Pointers do none of that.

Antipatterns

Cloning to make the compiler stop

The most common, and the one with the longest tail. A borrow error appears, .clone() removes it, and the design question goes unasked.

The problem is not the allocation. It is that each clone hides a decision that was never made — usually that a function is taking ownership when it wanted a borrow, or that two parts of the program are arguing about who owns something. Chapter 2’s four answers are the checklist: does one of them only need to read, can they take turns, do they genuinely need independent values, or should the thing be identified rather than pointed at.

Cloning deliberately, in cold code, for a reason you can state, is fine.

unwrap as punctuation

Option and Result exist to force a decision at the point where absence or failure appears. unwrap answers “crash”, which is sometimes right and is rarely the answer you would have chosen if asked.

The test from chapter 7: can you state, in one sentence, why the failure is impossible, and is that sentence about the code rather than about what you hope the world will do? If yes, use expect with that sentence as the message. If no, the type was pointing at a case you have not designed.

Rc<RefCell<T>> by default

Reaching for shared ownership plus interior mutability the first time the borrow checker objects reproduces the language you came from, at the cost of runtime panics where compile errors used to be and no static knowledge of who owns anything.

It is the right tool for genuine shared mutable graphs. It is the wrong tool for “I could not work out how to structure this”, and the difference is whether you walked down chapter 12’s ladder first.

Stringly-typed everything

Identifiers, states, kinds and categories represented as String, compared with == against literals, with typos discovered at runtime. A status field holding "approved" is an enum that has not been written yet, and a String parameter named customer_id is a newtype that has not been written yet.

The tell is a comparison against a string literal, or a function whose parameters are three Strings in an order nobody can remember.

Lifetimes on a storage type

Adding <'a> to a struct so that a field can be &str instead of String, and then threading the parameter through every type that contains it. Chapter 4’s rule: a lifetime parameter says the type is a temporary view. If the type is stored, returned up several layers, or held across an await, it should own its data, and the allocation you avoided was cheaper than the complexity you bought.

Deref for reuse

Implementing Deref on a newtype so that the inner type’s methods appear on the outer one, as a substitute for inheritance. It works until it does not: method resolution follows Deref and trait resolution does not, so the type appears to have methods it cannot use generically, and the error messages are poor. Deref is for smart pointers. Write the delegating methods.

&String, &Vec<T>, &Box<T> in signatures

Taking a reference to an owned container when a slice would do. &str accepts string literals and sub-ranges; &String does not. &[T] accepts arrays, vectors and slices; &Vec<T> does not. There is nothing you can do with the narrow form that you cannot do with the wide one, so the narrow form only restricts callers.

The same applies to impl AsRef<Path> versus &PathBuf, and to taking String by value when the function only reads it.

The god struct

One type with forty fields and a hundred methods, because everything needs everything. Rust makes this painful earlier than other languages do, since a method taking &mut self borrows all of it and blocks every other access.

Chapter 3 named the signal: when the same borrow conflict keeps appearing, the fields involved want to be their own type. Treat recurring borrow errors in one place as structural feedback rather than as an obstacle.

Abstraction bought on speculation

A trait with one implementor, a generic parameter with one instantiation, a Box<dyn Trait> where a concrete type would do. Each costs indirection, compile time and a layer for the reader to see through, and returns nothing until the second implementation arrives.

The Rust-specific version is over-generic function signatures: six type parameters with eight bounds, where two concrete types would have been clearer and just as reusable. Write the concrete version, and generalize when the second case shows up.

Ignoring a Result

let _ = fallible(); discards a failure silently, which is the empty catch block with better syntax. Where ignoring is genuinely right — best-effort cleanup, a log write that must not take down the program — make the decision visible by logging the error explicitly, so the next reader sees a choice rather than an oversight.

Conclusion

The patterns worth knowing in Rust are mostly small: a wrapper that makes a distinction real, a builder for types with many options, a state machine in the type system, a value whose destruction does the cleanup, a trait that adds methods from outside, logic separated from I/O, and identity in place of pointers. Several classic object-oriented patterns are absent because the language provides directly what they were reconstructing.

The antipatterns share a single root, which is using a mechanism to stop the compiler talking rather than to answer what it asked. clone, unwrap, Rc<RefCell<T>> and a speculative lifetime parameter are all legitimate tools and all common ways to avoid a decision. The question that separates the two cases is the same every time: can you say what the design reason was?

That question is what this book has been about. The compiler asks who owns this, who may change it, how long it lives, and what states it can be in. Those are questions about your program, not about Rust, and they had answers before you started writing Rust. Now they are written down, and something checks them.

Questions to think through

Pick three patterns from the Gang of Four catalogue and decide what each becomes in Rust. Which ones disappear entirely, and what feature replaced them?

Search a Rust project you have written for .clone(). For each one, write the sentence explaining why a second independent value was needed. How many sentences are about the design, and how many are about the compiler?

The chapter claims the antipatterns share one root. Test it: take one you recognize from your own code and ask what question you were avoiding when you wrote it.