Table of Contents
Creational Patterns
Creational patterns focus on object instantiation, abstracting the creation process to make code more flexible and reusable.
1. Singleton Pattern
Problem: Ensure a type has only one instance, providing a global point of access to it (e.g., a database connection pool, logger, or configuration manager).
Solution: Use Rust’s concurrency primitives to enforce lazy, thread-safe initialization of a single instance.
Implementation:
Rust avoids global mutable state by default, but libraries like lazy_static or once_cell simplify safe Singleton creation. Here’s an example with once_cell (a lightweight, zero-cost alternative to lazy_static):
use once_cell::sync::OnceCell;
// Define a struct for the Singleton (e.g., a configuration manager)
#[derive(Debug)]
struct AppConfig {
api_url: String,
max_retries: u8,
}
impl AppConfig {
// Lazy-initialized, thread-safe Singleton instance
static INSTANCE: OnceCell<AppConfig> = OnceCell::new();
// Initialize the Singleton with configuration data
fn init(config: AppConfig) -> Result<(), AppConfig> {
Self::INSTANCE.set(config)
}
// Get the global instance (panics if not initialized)
fn get() -> &'static AppConfig {
Self::INSTANCE.get().expect("AppConfig not initialized")
}
}
fn main() {
// Initialize the Singleton (e.g., from a config file)
AppConfig::init(AppConfig {
api_url: "https://api.example.com".to_string(),
max_retries: 3,
}).unwrap();
// Access the Singleton globally
let config = AppConfig::get();
println!("API URL: {}", config.api_url); // Output: API URL: https://api.example.com
}
Rust-Specific Notes:
OnceCellensures the instance is initialized exactly once, even across threads.- Avoid Singletons for mutable state unless necessary—prefer dependency injection for testability.
- For mutable Singletons, use
RwLockorMutexinsideOnceCell(e.g.,OnceCell<Mutex<AppConfig>>).
2. Factory Method Pattern
Problem: Define an interface for creating objects but delegate instantiation to subclasses (or, in Rust, trait implementations).
Solution: Use a trait to define a “factory method,” with concrete types implementing the trait to return specific products.
Example: Document Editor
Suppose we need an editor that creates different document types (text, spreadsheet) via a factory.
// Product trait: Defines the interface for documents
trait Document {
fn open(&self);
fn save(&self);
}
// Concrete Products
struct TextDocument;
impl Document for TextDocument {
fn open(&self) { println!("Opening text document..."); }
fn save(&self) { println!("Saving text document..."); }
}
struct SpreadsheetDocument;
impl Document for SpreadsheetDocument {
fn open(&self) { println!("Opening spreadsheet..."); }
fn save(&self) { println!("Saving spreadsheet..."); }
}
// Factory trait: Defines the factory method
trait DocumentFactory {
fn create_document(&self) -> Box<dyn Document>;
}
// Concrete Factories
struct TextDocumentFactory;
impl DocumentFactory for TextDocumentFactory {
fn create_document(&self) -> Box<dyn Document> {
Box::new(TextDocument)
}
}
struct SpreadsheetFactory;
impl DocumentFactory for SpreadsheetFactory {
fn create_document(&self) -> Box<dyn Document> {
Box::new(SpreadsheetDocument)
}
}
fn main() {
let text_factory: Box<dyn DocumentFactory> = Box::new(TextDocumentFactory);
let text_doc = text_factory.create_document();
text_doc.open(); // Output: Opening text document...
}
Rust-Specific Notes:
- Traits (
Document,DocumentFactory) replace OOP interfaces. Box<dyn Document>enables dynamic dispatch for polymorphic behavior.
3. Builder Pattern
Problem: Construct complex objects with many optional fields (e.g., a User with required id/name and optional email/age).
Solution: Use a “builder” struct to incrementally set fields, then validate and build the final object.
Implementation:
#[derive(Debug)]
struct User {
id: u64, // Required
name: String, // Required
email: Option<String>, // Optional
age: Option<u8>, // Optional
}
// Builder struct to construct User
#[derive(Default)]
struct UserBuilder {
id: Option<u64>,
name: Option<String>,
email: Option<String>,
age: Option<u8>,
}
impl UserBuilder {
// Set required fields
fn id(mut self, id: u64) -> Self {
self.id = Some(id);
self
}
fn name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
// Set optional fields
fn email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
fn age(mut self, age: u8) -> Self {
self.age = Some(age);
self
}
// Build the User (returns Result to handle missing required fields)
fn build(self) -> Result<User, String> {
Ok(User {
id: self.id.ok_or("ID is required")?,
name: self.name.ok_or("Name is required")?,
email: self.email,
age: self.age,
})
}
}
fn main() {
// Create a User with required fields and optional email
let user = UserBuilder::default()
.id(123)
.name("Alice".to_string())
.email("[email protected]".to_string())
.build()
.expect("Failed to create user");
println!("User: {:?}", user);
// Output: User: User { id: 123, name: "Alice", email: Some("[email protected]"), age: None }
}
Rust-Specific Notes:
- Use
Defaultfor the builder to simplify initialization. - Return
Resultfrombuild()to enforce validation of required fields.
Structural Patterns
Structural patterns compose objects to form larger structures, optimizing relationships between entities.
4. Adapter Pattern
Problem: Make two incompatible interfaces work together (e.g., integrating a legacy library with a new system).
Solution: Create an “adapter” struct that wraps the legacy type and implements the target interface.
Example: Legacy Printer Adapter
// Legacy library with an incompatible interface
struct LegacyPrinter {
model: String,
}
impl LegacyPrinter {
fn print_legacy(&self, text: &str) {
println!("[Legacy {}] Printing: {}", self.model, text);
}
}
// New system's target interface
trait ModernPrinter {
fn print(&self, text: &str);
}
// Adapter: Wraps LegacyPrinter and implements ModernPrinter
struct LegacyPrinterAdapter(LegacyPrinter);
impl ModernPrinter for LegacyPrinterAdapter {
fn print(&self, text: &str) {
// Adapt the new interface to the legacy method
self.0.print_legacy(text);
}
}
// Usage: New system works with ModernPrinter
fn main() {
let legacy_printer = LegacyPrinter { model: "LX-100".to_string() };
let adapter = LegacyPrinterAdapter(legacy_printer);
// Use the adapter via the ModernPrinter interface
adapter.print("Hello, Adapter Pattern!");
// Output: [Legacy LX-100] Printing: Hello, Adapter Pattern!
}
Rust-Specific Notes:
- The adapter uses tuple struct syntax (
LegacyPrinterAdapter(LegacyPrinter)) for concise wrapping. - Traits (
ModernPrinter) define the target interface, ensuring compatibility.
Behavioral Patterns
Behavioral patterns focus on communication between objects, defining how they interact and distribute responsibility.
5. Observer Pattern
Problem: Notify multiple “observer” objects of state changes in a “subject” object (e.g., a weather station updating displays).
Solution: Define Subject and Observer traits, with the subject maintaining a list of observers to notify on state changes.
Implementation with Shared Ownership:
use std::cell::RefCell;
use std::rc::Rc;
// Observer trait: Defines the update interface
trait Observer {
fn update(&mut self, temperature: f64);
}
// Subject trait: Defines methods to manage observers
trait Subject {
fn attach(&mut self, observer: Rc<RefCell<dyn Observer>>);
fn detach(&mut self, observer: Rc<RefCell<dyn Observer>>);
fn notify(&self);
}
// Concrete Subject: Weather Station
struct WeatherStation {
temperature: f64,
observers: Vec<Rc<RefCell<dyn Observer>>>,
}
impl WeatherStation {
fn new() -> Self {
WeatherStation { temperature: 0.0, observers: Vec::new() }
}
// Update temperature and notify observers
fn set_temperature(&mut self, temp: f64) {
self.temperature = temp;
self.notify();
}
}
impl Subject for WeatherStation {
fn attach(&mut self, observer: Rc<RefCell<dyn Observer>>) {
self.observers.push(observer);
}
fn detach(&mut self, observer: Rc<RefCell<dyn Observer>>) {
self.observers.retain(|o| !Rc::ptr_eq(o, &observer));
}
fn notify(&self) {
for observer in &self.observers {
observer.borrow_mut().update(self.temperature);
}
}
}
// Concrete Observers
struct PhoneDisplay {
name: String,
}
impl Observer for PhoneDisplay {
fn update(&mut self, temperature: f64) {
println!("[{}] Temperature updated: {}°C", self.name, temperature);
}
}
struct LaptopDisplay {
name: String,
}
impl Observer for LaptopDisplay {
fn update(&mut self, temperature: f64) {
println!("[{}] Current temp: {}°C", self.name, temperature);
}
}
fn main() {
let mut station = WeatherStation::new();
// Create observers (Rc<RefCell<...>> for shared, mutable ownership)
let phone = Rc::new(RefCell::new(PhoneDisplay { name: "Phone".to_string() }));
let laptop = Rc::new(RefCell::new(LaptopDisplay { name: "Laptop".to_string() }));
// Attach observers
station.attach(Rc::clone(&phone));
station.attach(Rc::clone(&laptop));
// Update temperature (triggers notifications)
station.set_temperature(22.5);
// Output: [Phone] Temperature updated: 22.5°C
// [Laptop] Current temp: 22.5°C
// Detach laptop
station.detach(laptop);
station.set_temperature(25.0);
// Output: [Phone] Temperature updated: 25°C
}
Rust-Specific Notes:
- Use
Rc<RefCell<...>>to enable shared, mutable ownership of observers (required for multiple observers). Rc::ptr_eqchecks for pointer equality to detach observers safely.
6. Strategy Pattern
Problem: Define a family of algorithms, encapsulate each, and make them interchangeable (e.g., different payment methods in a checkout system).
Solution: Use a Strategy trait to abstract algorithms, with concrete strategies implementing the trait.
Example: Payment Strategies
// Strategy trait: Defines the algorithm interface
trait PaymentStrategy {
fn pay(&self, amount: f64) -> Result<(), String>;
}
// Concrete Strategies
struct CreditCardPayment {
card_number: String,
}
impl PaymentStrategy for CreditCardPayment {
fn pay(&self, amount: f64) -> Result<(), String> {
Ok(println!("Paid ${} with credit card {}", amount, self.card_number))
}
}
struct PayPalPayment {
email: String,
}
impl PaymentStrategy for PayPalPayment {
fn pay(&self, amount: f64) -> Result<(), String> {
Ok(println!("Paid ${} via PayPal ({})", amount, self.email))
}
}
// Context: Uses a strategy to perform an action
struct ShoppingCart {
items: Vec<f64>,
payment_strategy: Box<dyn PaymentStrategy>,
}
impl ShoppingCart {
fn new(strategy: Box<dyn PaymentStrategy>) -> Self {
ShoppingCart { items: Vec::new(), payment_strategy: strategy }
}
fn add_item(&mut self, price: f64) {
self.items.push(price);
}
fn checkout(&self) -> Result<(), String> {
let total: f64 = self.items.iter().sum();
self.payment_strategy.pay(total)
}
}
fn main() {
// Use Credit Card strategy
let mut cart = ShoppingCart::new(Box::new(CreditCardPayment {
card_number: "****-****-****-1234".to_string(),
}));
cart.add_item(29.99);
cart.add_item(15.50);
cart.checkout().unwrap();
// Output: Paid $45.49 with credit card ****-****-****-1234
// Switch to PayPal strategy
let mut cart = ShoppingCart::new(Box::new(PayPalPayment {
email: "[email protected]".to_string(),
}));
cart.add_item(99.99);
cart.checkout().unwrap();
// Output: Paid $99.99 via PayPal ([email protected])
}
Rust-Specific Notes:
- Strategies are boxed (
Box<dyn PaymentStrategy>) for dynamic dispatch, allowing runtime swapping.
7. Iterator Pattern (Rust’s Built-in Implementation)
Problem: Provide a way to traverse elements of a collection without exposing its underlying structure.
Solution: Rust’s standard library includes the Iterator trait, which is a built-in implementation of the Iterator pattern.
Example: Custom Fibonacci Iterator
// Custom iterator for Fibonacci sequence
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { a: 0, b: 1 }
}
}
// Implement Rust's Iterator trait
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let next = self.a;
self.a = self.b;
self.b = next + self.b;
Some(next) // Infinite iterator (returns None only on overflow)
}
}
fn main() {
// Generate first 10 Fibonacci numbers
let fib_sequence: Vec<u64> = Fibonacci::new().take(10).collect();
println!("Fibonacci: {:?}", fib_sequence);
// Output: Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
Rust-Specific Notes:
- The
Iteratortrait defines a uniform interface for iteration (next() -> Option<Item>). - Combinators like
take(),map(), andfilter()enable powerful, declarative iteration pipelines.
Conclusion
Design patterns are timeless tools, but their implementation in Rust is shaped by its unique features: traits for interfaces, ownership for safety, and built-in abstractions like Iterator. By adapting patterns to Rust’s paradigm, you can write code that is both idiomatic and robust. Remember to choose patterns judiciously—Rust’s type system often eliminates the need for patterns like Singleton, while enabling others (like Builder) to be more expressive.
References
- Gamma, E., et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Rust Documentation: Traits
- Rust Documentation: Iterator Trait
- once_cell Crate
- Rust By Example: Design Patterns