Rust · lesson 28 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Also part of: Low-Level Design
Design Patterns in Rust
Implement idiomatic Rust design patterns — builder, newtype, typestate, RAII guards, strategy closures and observers — as complete runnable programs.
By SkillByExample · Updated
Builder Pattern
The builder pattern constructs complex objects step by step. In Rust it is especially useful because you cannot have optional fields set to undefined — everything must be initialized.
#[derive(Debug)]
#[allow(dead_code)] // fields are only inspected through Debug in this demo
struct Request {
url: String,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
follow_redirects: bool,
}
#[derive(Default)]
struct RequestBuilder {
url: Option<String>,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
follow_redirects: bool,
}
impl RequestBuilder {
fn new() -> Self {
Self {
method: "GET".to_string(),
timeout_ms: 30_000,
follow_redirects: true,
..Default::default()
}
}
fn url(mut self, url: &str) -> Self {
self.url = Some(url.to_string());
self
}
fn method(mut self, method: &str) -> Self {
self.method = method.to_uppercase();
self
}
fn header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = Some(body.into());
self
}
fn timeout(mut self, ms: u64) -> Self {
self.timeout_ms = ms;
self
}
fn build(self) -> Result<Request, String> {
let url = self.url.ok_or("URL is required")?;
Ok(Request {
url,
method: self.method,
headers: self.headers,
body: self.body,
timeout_ms: self.timeout_ms,
follow_redirects: self.follow_redirects,
})
}
}
fn main() {
let request = RequestBuilder::new()
.url("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body(r#"{"name":"Alice"}"#)
.timeout(5_000)
.build()
.unwrap();
println!("{:#?}", request);
}
$ rustc design_patterns_builder_pattern.rs && ./design_patterns_builder_pattern
Request {
url: "https://api.example.com/users",
method: "POST",
headers: [
(
"Content-Type",
"application/json",
),
(
"Authorization",
"Bearer token123",
),
],
body: Some(
[
123,
34,
110,
97,
109,
101,
34,
58,
34,
65,
108,
105,
99,
101,
34,
125,
],
),
timeout_ms: 5000,
follow_redirects: true,
}
Newtype Pattern
Wrap a primitive in a tuple struct to create a semantically distinct type and prevent mixing values with the same underlying type:
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Seconds(f64);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);
impl Meters {
fn value(self) -> f64 { self.0 }
}
impl Seconds {
fn value(self) -> f64 { self.0 }
}
impl std::ops::Div<Seconds> for Meters {
type Output = MetersPerSecond;
fn div(self, t: Seconds) -> MetersPerSecond {
MetersPerSecond(self.0 / t.0)
}
}
// Implement Display
impl std::fmt::Display for Meters {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}m", self.0)
}
}
fn main() {
let distance = Meters(100.0);
let time = Seconds(9.58);
let speed = distance / time;
println!("distance: {}", distance);
println!("speed: {:.2} m/s", speed.0);
println!("raw values: {} and {}", distance.value(), time.value());
// This would be a compile error — you cannot accidentally pass
// Seconds where Meters is expected:
// fn needs_meters(m: Meters) {}
// needs_meters(time); // ERROR: expected Meters, found Seconds
}
$ rustc design_patterns_newtype_pattern.rs && ./design_patterns_newtype_pattern
distance: 100m
speed: 10.44 m/s
raw values: 100 and 9.58
Newtype for validated data:
#[derive(Debug, Clone)]
struct EmailAddress(String);
impl EmailAddress {
fn new(s: &str) -> Result<Self, String> {
if s.contains('@') && s.contains('.') {
Ok(EmailAddress(s.to_lowercase()))
} else {
Err(format!("'{}' is not a valid email address", s))
}
}
fn as_str(&self) -> &str { &self.0 }
}
fn send_email(to: &EmailAddress, subject: &str) {
println!("sending '{}' to {}", subject, to.as_str());
}
fn main() {
let email = EmailAddress::new("Alice@Example.COM").unwrap();
send_email(&email, "Hello!");
// send_email(&"not-an-email".to_string(), "Hi"); // won't compile
}
$ rustc design_patterns_newtype_pattern_2.rs && ./design_patterns_newtype_pattern_2
sending 'Hello!' to alice@example.com
Typestate Pattern
Encode state transitions in the type system. Invalid state transitions become compile errors:
use std::marker::PhantomData;
// State types — zero-sized, used only as type markers
struct Locked;
struct Unlocked;
struct Safe<State> {
contents: String,
_state: PhantomData<State>,
}
impl Safe<Locked> {
fn new(contents: &str) -> Self {
Safe { contents: contents.to_string(), _state: PhantomData }
}
fn unlock(self, password: &str) -> Result<Safe<Unlocked>, Safe<Locked>> {
if password == "secret" {
Ok(Safe { contents: self.contents, _state: PhantomData })
} else {
Err(self)
}
}
}
impl Safe<Unlocked> {
fn get_contents(&self) -> &str { &self.contents }
fn put_contents(mut self, contents: &str) -> Self {
self.contents = contents.to_string();
self
}
fn lock(self) -> Safe<Locked> {
Safe { contents: self.contents, _state: PhantomData }
}
}
fn main() {
let safe = Safe::<Locked>::new("diamonds");
// Cannot access contents when locked:
// safe.get_contents(); // ERROR: no method get_contents on Safe<Locked>
let Ok(safe) = safe.unlock("secret") else { panic!("wrong password") };
println!("contents: {}", safe.get_contents());
let _locked = safe.put_contents("rubies").lock();
// safe.get_contents(); // ERROR: locked again
println!("safe is locked");
}
$ rustc design_patterns_typestate_pattern.rs && ./design_patterns_typestate_pattern
contents: diamonds
safe is locked
RAII — Resource Management
Rust’s Drop trait implements RAII naturally. Resources are always released when the owner goes out of scope:
use std::sync::Mutex;
struct ConnectionPool {
connections: Mutex<Vec<String>>,
}
impl ConnectionPool {
fn new(size: usize) -> Self {
let connections = (0..size).map(|i| format!("conn_{}", i)).collect();
Self { connections: Mutex::new(connections) }
}
fn acquire(&self) -> Option<PooledConnection<'_>> {
let mut pool = self.connections.lock().unwrap();
pool.pop().map(|conn| PooledConnection {
conn,
pool: &self.connections,
})
}
}
struct PooledConnection<'a> {
conn: String,
pool: &'a Mutex<Vec<String>>,
}
impl<'a> PooledConnection<'a> {
fn execute(&self, query: &str) -> String {
format!("[{}] executed: {}", self.conn, query)
}
}
impl<'a> Drop for PooledConnection<'a> {
fn drop(&mut self) {
// Connection automatically returned to pool
println!("returning {} to pool", self.conn);
self.pool.lock().unwrap().push(self.conn.clone());
}
}
fn main() {
let pool = ConnectionPool::new(3);
{
let conn = pool.acquire().expect("no connections available");
println!("{}", conn.execute("SELECT 1"));
} // conn dropped here — automatically returned to pool
println!("pool size: {}", pool.connections.lock().unwrap().len());
}
$ rustc design_patterns_raii_resource_management.rs && ./design_patterns_raii_resource_management
[conn_2] executed: SELECT 1
returning conn_2 to pool
pool size: 3
Strategy Pattern with Closures
struct Sorter<T, F: Fn(&T, &T) -> std::cmp::Ordering> {
comparator: F,
_phantom: std::marker::PhantomData<T>,
}
impl<T, F: Fn(&T, &T) -> std::cmp::Ordering> Sorter<T, F> {
fn new(comparator: F) -> Self {
Self { comparator, _phantom: std::marker::PhantomData }
}
fn sort(&self, data: &mut Vec<T>) {
data.sort_by(|a, b| (self.comparator)(a, b));
}
}
fn main() {
let mut numbers = vec![3, 1, 4, 1, 5, 9, 2, 6];
let asc_sorter = Sorter::new(|a: &i32, b| a.cmp(b));
asc_sorter.sort(&mut numbers);
println!("{:?}", numbers);
let desc_sorter = Sorter::new(|a: &i32, b| b.cmp(a));
desc_sorter.sort(&mut numbers);
println!("{:?}", numbers);
// Sort strings by length
let mut words = vec!["banana", "apple", "cherry", "fig"];
let len_sorter = Sorter::new(|a: &&str, b: &&str| a.len().cmp(&b.len()));
len_sorter.sort(&mut words);
println!("{:?}", words);
}
$ rustc design_patterns_strategy_pattern_with_closures.rs && ./design_patterns_strategy_pattern_with_closures
[1, 1, 2, 3, 4, 5, 6, 9]
[9, 6, 5, 4, 3, 2, 1, 1]
["fig", "apple", "banana", "cherry"]
Observer Pattern with Trait Objects
trait Observer {
fn on_event(&self, event: &str);
}
struct EventSource {
observers: Vec<Box<dyn Observer>>,
}
impl EventSource {
fn new() -> Self { Self { observers: Vec::new() } }
fn subscribe(&mut self, obs: Box<dyn Observer>) {
self.observers.push(obs);
}
fn emit(&self, event: &str) {
for obs in &self.observers {
obs.on_event(event);
}
}
}
struct Logger { prefix: String }
struct Counter { count: std::cell::Cell<u32> }
impl Observer for Logger {
fn on_event(&self, event: &str) {
println!("[{}] {}", self.prefix, event);
}
}
impl Observer for Counter {
fn on_event(&self, _event: &str) {
self.count.set(self.count.get() + 1);
}
}
fn main() {
let mut source = EventSource::new();
source.subscribe(Box::new(Logger { prefix: "LOG".to_string() }));
source.subscribe(Box::new(Counter { count: std::cell::Cell::new(0) }));
source.emit("user_login");
source.emit("page_view");
source.emit("user_logout");
}
$ rustc design_patterns_observer_pattern_with_trait_objects.rs && ./design_patterns_observer_pattern_with_trait_objects
[LOG] user_login
[LOG] page_view
[LOG] user_logout
Practice
-
In the typestate example, call
safe.get_contents()on the freshly createdSafe<Locked>. What kind of error is it — runtime or compile time?Solution
Compile time:
error[E0599]: no method named `get_contents` found for struct `Safe<Locked>` in the current scopeget_contentsonly exists inimpl Safe<Unlocked>, so reading a locked safe isn’t a check that could fail — the method simply isn’t there. -
In the newtype example, call a function
fn needs_meters(m: Meters)withSeconds(9.58).Solution
error[E0308]: mismatched types ... | needs_meters(Seconds(9.58)); | ------------ ^^^^^^^^^^^^^ expected `Meters`, found `Seconds`Both wrap an
f64, but they are distinct types, and the wrapper costs nothing at runtime. -
Acquire all three pooled connections at once, then try a fourth. What does
acquirereturn, and what happens to the pool when the three guards go out of scope?Solution
The fourth call returns
None— the pool is empty while all three guards are alive. When their scope ends, eachDroppushes its connection back, and the pool size returns to 3.