Design Patterns in Rust: From Theory to Practice
Design patterns are proven solutions to common problems in software development. In Rust, due to its unique ownership system and the absence of classical OOP, many classic patterns take on a new meaning. In this article, we will cover four key patterns: Singleton, Factory, Builder, and Strategy, adapted for idiomatic Rust.
1. Singleton in Rust
Rust does not have global static variables in the conventional sense. However, a Singleton can be implemented using lazy_static or OnceCell. Let's look at an example with std::sync::OnceLock (available since Rust 1.70).
Implementation with OnceLock
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
#[derive(Debug)]struct Config { db_url: String, port: u16,}
fn get_config() -> &'static Config { CONFIG.get_or_init(|| { Config { db_url: String::from("postgres://localhost"), port: 5432, } })}
fn main() { let cfg = get_config(); println!("{:?}", cfg);}Key point: OnceLock guarantees single initialization and thread safety. This is an idiomatic approach for Rust.
2. Factory Pattern via Traits
The Factory pattern in Rust is perfectly implemented using traits. We don't need abstract classes — it's enough to define an interface through a trait.
Example: creating vehicles
trait Transport { fn deliver(&self);}
struct Truck;impl Transport for Truck { fn deliver(&self) { println!("Delivery by truck on land"); }}
struct Ship;impl Transport for Ship { fn deliver(&self) { println!("Delivery by ship by sea"); }}
struct LogisticsFactory;
impl LogisticsFactory { fn create_transport(route_type: &str) -> Box<dyn Transport> { match route_type { "land" => Box::new(Truck), "sea" => Box::new(Ship), _ => panic!("Unknown route type"), } }}
fn main() { let transport = LogisticsFactory::create_transport("sea"); transport.deliver();}Advantage: Rust does not require inheritance — polymorphism is achieved through traits and Box<dyn Trait>.
3. Builder Pattern with a Typed Chain
The Builder pattern is widely used in Rust, especially for structures with many optional parameters. We implement it using a typed method chain that returns &mut Self.
Example: creating an HTTP request
#[derive(Debug, Default)]struct HttpRequest { url: String, method: String, headers: Vec<(String, String)>, body: Option<String>,}
struct HttpRequestBuilder { request: HttpRequest,}
impl HttpRequestBuilder { fn new(url: &str) -> Self { HttpRequestBuilder { request: HttpRequest { url: url.to_string(), method: "GET".to_string(), ..Default::default() }, } }
fn method(&mut self, method: &str) -> &mut Self { self.request.method = method.to_string(); self }
fn header(&mut self, key: &str, value: &str) -> &mut Self { self.request.headers.push((key.to_string(), value.to_string())); self }
fn body(&mut self, body: &str) -> &mut Self { self.request.body = Some(body.to_string()); self }
fn build(&self) -> HttpRequest { // Validation can be added here HttpRequest { ..self.request.clone() } }}
fn ma