Strategy Pattern

The problem

Let's begin learning this design pattern by asking a very fundamental question - Why?

Consider a problem in which you are building a video game which has a hero in it. The hero has a weapon and can use it. Initially your game has only a melee weapon wielding hero. As time goes on your game gains popularity and you get requests to add a ranged hero, so you go back to your implementation of the hero and you add functionality to the weapon. The hero can now wield a bow.

Great!

Sales are growing, game is getting more popular. Now the users want a magical hero. Hmm. Now you have to add another functionality to the Hero's weapon. This is great but as time goes on, your users demand more wilder weapons, with weird capabilities. Now since you are the creator you can decide to stop adding more weapons, but this seems bad for business. More variety means more creativity in builds. The problem here is that we need to modify our weapon use functionality every time we add a new weapon, this bloats up the Hero implementation by a lot. This will eventually become a nightmare to maintain.

The solution

So how do we solve this? Enter the Strategy Pattern. Using this pattern, we can easily add new weapon 'strategies' to our Hero all without messing up the whole implementation with code. What we will do is take all the different ways to use the weapon and extract it into a 'Strategy'.

Here's roughly what we are going to build...

Strategy UML Diagram

The code

Lets get started by creating a new project using cargo.

$ cargo init gof

Create a new blank file called lib.rs and another called strategy.rs. Add the following to strategy.rs

pub trait WeaponStrategy {
    fn use_weapon(&self) -> &str;
}

pub struct MeleeStrategy;

impl WeaponStrategy for MeleeStrategy {
    fn use_weapon(&self) -> &str {
        "slash slash!"
    }
}

pub struct MagicStrategy;

impl WeaponStrategy for MagicStrategy {
    fn use_weapon(&self) -> &str {
        "poof poof!"
    }
}

pub struct Hero<T: WeaponStrategy> {
    weapon_strategy: T,
}

impl<T: WeaponStrategy> Hero<T> {
    pub fn new(weapon_strategy: T) -> Self {
        Self { weapon_strategy }
    }

    pub fn use_weapon(&self) -> &str {
        self.weapon_strategy.use_weapon()
    }
}

Here we take advantage of Rust's generic capabilities and the ability to constraint the generic types to implement the Strategy Pattern in our game. Great so the implementation is done. Lets go ahead and write some unit tests for this code.

Add the following to lib.rs

pub mod strategy;


#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn strategy_pattern_tests() {
        let melee_hero = strategy::Hero::new(strategy::MeleeStrategy);
        let magical_hero = strategy::Hero::new(strategy::MagicStrategy);

        assert_eq!(melee_hero.use_weapon(), "slash slash!");
        assert_eq!(magical_hero.use_weapon(), "poof poof!");
    }
}

Now finally we can run our tests using cargo.

$ cargo test
running 1 test
test tests::strategy_pattern_tests ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00

Cool all tests passed. Note how the Hero implementation does not have to worry about the details of the implementation and can guarantee that it just works since it conforms to the WeaponStrategy interface.