Course contentsShow
← Rust
Lesson 379 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

The Strategy Pattern with Lifetimes

Define polymorphic behaviors that rely on borrowed state without leaking memory.

In this lesson, you will learn how to implement the Strategy pattern when your algorithms need to perform operations on borrowed data without violating ownership rules.

The Problem: Borrowed Strategies

The Strategy pattern allows us to swap algorithms at runtime by defining a trait that encapsulates a behavior. Often, an algorithm needs to process data that it doesn't own—like a large buffer or a database connection. If we define a strategy that holds a reference to this data, we must explicitly link the strategy's lifetime to the data it borrows.

Without careful lifetime annotation, the compiler won't know if the borrowed data will outlive the strategy object, leading to potential memory unsafety.

Implementation with Lifetime Bounds

To implement this, we add a lifetime parameter 'a to our trait. This tells the compiler that the strategy is bound to the duration of the data it references.

trait DataProcessor<'a> {
    fn process(&self, data: &'a str) -> String;
}

struct UppercaseStrategy;
impl<'a> DataProcessor<'a> for UppercaseStrategy {
    fn process(&self, data: &'a str) -> String {
        data.to_uppercase()
    }
}

struct Context<'a> {
    strategy: Box<dyn DataProcessor<'a> + 'a>,
}

fn main() {
    let input = String::from("hello world");
    let strategy = UppercaseStrategy;
    
    let ctx = Context {
        strategy: Box::new(strategy),
    };
    
    println!("{}", ctx.strategy.process(&input));
}

Why this works

By attaching 'a to the DataProcessor trait, we ensure that any implementation is restricted to working with data that lives at least as long as the strategy itself. When we use Box<dyn DataProcessor<'a> + 'a>, we are declaring a "trait object with lifetime bounds."

The + 'a after the trait object is crucial; it explicitly tells the Rust compiler that the dyn trait object itself contains references valid for at least 'a. This prevents the "leaking" of memory, as the compiler will reject any code that attempts to store the strategy in a scope where the borrowed data is no longer valid. By formalizing this relationship, we keep our polymorphic behaviors flexible yet memory-safe.

Key Takeaway: By binding lifetime parameters to trait objects in the Strategy pattern, you enforce that your polymorphic algorithms strictly adhere to the ownership constraints of the data they process.