Behavioral Patterns
Patterns that focus on communication between objects:
Iterator Pattern
The iterator pattern provides a way to access elements of a collection sequentially:
struct Counter {
count: usize,
limit: usize,
}
impl Counter {
fn new(limit: usize) -> Self {
Counter { count: 0, limit }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.limit {
let current = self.count;
self.count += 1;
Some(current)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
// Use the iterator
for count in counter {
println!("Count: {}", count);
}
// Create another counter and use iterator methods
let sum: usize = Counter::new(10).sum();
println!("Sum: {}", sum);
let even_counts: Vec<_> = Counter::new(10).filter(|&n| n % 2 == 0).collect();
println!("Even counts: {:?}", even_counts);
}
Observer Pattern
The observer pattern allows objects to be notified of changes:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
// Observer trait
trait Observer {
fn update(&self, message: &str);
}
// Concrete observer
struct ConcreteObserver {
name: String,
}
impl ConcreteObserver {
fn new(name: &str) -> Self {
ConcreteObserver {
name: name.to_string(),
}
}
}
impl Observer for ConcreteObserver {
fn update(&self, message: &str) {
println!("Observer {} received: {}", self.name, message);
}
}
// Subject that notifies observers
struct Subject {
observers: RefCell<Vec<Weak<dyn Observer>>>,
}
impl Subject {
fn new() -> Self {
Subject {
observers: RefCell::new(Vec::new()),
}
}
fn attach(&self, observer: Weak<dyn Observer>) {
self.observers.borrow_mut().push(observer);
}
fn notify(&self, message: &str) {
// Clean up any dropped observers
self.observers.borrow_mut().retain(|o| o.upgrade().is_some());
// Notify all active observers
for observer in self.observers.borrow().iter() {
if let Some(o) = observer.upgrade() {
o.update(message);
}
}
}
}
Strategy Pattern
The strategy pattern defines a family of algorithms and makes them interchangeable:
// Strategy trait
trait SortStrategy {
fn sort(&self, data: &mut [i32]);
}
// Concrete strategies
struct BubbleSort;
impl SortStrategy for BubbleSort {
fn sort(&self, data: &mut [i32]) {
println!("Sorting using bubble sort");
// Bubble sort implementation
let n = data.len();
for i in 0..n {
for j in 0..n - i - 1 {
if data[j] > data[j + 1] {
data.swap(j, j + 1);
}
}
}
}
}
struct QuickSort;
impl SortStrategy for QuickSort {
fn sort(&self, data: &mut [i32]) {
println!("Sorting using quick sort");
// Quick sort implementation (simplified)
if data.len() <= 1 {
return;
}
// Simplified implementation for demonstration
data.sort();
}
}
// Context that uses a strategy
struct Sorter {
strategy: Box<dyn SortStrategy>,
}
impl Sorter {
fn new(strategy: Box<dyn SortStrategy>) -> Self {
Sorter { strategy }
}
fn set_strategy(&mut self, strategy: Box<dyn SortStrategy>) {
self.strategy = strategy;
}
fn sort(&self, data: &mut [i32]) {
self.strategy.sort(data);
}
}
Rust-Specific Idioms
Patterns that are particularly idiomatic in Rust:
Newtype Pattern
The newtype pattern creates a new type that wraps an existing type, providing type safety and encapsulation:
// Newtype for user IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
// Newtype for product IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ProductId(u64);
// Functions that accept specific types
fn get_user(id: UserId) -> Option<User> {
// Look up user by ID
Some(User { id, name: "Alice".to_string() })
}
fn get_product(id: ProductId) -> Option<Product> {
// Look up product by ID
Some(Product { id, name: "Widget".to_string() })
}
// User and Product structs
struct User {
id: UserId,
name: String,
}
struct Product {
id: ProductId,
name: String,
}