Design Patterns in Rust with Examples
Design patterns are proven solutions to typical problems in software development. In Rust, due to its unique ownership, borrowing, and lifetime system, implementing classic GoF patterns often requires a special approach. In this article, we will examine several popular patterns using real Rust code examples.
Why does Rust change the approach to patterns?
Unlike garbage-collected languages (Java, C#), Rust imposes strict rules on memory ownership. This makes some classic implementations (for example, Singleton through global mutable state) unsafe or inefficient. However, Rust offers its own elegant alternatives based on types and traits.
1. Builder — step-by-step object creation
Builder is one of the most frequently used patterns in Rust. It is ideal for configuring complex structures, especially when there are many optional fields.
Example: creating an HTTP request
struct Request { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
struct RequestBuilder { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
impl RequestBuilder { fn new(method: &str, url: &str) -> Self { Self { method: method.to_string(), url: url.to_string(), headers: Vec::new(), body: None, } }
fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
fn body(mut self, body: &str) -> Self { self.body = Some(body.to_string()); self }
fn build(self) -> Request { Request { method: self.method, url: self.url, headers: self.headers, body: self.body, } }}
fn main() { let req = RequestBuilder::new("POST", "https://api.example.com/data") .header("Content-Type", "application/json") .header("Authorization", "Bearer token123") .body(r#"{"key": "value"}"#) .build();
println!("URL: {}", req.url); println!("Headers count: {}", req.headers.len());}Advantages of the approach in Rust
- Safety: the object is created only after calling
build()— incomplete configuration is impossible. - Readability: the method chain is intuitively understandable.
- Type safety: the compiler checks types at every step.
2. Strategy — choosing an algorithm on the fly
Strategy allows changing object behavior at runtime. In Rust, this is implemented through traits and dynamic dispatch (Box with traits) or static dispatch (generics).
Example: data sorting
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;struct QuickSort;
impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { // Simplest bubble sort implementation for i in 0..data.len() { for j in 0..data.len() - 1 - i { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
impl SortStrategy for QuickSort { fn sort(&self, data: &mut [i32]) { // Quick sort (simplified) if data.len() > 1 { let pivot = data.len() / 2; data.sort(); // Using built-in for the example } }}
struct Sorter { strategy: Box<dyn SortStrategy>,}
impl Sorter { fn new(strategy: Box<dyn SortStrategy>) ->