Type-State Pattern

The type-state pattern encodes state transitions in the type system:

// States
struct Idle;
struct Running;
struct Paused;
struct Stopped;

// Machine with type-state
struct StateMachine<S> {
    state: S,
}

// Constructors and transitions
impl StateMachine<Idle> {
    fn new() -> Self {
        StateMachine { state: Idle }
    }
    
    fn start(self) -> StateMachine<Running> {
        println!("Starting the machine");
        StateMachine { state: Running }
    }
}

impl StateMachine<Running> {
    fn pause(self) -> StateMachine<Paused> {
        println!("Pausing the machine");
        StateMachine { state: Paused }
    }
    
    fn stop(self) -> StateMachine<Stopped> {
        println!("Stopping the machine");
        StateMachine { state: Stopped }
    }
}

impl StateMachine<Paused> {
    fn resume(self) -> StateMachine<Running> {
        println!("Resuming the machine");
        StateMachine { state: Running }
    }
    
    fn stop(self) -> StateMachine<Stopped> {
        println!("Stopping the machine");
        StateMachine { state: Stopped }
    }
}

impl StateMachine<Stopped> {
    fn reset(self) -> StateMachine<Idle> {
        println!("Resetting the machine");
        StateMachine { state: Idle }
    }
}

Visitor Pattern

The visitor pattern allows adding operations to objects without modifying them:

// Element trait
trait Element {
    fn accept(&self, visitor: &dyn Visitor);
}

// Concrete elements
struct Circle {
    radius: f64,
}

impl Element for Circle {
    fn accept(&self, visitor: &dyn Visitor) {
        visitor.visit_circle(self);
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Element for Rectangle {
    fn accept(&self, visitor: &dyn Visitor) {
        visitor.visit_rectangle(self);
    }
}

// Visitor trait
trait Visitor {
    fn visit_circle(&self, circle: &Circle);
    fn visit_rectangle(&self, rectangle: &Rectangle);
}

// Concrete visitors
struct AreaCalculator;

impl Visitor for AreaCalculator {
    fn visit_circle(&self, circle: &Circle) {
        let area = std::f64::consts::PI * circle.radius * circle.radius;
        println!("Circle area: {:.2}", area);
    }
    
    fn visit_rectangle(&self, rectangle: &Rectangle) {
        let area = rectangle.width * rectangle.height;
        println!("Rectangle area: {:.2}", area);
    }
}

Best Practices for Design Patterns in Rust

Based on experience from real-world Rust projects, here are some best practices:

1. Prefer Composition Over Inheritance

Rust doesn’t have inheritance, so use composition and traits instead:

// Instead of inheritance, use composition
struct Logger {
    // ...
}

struct DatabaseConnection {
    // ...
}

struct Application {
    logger: Logger,
    db: DatabaseConnection,
}

// Use traits for shared behavior
trait Loggable {
    fn log(&self, message: &str);
}

impl Loggable for Logger {
    fn log(&self, message: &str) {
        println!("[LOG] {}", message);
    }
}

impl Loggable for Application {
    fn log(&self, message: &str) {
        self.logger.log(message);
    }
}

2. Leverage Rust’s Type System

Use Rust’s type system to enforce correctness:

// Use enums for state machines
enum ConnectionState {
    Disconnected,
    Connecting,
    Connected,
    Failed(String),
}

struct Connection {
    state: ConnectionState,
}

impl Connection {
    fn new() -> Self {
        Connection { state: ConnectionState::Disconnected }
    }
    
    fn connect(&mut self) {
        self.state = match self.state {
            ConnectionState::Disconnected => ConnectionState::Connecting,
            ConnectionState::Failed(_) => ConnectionState::Connecting,
            _ => return, // Already connecting or connected
        };
        
        // Attempt connection...
    }
}

3. Use Traits for Polymorphism

Traits provide a clean way to implement polymorphic behavior:

trait Renderer {
    fn render(&self, text: &str) -> String;
}

struct HtmlRenderer;
impl Renderer for HtmlRenderer {
    fn render(&self, text: &str) -> String {
        format!("<p>{}</p>", text)
    }
}

struct MarkdownRenderer;
impl Renderer for MarkdownRenderer {
    fn render(&self, text: &str) -> String {
        format!("*{}*", text)
    }
}

// Function that works with any Renderer
fn render_document(renderer: &impl Renderer, content: &str) -> String {
    renderer.render(content)
}

4. Embrace Ownership and Borrowing

Design your APIs with ownership in mind:

// Builder that consumes self
struct RequestBuilder {
    url: Option<String>,
    method: Option<String>,
}

impl RequestBuilder {
    fn new() -> Self {
        RequestBuilder {
            url: None,
            method: None,
        }
    }
    
    // Takes ownership and returns self
    fn url(mut self, url: impl Into<String>) -> Self {
        self.url = Some(url.into());
        self
    }
    
    // Takes ownership and returns self
    fn method(mut self, method: impl Into<String>) -> Self {
        self.method = Some(method.into());
        self
    }
    
    // Consumes self to build the final object
    fn build(self) -> Result<Request, &'static str> {
        let url = self.url.ok_or("URL is required")?;
        let method = self.method.unwrap_or_else(|| "GET".to_string());
        
        Ok(Request { url, method })
    }
}

struct Request {
    url: String,
    method: String,
}

Conclusion

Design patterns and idioms in Rust often differ from their counterparts in other languages due to Rust’s unique features like ownership, traits, and algebraic data types. By understanding and applying these patterns appropriately, you can write Rust code that is not only correct and efficient but also maintainable and idiomatic.

The key takeaways from this exploration of Rust design patterns are:

  1. Memory management patterns like RAII and drop guards leverage Rust’s ownership system
  2. Creational patterns like builder and factory method provide flexible object creation
  3. Structural patterns like adapter and decorator help with object composition
  4. Behavioral patterns like iterator and strategy define interactions between objects
  5. Rust-specific idioms like newtype and type-state patterns leverage Rust’s type system

Remember that patterns are tools, not rules. Always consider the specific requirements and constraints of your project when deciding which patterns to apply. By using these patterns judiciously, you can create Rust code that is both elegant and effective.