Home/Design Patterns in Rust/Strategy Pattern

Strategy Pattern

Interchangeable algorithms with traits

intermediate
strategybehavioraltraits
🎮 Interactive Playground

What is the Strategy Pattern?

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. In Rust, traits are the natural way to express this - each strategy implements a common trait.

The Problem

When you need strategies in Rust:

  • Algorithm selection: Choose compression, sorting, or encryption at runtime
  • Behavior variation: Different validation rules, pricing calculations
  • Testing: Inject mock strategies for testing
  • Configuration: Change behavior without changing code

Example Code

use std::fmt::Debug;

/// Strategy trait for compression algorithms
pub trait CompressionStrategy: Debug {
    fn compress(&self, data: &[u8]) -> Vec<u8>;
    fn decompress(&self, data: &[u8]) -> Vec<u8>;
    fn name(&self) -> &str;
}

/// Concrete strategy: No compression
#[derive(Debug, Default)]
pub struct NoCompression;

impl CompressionStrategy for NoCompression {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        data.to_vec()
    }

    fn decompress(&self, data: &[u8]) -> Vec<u8> {
        data.to_vec()
    }

    fn name(&self) -> &str {
        "none"
    }
}

/// Concrete strategy: Run-length encoding
#[derive(Debug, Default)]
pub struct RleCompression;

impl CompressionStrategy for RleCompression {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        if data.is_empty() {
            return Vec::new();
        }

        let mut result = Vec::new();
        let mut count = 1u8;
        let mut current = data[0];

        for &byte in &data[1..] {
            if byte == current && count < 255 {
                count += 1;
            } else {
                result.push(count);
                result.push(current);
                count = 1;
                current = byte;
            }
        }
        result.push(count);
        result.push(current);

        result
    }

    fn decompress(&self, data: &[u8]) -> Vec<u8> {
        let mut result = Vec::new();
        for chunk in data.chunks(2) {
            if chunk.len() == 2 {
                let count = chunk[0];
                let byte = chunk[1];
                result.extend(std::iter::repeat(byte).take(count as usize));
            }
        }
        result
    }

    fn name(&self) -> &str {
        "rle"
    }
}

/// Context that uses the strategy
pub struct FileCompressor {
    strategy: Box<dyn CompressionStrategy>,
}

impl FileCompressor {
    pub fn new(strategy: Box<dyn CompressionStrategy>) -> Self {
        FileCompressor { strategy }
    }

    pub fn set_strategy(&mut self, strategy: Box<dyn CompressionStrategy>) {
        self.strategy = strategy;
    }

    pub fn compress_file(&self, data: &[u8]) -> Vec<u8> {
        println!("Compressing with {} strategy", self.strategy.name());
        self.strategy.compress(data)
    }

    pub fn decompress_file(&self, data: &[u8]) -> Vec<u8> {
        self.strategy.decompress(data)
    }
}

/// Generic strategy using generics instead of trait objects
pub struct GenericCompressor<S: CompressionStrategy> {
    strategy: S,
}

impl<S: CompressionStrategy> GenericCompressor<S> {
    pub fn new(strategy: S) -> Self {
        GenericCompressor { strategy }
    }

    pub fn compress(&self, data: &[u8]) -> Vec<u8> {
        self.strategy.compress(data)
    }
}

/// Strategy with closures (functional approach)
pub struct FunctionStrategy<F, G>
where
    F: Fn(&[u8]) -> Vec<u8>,
    G: Fn(&[u8]) -> Vec<u8>,
{
    compress_fn: F,
    decompress_fn: G,
}

impl<F, G> FunctionStrategy<F, G>
where
    F: Fn(&[u8]) -> Vec<u8>,
    G: Fn(&[u8]) -> Vec<u8>,
{
    pub fn new(compress_fn: F, decompress_fn: G) -> Self {
        FunctionStrategy { compress_fn, decompress_fn }
    }

    pub fn compress(&self, data: &[u8]) -> Vec<u8> {
        (self.compress_fn)(data)
    }

    pub fn decompress(&self, data: &[u8]) -> Vec<u8> {
        (self.decompress_fn)(data)
    }
}

/// Payment processing strategies
pub trait PaymentStrategy {
    fn pay(&self, amount: f64) -> Result<String, String>;
    fn name(&self) -> &str;
}

#[derive(Debug)]
pub struct CreditCardPayment {
    card_number: String,
    expiry: String,
}

impl CreditCardPayment {
    pub fn new(card_number: &str, expiry: &str) -> Self {
        CreditCardPayment {
            card_number: card_number.to_string(),
            expiry: expiry.to_string(),
        }
    }
}

impl PaymentStrategy for CreditCardPayment {
    fn pay(&self, amount: f64) -> Result<String, String> {
        // Simulate payment processing
        if self.card_number.len() < 13 {
            return Err("Invalid card number".to_string());
        }
        Ok(format!(
            "Paid ${:.2} with credit card ending in {}",
            amount,
            &self.card_number[self.card_number.len() - 4..]
        ))
    }

    fn name(&self) -> &str {
        "Credit Card"
    }
}

#[derive(Debug)]
pub struct PayPalPayment {
    email: String,
}

impl PayPalPayment {
    pub fn new(email: &str) -> Self {
        PayPalPayment { email: email.to_string() }
    }
}

impl PaymentStrategy for PayPalPayment {
    fn pay(&self, amount: f64) -> Result<String, String> {
        Ok(format!("Paid ${:.2} via PayPal ({})", amount, self.email))
    }

    fn name(&self) -> &str {
        "PayPal"
    }
}

#[derive(Debug)]
pub struct CryptoPayment {
    wallet_address: String,
}

impl CryptoPayment {
    pub fn new(wallet_address: &str) -> Self {
        CryptoPayment {
            wallet_address: wallet_address.to_string(),
        }
    }
}

impl PaymentStrategy for CryptoPayment {
    fn pay(&self, amount: f64) -> Result<String, String> {
        Ok(format!(
            "Paid ${:.2} in crypto to {}",
            amount,
            &self.wallet_address[..10]
        ))
    }

    fn name(&self) -> &str {
        "Cryptocurrency"
    }
}

/// Checkout that uses payment strategy
pub struct Checkout {
    items: Vec<(String, f64)>,
    payment_strategy: Option<Box<dyn PaymentStrategy>>,
}

impl Checkout {
    pub fn new() -> Self {
        Checkout {
            items: Vec::new(),
            payment_strategy: None,
        }
    }

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

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

    pub fn total(&self) -> f64 {
        self.items.iter().map(|(_, price)| price).sum()
    }

    pub fn complete_purchase(&self) -> Result<String, String> {
        let strategy = self.payment_strategy.as_ref()
            .ok_or("No payment method selected")?;

        let total = self.total();
        println!("Processing ${:.2} via {}", total, strategy.name());
        strategy.pay(total)
    }
}

impl Default for Checkout {
    fn default() -> Self {
        Self::new()
    }
}

/// Strategy selection at runtime
pub fn select_compression(name: &str) -> Box<dyn CompressionStrategy> {
    match name {
        "rle" => Box::new(RleCompression),
        "none" | _ => Box::new(NoCompression),
    }
}

fn main() {
    // Compression strategies
    let data = b"AAAAAABBBCCCCCCCC";

    let mut compressor = FileCompressor::new(Box::new(NoCompression));
    let no_compress = compressor.compress_file(data);
    println!("No compression: {} bytes", no_compress.len());

    compressor.set_strategy(Box::new(RleCompression));
    let rle_compress = compressor.compress_file(data);
    println!("RLE compression: {} bytes", rle_compress.len());

    let decompressed = compressor.decompress_file(&rle_compress);
    assert_eq!(data.as_slice(), decompressed.as_slice());
    println!("Decompression verified!");

    // Generic compressor (monomorphized, no vtable)
    let generic = GenericCompressor::new(RleCompression);
    let compressed = generic.compress(data);
    println!("Generic RLE: {} bytes", compressed.len());

    // Functional strategy
    let func_strategy = FunctionStrategy::new(
        |data| data.to_vec(), // identity compress
        |data| data.to_vec(), // identity decompress
    );
    let result = func_strategy.compress(data);
    println!("Functional strategy: {} bytes", result.len());

    // Payment strategies
    println!("\n=== Payment Strategies ===");
    let mut checkout = Checkout::new();
    checkout.add_item("Rust Book", 49.99);
    checkout.add_item("Coffee", 4.50);

    // Pay with credit card
    checkout.set_payment_method(Box::new(
        CreditCardPayment::new("4111111111111111", "12/25")
    ));
    println!("{}", checkout.complete_purchase().unwrap());

    // Switch to PayPal
    checkout.set_payment_method(Box::new(
        PayPalPayment::new("user@example.com")
    ));
    println!("{}", checkout.complete_purchase().unwrap());

    // Runtime strategy selection
    let strategy = select_compression("rle");
    println!("\nSelected strategy: {}", strategy.name());
}

Why This Works

  1. Traits as interfaces: Define behavior contract
  2. Box: Runtime polymorphism when needed
  3. Generics: Zero-cost abstraction when strategy is known at compile time
  4. Closures: Lightweight strategies without defining types

Strategy vs. Other Patterns

| Pattern | Purpose | Rust Implementation |

|---------|---------|---------------------|

| Strategy | Algorithm selection | Trait + implementations |

| State | Behavior based on state | Enum or trait objects |

| Template Method | Algorithm skeleton | Trait with default methods |

| Command | Encapsulate action | Fn trait or command struct |

When to Use

  • Multiple algorithms: Sorting, compression, encryption
  • Runtime configuration: Choose behavior from config/user input
  • Testability: Inject mock implementations
  • Open/Closed principle: Add strategies without modifying context

⚠️ Anti-patterns

// DON'T: Strategy that does too much
trait BadStrategy {
    fn process(&self, data: &[u8]) -> Vec<u8>;
    fn validate(&self, data: &[u8]) -> bool;
    fn log(&self, message: &str);
    fn connect_to_database(&self); // Too many responsibilities!
}

// DON'T: Enum when extensibility is needed
enum ClosedStrategy {
    A, B, C, // Can't add new strategies without modifying this enum
}

// DO: Open for extension
trait OpenStrategy {
    fn execute(&self);
}
// Anyone can implement new strategies

Real-World Usage

  • serde: Serialization strategies for different formats
  • reqwest: TLS backends (native-tls, rustls)
  • rand: Random number generators
  • log: Logger implementations

Exercises

  1. Add a GzipCompression strategy (using flate2 crate)
  2. Implement a sorting strategy that can use different algorithms
  3. Create a retry strategy with configurable backoff
  4. Build a validation strategy system with composable validators

🎮 Try it Yourself

🎮

Strategy Pattern - Playground

Run this code in the official Rust Playground