🎓 A1Learner
🔍

Rust Strategy Pattern

The strategy pattern puts each way of doing one job behind a single trait. The rest of the program asks the trait to do the job and never cares which strategy stands behind it.

One trait, one job

Every pricing rule answers the same question: what does this price become?

trait Pricing {
    fn total(&self, price: u32) -> u32;
}

struct FullPrice;
struct HalfPrice;

Each strategy is a small type

FullPrice changes nothing. HalfPrice halves. Neither knows the other exists.

impl Pricing for FullPrice {
    fn total(&self, price: u32) -> u32 { price }
}

impl Pricing for HalfPrice {
    fn total(&self, price: u32) -> u32 { price / 2 }
}

Pick at run time

A flag chooses the strategy, and the caller just calls total.

let sale = true;
let pricing: &dyn Pricing = if sale { &HalfPrice } else { &FullPrice };
println!("pay: {}", pricing.total(100));

Or pick at compile time

A generic parameter bakes the choice into the compiled code instead — one version per strategy, no trait object involved.

fn checkout<P: Pricing>(pricing: P, price: u32) -> u32 {
    pricing.total(price)
}

Example

trait Pricing {
    fn total(&self, price: u32) -> u32;
}

struct FullPrice;
struct HalfPrice;
struct MemberPrice;

impl Pricing for FullPrice {
    fn total(&self, price: u32) -> u32 {
        price
    }
}

impl Pricing for HalfPrice {
    fn total(&self, price: u32) -> u32 {
        price / 2
    }
}

impl Pricing for MemberPrice {
    fn total(&self, price: u32) -> u32 {
        price * 9 / 10
    }
}

fn main() {
    let strategies: Vec<(&str, Box<dyn Pricing>)> = vec![
        ("full", Box::new(FullPrice)),
        ("half", Box::new(HalfPrice)),
        ("member", Box::new(MemberPrice)),
    ];
    for (name, pricing) in &strategies {
        println!("{name}: {}", pricing.total(200));
    }
}

Key points

  • Strategy pattern: put each interchangeable way of doing a job behind one trait
  • Pick a strategy at run time or at compile time.
  • At run time, use &dyn Trait or Box<dyn Trait>.
  • At compile time, use a generic parameter P: Trait
  • The caller leans on the trait alone: adding a new strategy means adding a new type, never editing the caller
  • The family of strategies may be closed and small.
  • Then one enum does the same job.
  • It uses a match.
  • It needs less machinery
  • sort_by_key takes its comparison as a closure.
  • map takes the step to apply.
  • A closure is the smallest strategy there is
  • A sale may bring half price, and a member may get a discount.

In this example, three strategies stand behind the Pricing trait. FullPrice returns the price unchanged. HalfPrice halves it. MemberPrice takes a tenth off. The list pairs each name with a boxed strategy, and the loop asks each one for the total of 200. The loop never learns which rule produced which number — it only knows the trait.

Note

Strategies have been in use all along.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A report writer must print as text today and as JSON next month, the choice made by a flag at run time. Which design fits best?