Design Patterns in Rust: A Practical Guide with Examples
Design patterns are proven solutions to typical development problems. In Rust, due to its unique ownership system and memory model, the implementation of classic patterns often differs from languages with garbage collection. In this article, we will cover three key patterns: Singleton, Builder, and Strategy, adapted to Rust's features.
1. Singleton Pattern in Rust: Global State Without unsafe
Singleton guarantees that a class has only one instance and provides a global access point to it. In Rust, the once_cell crate or the stable std::sync::OnceLock (since Rust 1.70) is ideal for this.
Implementation with OnceLock
use std::sync::OnceLock;
pub struct Logger { level: String,}
impl Logger { pub fn global() -> &'static Logger { static INSTANCE: OnceLock<Logger> = OnceLock::new(); INSTANCE.get_or_init(|| Logger { level: "info".to_string(), }) }
pub fn log(&self, msg: &str) { println!("[{}] {}", self.level, msg); }}
fn main() { let logger = Logger::global(); logger.log("Hello, Rust!"); // Logger::global() will return the same instance}Advantages: Safety (no unsafe), lazy initialization, thread safety. Disadvantage — global state complicates testing.
2. Builder Pattern: Constructing Complex Objects
Builder separates the construction of a complex object from its representation. In Rust, it is especially useful for structs with many optional fields.
Example: HTTP Client Configuration
pub struct HttpClient { url: String, timeout: u64, headers: Vec<(String, String)>,}
pub struct HttpClientBuilder { url: String, timeout: Option<u64>, headers: Vec<(String, String)>,}
impl HttpClientBuilder { pub fn new(url: &str) -> Self { HttpClientBuilder { url: url.to_string(), timeout: None, headers: vec![], } }
pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(secs); self }
pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
pub fn build(self) -> HttpClient { HttpClient { url: self.url, timeout: self.timeout.unwrap_or(30), headers: self.headers, } }}
fn main() { let client = HttpClientBuilder::new("https://api.example.com") .timeout(10) .header("Authorization", "Bearer token123") .build(); println!("Client URL: {}", client.url);}Important: Using self (not &self) in builder methods avoids cloning and makes the API more ergonomic.
3. Strategy Pattern: Algorithms as Interchangeable Strategies
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. In Rust, this is implemented via traits and dynamic dispatch (Box<dyn Trait>).
Example: Sorting System
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { let n = data.len(); for i in 0..n { for j in 0..n - i - 1 { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
struct QuickSort;impl SortStrategy for QuickSort { fn sort(&self, da
```