Design Patterns in Rust with Examples: Advanced Level
Rust is a language that changes the approach to design. Classic GoF patterns often require adaptation due to the strict ownership system, lifetimes, and lack of inheritance. In this article, we will explore advanced design patterns that not only work in Rust but leverage its strengths: memory safety, zero-cost abstractions, and a powerful type system.
1. Builder Pattern with Compile-Time Checks
The classic Builder in Rust is usually implemented using a struct with optional fields. At an advanced level, we can use the type system to check mandatory fields at compile time. This eliminates runtime errors like "forgot to set a required field."
Implementation via Type State makes the Builder safe and self-documenting.
// States for Builderstruct Empty;struct WithName(String);struct WithAge(u8);struct Ready(WithName, WithAge);
struct PersonBuilder<State> { state: State,}
impl PersonBuilder<Empty> { pub fn new() -> Self { PersonBuilder { state: Empty } } pub fn name(self, name: &str) -> PersonBuilder<WithName> { PersonBuilder { state: WithName(name.to_string()), } }}
impl PersonBuilder<WithName> { pub fn age(self, age: u8) -> PersonBuilder<Ready> { PersonBuilder { state: Ready(self.state, WithAge(age)), } }}
impl PersonBuilder<Ready> { pub fn build(self) -> Person { Person { name: self.state.0 .0, age: self.state.1 .0, } }}
struct Person { name: String, age: u8,}
fn main() { // Compiles - all fields are set let person = PersonBuilder::new() .name("Alice") .age(30) .build(); // Will not compile: missing name() // let person = PersonBuilder::new().age(30).build(); println!("{} is {} years old", person.name, person.age);}Advantage: configuration errors are caught at compile time, not at runtime. This is especially valuable for complex objects with dozens of parameters.
2. RAII Pattern and Smart Pointers for Resource Management
RAII (Resource Acquisition Is Initialization) is not just a pattern, but a philosophy of Rust. Every resource (memory, file, socket, lock) should be acquired in the constructor and released in the destructor. Rust implements this through the Drop trait and the ownership system.
An advanced example is creating an RAII wrapper for a mutex with automatic release.
use std::ops::Deref;use std::sync::{Arc, Mutex};
struct Guard<T> { data: T,}
impl<T> Drop for Guard<T> { fn drop(&mut self) { println!("Resource released!"); }}
impl<T> Deref for Guard<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.data }}
struct ResourceManager<T> { resource: Arc<Mutex<T>>,}
impl<T> ResourceManager<T> { fn new(data: T) -> Self { ResourceManager { resource: Arc::new(Mutex::new(data)), } } fn acquire(&self) -> Guard<std::sync::MutexGuard<T>> { let guard = self.resource.lock().unwrap(); Guard { data: guard } }}
fn main() { let manager = ResourceManager::new(42); { let guard = manager.acquire(); println!("Value: {}", *guard); // guard goes out of scope -> automatically releases the mutex } // Prints "Resource released!" println!("Mutex is available again");}Key idea: the destructor is