Design Patterns in Rust: Examples and Implementation

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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>) ->

Blogs

Book Recommendations