As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Rust's design patterns offer a fresh approach to familiar software architecture concepts. The language's unique features create opportunities to implement patterns that are both safer and more efficient than traditional approaches. I'll explore how Rust's ownership model, type system, and zero-cost abstractions influence pattern implementation.
When I first encountered Rust, I was struck by how its constraints led to more thoughtful design decisions. The patterns we'll examine aren't just translations of object-oriented concepts—they're evolved versions that leverage Rust's strengths.
Builder Pattern
The Builder pattern shines in Rust due to the type system's ability to enforce correctness at compile time. Unlike dynamic languages where builders might fail at runtime, Rust can guarantee certain properties.
struct HouseBuilder {
floors: Option<u32>,
bedrooms: Option<u32>,
has_garage: Option<bool>,
}
impl HouseBuilder {
fn new() -> Self {
HouseBuilder {
floors: None,
bedrooms: None,
has_garage: None,
}
}
fn floors(mut self, count: u32) -> Self {
self.floors = Some(count);
self
}
fn bedrooms(mut self, count: u32) -> Self {
self.bedrooms = Some(count);
self
}
fn garage(mut self, has_garage: bool) -> Self {
self.has_garage = Some(has_garage);
self
}
fn build(self) -> Result<House, &'static str> {
let floors = self.floors.ok_or("Floors must be specified")?;
let bedrooms = self.bedrooms.ok_or("Bedroom count is required")?;
let has_garage = self.has_garage.unwrap_or(false);
Ok(House { floors, bedrooms, has_garage })
}
}
struct House {
floors: u32,
bedrooms: u32,
has_garage: bool,
}
I've found this pattern particularly useful for creating complex configurations where some fields are required and others optional. The Result type provides clear error messages when requirements aren't met.
For more sophisticated builders, we can use the typestate pattern to enforce valid building sequences through the type system:
struct Uninitialized;
struct HasFloors(u32);
struct HasBedrooms(u32, u32);
struct HouseBuilder<State = Uninitialized> {
state: State,
}
impl HouseBuilder<Uninitialized> {
fn new() -> Self {
HouseBuilder { state: Uninitialized }
}
fn floors(self, count: u32) -> HouseBuilder<HasFloors> {
HouseBuilder { state: HasFloors(count) }
}
}
impl HouseBuilder<HasFloors> {
fn bedrooms(self, count: u32) -> HouseBuilder<HasBedrooms> {
let HasFloors(floors) = self.state;
HouseBuilder { state: HasBedrooms(floors, count) }
}
}
impl HouseBuilder<HasBedrooms> {
fn build(self) -> House {
let HasBedrooms(floors, bedrooms) = self.state;
House {
floors,
bedrooms,
has_garage: false
}
}
fn garage(self, has_garage: bool) -> Self {
// Can't modify the existing state directly, so we create a new one
let HasBedrooms(floors, bedrooms) = self.state;
HouseBuilder { state: HasBedrooms(floors, bedrooms) }
}
}
Visitor Pattern
The Visitor pattern enables operations across heterogeneous data structures without modifying them. Rust's traits and pattern matching make this particularly elegant:
trait Shape {
fn accept<V: Visitor>(&self, visitor: &mut V);
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn accept<V: Visitor>(&self, visitor: &mut V) {
visitor.visit_circle(self)
}
}
impl Shape for Rectangle {
fn accept<V: Visitor>(&self, visitor: &mut V) {
visitor.visit_rectangle(self)
}
}
trait Visitor {
fn visit_circle(&mut self, circle: &Circle);
fn visit_rectangle(&mut self, rectangle: &Rectangle);
}
struct AreaCalculator {
total_area: f64,
}
impl Visitor for AreaCalculator {
fn visit_circle(&mut self, circle: &Circle) {
self.total_area += std::f64::consts::PI * circle.radius * circle.radius;
}
fn visit_rectangle(&mut self, rectangle: &Rectangle) {
self.total_area += rectangle.width * rectangle.height;
}
}
In my projects, I've found this pattern especially valuable when working with abstract syntax trees or document object models. It allows clean separation of concerns while maintaining type safety.
Command Pattern
Commands encapsulate actions as objects. In Rust, we can implement this with either trait objects or generics:
trait Command {
fn execute(&self);
}
struct SaveCommand {
filename: String,
}
impl Command for SaveCommand {
fn execute(&self) {
println!("Saving to {}", self.filename);
}
}
struct PrintCommand {
content: String,
}
impl Command for PrintCommand {
fn execute(&self) {
println!("{}", self.content);
}
}
// Using trait objects with dynamic dispatch
struct CommandQueue {
commands: Vec<Box<dyn Command>>,
}
impl CommandQueue {
fn new() -> Self {
CommandQueue { commands: Vec::new() }
}
fn add_command(&mut self, command: Box<dyn Command>) {
self.commands.push(command);
}
fn execute_all(&self) {
for cmd in &self.commands {
cmd.execute();
}
}
}
// Alternative using generics with static dispatch
struct StaticCommand<T: Command> {
command: T,
}
impl<T: Command> StaticCommand<T> {
fn new(command: T) -> Self {
StaticCommand { command }
}
fn execute(&self) {
self.command.execute();
}
}
I prefer the generic approach when performance is critical, as it enables monomorphization—the compiler generates specialized code for each command type, eliminating virtual dispatch overhead.
Factory Pattern
Factory methods create objects without specifying concrete classes. Rust factories utilize associated types to create objects with zero overhead:
trait DatabaseConnection {
fn connect(&self) -> bool;
fn query(&self, query: &str) -> Vec<String>;
}
struct PostgresConnection {
connection_string: String,
}
impl DatabaseConnection for PostgresConnection {
fn connect(&self) -> bool {
println!("Connecting to Postgres with {}", self.connection_string);
true
}
fn query(&self, query: &str) -> Vec<String> {
println!("Executing query on Postgres: {}", query);
vec!["result1".to_string(), "result2".to_string()]
}
}
trait ConnectionFactory {
type Connection: DatabaseConnection;
fn create_connection(&self, config: &str) -> Self::Connection;
}
struct PostgresFactory;
impl ConnectionFactory for PostgresFactory {
type Connection = PostgresConnection;
fn create_connection(&self, config: &str) -> PostgresConnection {
PostgresConnection {
connection_string: config.to_string(),
}
}
}
fn run_query<F: ConnectionFactory>(factory: F, config: &str, query: &str) -> Vec<String> {
let connection = factory.create_connection(config);
if connection.connect() {
connection.query(query)
} else {
Vec::new()
}
}
This pattern is particularly powerful when combined with generics, as it allows for compile-time polymorphism rather than runtime dispatch.
Adapter Pattern
The Adapter pattern converts interfaces to work together. Rust's traits make this straightforward:
trait JsonSerializer {
fn to_json(&self) -> String;
}
trait XmlSerializer {
fn to_xml(&self) -> String;
}
struct User {
name: String,
email: String,
}
impl XmlSerializer for User {
fn to_xml(&self) -> String {
format!("<user><name>{}</name><email>{}</email></user>",
self.name, self.email)
}
}
struct XmlToJsonAdapter<T: XmlSerializer> {
adaptee: T,
}
impl<T: XmlSerializer> JsonSerializer for XmlToJsonAdapter<T> {
fn to_json(&self) -> String {
// This would normally use a proper XML parser
let xml = self.adaptee.to_xml();
xml.replace("<user>", "{")
.replace("</user>", "}")
.replace("<name>", "\"name\": \"")
.replace("</name>", "\", ")
.replace("<email>", "\"email\": \"")
.replace("</email>", "\"")
}
}
Observer Pattern
Traditional observer implementations often violate Rust's ownership rules. Instead, I use channels to maintain both safety and flexibility:
use std::sync::mpsc::{channel, Sender, Receiver};
enum Event {
ValueChanged(i32),
ConnectionLost,
Shutdown,
}
struct EventSource {
subscribers: Vec<Sender<Event>>,
value: i32,
}
impl EventSource {
fn new() -> Self {
EventSource {
subscribers: Vec::new(),
value: 0,
}
}
fn subscribe(&mut self) -> Receiver<Event> {
let (sender, receiver) = channel();
self.subscribers.push(sender);
receiver
}
fn set_value(&mut self, new_value: i32) {
self.value = new_value;
// Notify subscribers
self.subscribers.retain(|subscriber| {
subscriber.send(Event::ValueChanged(new_value)).is_ok()
});
}
fn shutdown(&mut self) {
for subscriber in &self.subscribers {
let _ = subscriber.send(Event::Shutdown);
}
}
}
fn observer_example() {
let mut source = EventSource::new();
let receiver = source.subscribe();
std::thread::spawn(move || {
for event in receiver {
match event {
Event::ValueChanged(value) => println!("Value changed to {}", value),
Event::ConnectionLost => println!("Connection lost!"),
Event::Shutdown => {
println!("Shutting down observer");
break;
}
}
}
});
source.set_value(42);
source.shutdown();
}
This approach has several advantages: it works across threads, automatically handles disconnected observers, and maintains Rust's ownership guarantees.
Singleton Pattern
While singletons are sometimes controversial, Rust provides several safe implementations:
use std::sync::{Arc, Mutex, Once};
use std::sync::atomic::{AtomicUsize, Ordering};
struct Configuration {
max_connections: AtomicUsize,
database_url: String,
}
impl Configuration {
fn get_max_connections(&self) -> usize {
self.max_connections.load(Ordering::Relaxed)
}
fn set_max_connections(&self, value: usize) {
self.max_connections.store(value, Ordering::Relaxed);
}
}
// Option 1: Once initialization
fn get_configuration() -> &'static Configuration {
static mut INSTANCE: Option<Configuration> = None;
static ONCE: Once = Once::new();
unsafe {
ONCE.call_once(|| {
INSTANCE = Some(Configuration {
max_connections: AtomicUsize::new(10),
database_url: "postgres://localhost/mydb".to_string(),
});
});
INSTANCE.as_ref().unwrap()
}
}
// Option 2: Lazy static (requires the lazy_static crate)
// lazy_static! {
// static ref CONFIGURATION: Mutex<Configuration> = Mutex::new(
// Configuration {
// max_connections: AtomicUsize::new(10),
// database_url: "postgres://localhost/mydb".to_string(),
// }
// );
// }
I generally prefer the second approach with lazy_static, as it avoids unsafe code while providing the same functionality.
Strategy Pattern
The Strategy pattern defines a family of algorithms. In Rust, we can implement it using traits and closures:
trait SortStrategy<T> {
fn sort(&self, data: &mut [T]);
}
struct QuickSort;
struct MergeSort;
struct BubbleSort;
impl<T: Ord> SortStrategy<T> for QuickSort {
fn sort(&self, data: &mut [T]) {
data.sort(); // Uses the standard library's sort
}
}
impl<T: Ord> SortStrategy<T> for MergeSort {
fn sort(&self, data: &mut [T]) {
data.sort_by(|a, b| a.cmp(b)); // In real code, would use a merge sort
}
}
impl<T: Ord> SortStrategy<T> for BubbleSort {
fn sort(&self, data: &mut [T]) {
// Simple bubble sort implementation
let len = data.len();
for i in 0..len {
for j in 0..len-i-1 {
if data[j] > data[j+1] {
data.swap(j, j+1);
}
}
}
}
}
struct Sorter<T, S: SortStrategy<T>> {
strategy: S,
_phantom: std::marker::PhantomData<T>,
}
impl<T, S: SortStrategy<T>> Sorter<T, S> {
fn new(strategy: S) -> Self {
Sorter {
strategy,
_phantom: std::marker::PhantomData,
}
}
fn sort(&self, data: &mut [T]) {
self.strategy.sort(data);
}
}
// Using closures as strategies
struct ClosureSorter<T, F: Fn(&mut [T])> {
sort_fn: F,
_phantom: std::marker::PhantomData<T>,
}
impl<T, F: Fn(&mut [T])> ClosureSorter<T, F> {
fn new(sort_fn: F) -> Self {
ClosureSorter {
sort_fn,
_phantom: std::marker::PhantomData,
}
}
fn sort(&self, data: &mut [T]) {
(self.sort_fn)(data);
}
}
The generic implementation allows compile-time optimization, which I've found crucial for high-performance code paths.
State Pattern
The State pattern allows objects to change behavior when internal state changes. Rust's type system makes states explicit:
trait State {
fn handle_input(&self) -> Option<Box<dyn State>>;
fn update(&self);
fn render(&self);
}
struct StandingState;
struct JumpingState {
velocity: f32,
}
struct DuckingState;
impl State for StandingState {
fn handle_input(&self) -> Option<Box<dyn State>> {
// Simplified: spacebar = jump, down arrow = duck
let input = get_input(); // Assume this function exists
if input == "SPACE" {
Some(Box::new(JumpingState { velocity: 10.0 }))
} else if input == "DOWN" {
Some(Box::new(DuckingState))
} else {
None
}
}
fn update(&self) {
// Standing state logic
}
fn render(&self) {
println!("Rendering player in standing state");
}
}
// Implementations for other states...
struct Player {
state: Box<dyn State>,
}
impl Player {
fn new() -> Self {
Player {
state: Box::new(StandingState),
}
}
fn update(&mut self) {
if let Some(new_state) = self.state.handle_input() {
self.state = new_state;
}
self.state.update();
self.state.render();
}
}
// Simulated input function
fn get_input() -> &'static str {
"NONE"
}
I've often used a variant of this pattern with enum-based states for improved performance:
enum PlayerState {
Standing,
Jumping { velocity: f32, elapsed_time: f32 },
Ducking { duration: f32 },
}
struct EnumPlayer {
state: PlayerState,
}
impl EnumPlayer {
fn new() -> Self {
EnumPlayer { state: PlayerState::Standing }
}
fn update(&mut self, dt: f32) {
// Handle transitions
self.state = match self.state {
PlayerState::Standing => {
if is_space_pressed() {
PlayerState::Jumping { velocity: 10.0, elapsed_time: 0.0 }
} else if is_down_pressed() {
PlayerState::Ducking { duration: 0.0 }
} else {
PlayerState::Standing
}
},
PlayerState::Jumping { velocity, elapsed_time } => {
let new_elapsed = elapsed_time + dt;
if velocity - 9.8 * new_elapsed <= 0.0 {
PlayerState::Standing
} else {
PlayerState::Jumping {
velocity,
elapsed_time: new_elapsed,
}
}
},
PlayerState::Ducking { duration } => {
if !is_down_pressed() {
PlayerState::Standing
} else {
PlayerState::Ducking { duration: duration + dt }
}
}
};
}
}
fn is_space_pressed() -> bool { false }
fn is_down_pressed() -> bool { false }
This approach eliminates dynamic dispatch overhead and enables more optimizations, which is critical for game development.
Conclusion
Rust's design patterns demonstrate how safety constraints can lead to better architectures. By forcing us to think about ownership, lifetimes, and types, Rust guides us toward patterns that are not just safer but often more efficient than traditional implementations.
I've found that the best Rust code doesn't blindly translate patterns from other languages but instead reimagines them to take advantage of Rust's strengths. This often leads to solutions that are both more elegant and more performant.
When implementing patterns in Rust, consider:
- Using generics and associated types for zero-cost abstractions
- Leveraging the type system to encode invariants
- Embracing Rust's ownership model rather than fighting it
- Using enums for state machines when possible
- Employing channels for observer-like patterns
These approaches have consistently led to code that's not only more robust but easier to maintain and evolve as requirements change.
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)