Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. In Rust, this pattern showcases the power of traits for both static and dynamic dispatch, allowing you to choose between compile-time optimization and runtime flexibility.

Problem Statement

You need to switch between different algorithms or behaviors at runtime based on configuration, user input, or changing conditions. Traditional object-oriented languages use inheritance or interfaces, but Rust's trait system provides more powerful and efficient alternatives.

Traditional Object-Oriented Implementation

In Java, the Strategy pattern typically uses interfaces and classes:

// Strategy interface
interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using credit card: " + cardNumber);
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal: " + email);
    }
}

// Context class
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

Rust Implementation

Rust provides multiple ways to implement the Strategy pattern, each with different trade-offs:

1. Static Dispatch with Generics

#![allow(unused)]
fn main() {
// Strategy trait
trait PaymentStrategy {
    fn pay(&self, amount: f64);
}

// Concrete strategies
struct CreditCardPayment {
    card_number: String,
}

impl CreditCardPayment {
    fn new(card_number: String) -> Self {
        Self { card_number }
    }
}

impl PaymentStrategy for CreditCardPayment {
    fn pay(&self, amount: f64) {
        println!("Paid ${:.2} using credit card: {}", amount, self.card_number);
    }
}

struct PayPalPayment {
    email: String,
}

impl PayPalPayment {
    fn new(email: String) -> Self {
        Self { email }
    }
}

impl PaymentStrategy for PayPalPayment {
    fn pay(&self, amount: f64) {
        println!("Paid ${:.2} using PayPal: {}", amount, self.email);
    }
}

// Context with static dispatch
struct ShoppingCart<T: PaymentStrategy> {
    payment_strategy: T,
    items: Vec<(String, f64)>,
}

impl<T: PaymentStrategy> ShoppingCart<T> {
    fn new(payment_strategy: T) -> Self {
        Self {
            payment_strategy,
            items: Vec::new(),
        }
    }

    fn add_item(&mut self, name: String, price: f64) {
        self.items.push((name, price));
    }

    fn checkout(&self) {
        let total: f64 = self.items.iter().map(|(_, price)| price).sum();
        self.payment_strategy.pay(total);
    }
}
}

2. Dynamic Dispatch with Trait Objects

#![allow(unused)]
fn main() {
// Using Box<dyn Trait> for owned trait objects
struct DynamicShoppingCart {
    payment_strategy: Box<dyn PaymentStrategy>,
    items: Vec<(String, f64)>,
}

impl DynamicShoppingCart {
    fn new(payment_strategy: Box<dyn PaymentStrategy>) -> Self {
        Self {
            payment_strategy,
            items: Vec::new(),
        }
    }

    fn set_payment_strategy(&mut self, strategy: Box<dyn PaymentStrategy>) {
        self.payment_strategy = strategy;
    }

    fn add_item(&mut self, name: String, price: f64) {
        self.items.push((name, price));
    }

    fn checkout(&self) {
        let total: f64 = self.items.iter().map(|(_, price)| price).sum();
        self.payment_strategy.pay(total);
    }
}

// Alternative: Using reference to trait object
fn process_payment(strategy: &dyn PaymentStrategy, amount: f64) {
    strategy.pay(amount);
}
}

3. Enum-Based Strategy (Rust Idiomatic)

#![allow(unused)]
fn main() {
// Enum-based approach - often more idiomatic in Rust
#[derive(Debug, Clone)]
enum PaymentMethod {
    CreditCard { card_number: String },
    PayPal { email: String },
    BankTransfer { account_number: String },
    Cryptocurrency { wallet_address: String },
}

impl PaymentMethod {
    fn pay(&self, amount: f64) {
        match self {
            PaymentMethod::CreditCard { card_number } => {
                println!("Paid ${:.2} using credit card: {}", amount, card_number);
            }
            PaymentMethod::PayPal { email } => {
                println!("Paid ${:.2} using PayPal: {}", amount, email);
            }
            PaymentMethod::BankTransfer { account_number } => {
                println!("Paid ${:.2} using bank transfer: {}", amount, account_number);
            }
            PaymentMethod::Cryptocurrency { wallet_address } => {
                println!("Paid ${:.2} using crypto wallet: {}", amount, wallet_address);
            }
        }
    }
}

struct ModernShoppingCart {
    payment_method: PaymentMethod,
    items: Vec<(String, f64)>,
}

impl ModernShoppingCart {
    fn new(payment_method: PaymentMethod) -> Self {
        Self {
            payment_method,
            items: Vec::new(),
        }
    }

    fn set_payment_method(&mut self, method: PaymentMethod) {
        self.payment_method = method;
    }

    fn add_item(&mut self, name: String, price: f64) {
        self.items.push((name, price));
    }

    fn checkout(&self) {
        let total: f64 = self.items.iter().map(|(_, price)| price).sum();
        self.payment_method.pay(total);
    }
}
}

4. Advanced: Function Pointer Strategy

#![allow(unused)]
fn main() {
// Using function pointers for strategy
type PaymentFunction = fn(f64, &str);

fn credit_card_payment(amount: f64, details: &str) {
    println!("Paid ${:.2} using credit card: {}", amount, details);
}

fn paypal_payment(amount: f64, details: &str) {
    println!("Paid ${:.2} using PayPal: {}", amount, details);
}

struct FunctionShoppingCart {
    payment_fn: PaymentFunction,
    payment_details: String,
    items: Vec<(String, f64)>,
}

impl FunctionShoppingCart {
    fn new(payment_fn: PaymentFunction, payment_details: String) -> Self {
        Self {
            payment_fn,
            payment_details,
            items: Vec::new(),
        }
    }

    fn checkout(&self) {
        let total: f64 = self.items.iter().map(|(_, price)| price).sum();
        (self.payment_fn)(total, &self.payment_details);
    }
}
}

Complete Example and Usage

fn main() {
    println!("=== Strategy Pattern Examples ===\n");

    // 1. Static dispatch example
    println!("1. Static Dispatch:");
    let mut static_cart = ShoppingCart::new(CreditCardPayment::new("1234-5678-9012-3456".to_string()));
    static_cart.add_item("Laptop".to_string(), 999.99);
    static_cart.add_item("Mouse".to_string(), 29.99);
    static_cart.checkout();

    // 2. Dynamic dispatch example
    println!("\n2. Dynamic Dispatch:");
    let mut dynamic_cart = DynamicShoppingCart::new(
        Box::new(PayPalPayment::new("user@example.com".to_string()))
    );
    dynamic_cart.add_item("Keyboard".to_string(), 79.99);
    dynamic_cart.checkout();

    // Switch strategy at runtime
    dynamic_cart.set_payment_strategy(
        Box::new(CreditCardPayment::new("9876-5432-1098-7654".to_string()))
    );
    dynamic_cart.checkout();

    // 3. Enum-based approach
    println!("\n3. Enum-based Strategy:");
    let mut modern_cart = ModernShoppingCart::new(
        PaymentMethod::Cryptocurrency {
            wallet_address: "1A2B3C4D5E6F7G8H9I0J".to_string()
        }
    );
    modern_cart.add_item("Graphics Card".to_string(), 599.99);
    modern_cart.checkout();

    // 4. Function pointer approach
    println!("\n4. Function Pointer Strategy:");
    let mut fn_cart = FunctionShoppingCart::new(
        credit_card_payment,
        "4444-3333-2222-1111".to_string()
    );
    fn_cart.items.push(("Monitor".to_string(), 299.99));
    fn_cart.checkout();

    // 5. Demonstrating runtime strategy selection
    println!("\n5. Runtime Strategy Selection:");
    demonstrate_runtime_selection();
}

fn demonstrate_runtime_selection() {
    use std::io;

    println!("Choose payment method:");
    println!("1. Credit Card");
    println!("2. PayPal");
    println!("3. Bank Transfer");

    // Simulate user input (in real code, you'd read from stdin)
    let choice = 2; // Simulated input

    let payment_method = match choice {
        1 => PaymentMethod::CreditCard {
            card_number: "1111-2222-3333-4444".to_string()
        },
        2 => PaymentMethod::PayPal {
            email: "customer@email.com".to_string()
        },
        3 => PaymentMethod::BankTransfer {
            account_number: "ACC123456789".to_string()
        },
        _ => PaymentMethod::CreditCard {
            card_number: "DEFAULT-CARD".to_string()
        },
    };

    let mut cart = ModernShoppingCart::new(payment_method);
    cart.add_item("Book".to_string(), 19.99);
    cart.checkout();
}

Performance Analysis

Static Dispatch (Generics)

  • Compile-time optimization: Zero runtime cost for method calls
  • Code size: May increase due to monomorphization
  • Flexibility: Strategy must be known at compile time

Dynamic Dispatch (Trait Objects)

  • Runtime flexibility: Can switch strategies at runtime
  • Vtable overhead: Small runtime cost for virtual method calls
  • Memory: Additional pointer indirection

Enum-Based Strategy

  • Best of both worlds: Pattern matching is optimized by compiler
  • Memory efficient: No heap allocation or vtables
  • Type safety: All variants known at compile time

Best Practices

  1. Use enums for closed sets of strategies known at compile time
  2. Use trait objects for open sets or plugin architectures
  3. Use generics for maximum performance when strategy is known at compile time
  4. Consider function pointers for simple strategies without state

When to Use Each Approach

  • Enums: When you control all strategies and they're known at compile time
  • Trait objects: When you need runtime flexibility or plugin systems
  • Generics: When performance is critical and strategy is compile-time known
  • Function pointers: For simple stateless strategies

Core Rust concepts introduced

Building on the foundation patterns, the Strategy pattern introduces several new concepts related to dispatch and polymorphism:

1. Trait Objects and Dynamic Dispatch

#![allow(unused)]
fn main() {
Box<dyn PaymentStrategy>  // Trait object
&dyn PaymentStrategy      // Reference to trait object
}
  • Trait objects: Allow runtime polymorphism through vtables
  • dyn keyword: Explicitly marks dynamic dispatch
  • Object safety: Not all traits can be made into trait objects
  • Vtable overhead: Small runtime cost for virtual method calls

2. Static vs Dynamic Dispatch Trade-offs

#![allow(unused)]
fn main() {
// Static dispatch - zero runtime cost
fn process_static<T: PaymentStrategy>(strategy: &T, amount: f64) {
    strategy.pay(amount);  // Compile-time method resolution
}

// Dynamic dispatch - runtime flexibility
fn process_dynamic(strategy: &dyn PaymentStrategy, amount: f64) {
    strategy.pay(amount);  // Runtime method lookup via vtable
}
}
  • Monomorphization: Compiler generates separate code for each concrete type
  • Code bloat vs performance: Static dispatch can increase binary size
  • Runtime flexibility: Dynamic dispatch allows changing behavior at runtime

3. Object Safety Rules

#![allow(unused)]
fn main() {
trait ObjectSafe {
    fn method(&self);  // ✅ Object safe
}

trait NotObjectSafe {
    fn generic_method<T>(&self);  // ❌ Not object safe
    fn static_method();           // ❌ Not object safe
}
}
  • Object safety requirements: Methods must be callable through trait objects
  • No generic methods: Generics prevent trait object creation
  • No associated functions: Static methods can't be called on trait objects
  • Self limitations: Self must be behind a pointer

4. Box Smart Pointers for Heap Allocation

#![allow(unused)]
fn main() {
Box<dyn PaymentStrategy>  // Owned trait object on heap
}
  • Heap allocation: Box<T> allocates T on the heap
  • Owned data: Box<T> owns its data, unlike references
  • Deref coercion: Box<T> automatically derefs to T
  • Zero-cost abstraction: No runtime overhead beyond the allocation

5. Performance Implications of Dispatch Types

Static dispatch characteristics:

  • Zero runtime overhead for method calls
  • Potential code size increase due to monomorphization
  • All optimizations possible (inlining, etc.)
  • Strategy must be known at compile time

Dynamic dispatch characteristics:

  • Small vtable lookup overhead
  • Consistent code size regardless of number of implementers
  • Limited optimization opportunities
  • Full runtime flexibility

6. Vtables and Runtime Polymorphism

#![allow(unused)]
fn main() {
// Under the hood, trait objects use vtables
struct TraitObject {
    data: *mut (),           // Pointer to the actual data
    vtable: *const VTable,   // Pointer to method implementations
}
}
  • Vtable structure: Contains pointers to trait method implementations
  • Double indirection: Data pointer + vtable pointer
  • Method resolution: Runtime lookup in vtable for correct implementation
  • Memory layout: Understanding the cost of dynamic dispatch

These concepts demonstrate how Rust provides both zero-cost abstractions (static dispatch) and runtime flexibility (dynamic dispatch), allowing you to choose the right trade-off for each situation. The Strategy pattern showcases when and how to use each approach effectively.