This post is about how to use the Strategy Design Pattern in Rust.
Strategy Pattern
This Design Pattern replaces a behavior with different implementation at run-time or sometimes at load-time using configuration. Two entities could have different implementations of the same behavior. Please read the Strategy Pattern for more information.
Strategy Pattern Implementation
Our example is about ducks. A model duck, a mallard duck, and a dead duck. These are their behaviors:
- A model duck does not fly but can quack.
- A Mallard duck can do both.
- A dead duck cannot do any of them!
Then, we will modify the codes later to add a RoboDuck that has the same set of behaviors but works differently.
The following struct represents a duck.
1 2 3 4 5 | struct Duck { name: String, fly_behavior: Box<dyn FlyBehavior>, quack_behavior: Box<dyn QuackBehavior> } |
fly_behavior and quack_behavior are traits.
1 2 3 4 5 6 7 | trait FlyBehavior { fn fly(&self); } trait QuackBehavior { fn quack(&self); } |
Behavior Strategy and Implementations
The first is can-fly behavior. Some ducks can fly, some can’t.
1 2 3 4 5 6 | struct FlyYesWay {} impl FlyBehavior for FlyYesWay { fn fly(&self) { println!("I can fly!"); } } |
Cannot-fly behavior.
1 2 3 4 5 6 | struct FlyNoWay {} impl FlyBehavior for FlyNoWay { fn fly(&self) { println!("I cannot fly!"); } } |
Cannot-quack behavior.
1 2 3 4 5 6 | struct NoQuack {} impl QuackBehavior for NoQuack { fn quack(&self) { println!("Dead ducks don't quack!"); } } |
Can-quack behavior.
1 2 3 4 5 6 | struct Quack {} impl QuackBehavior for Quack { fn quack(&self) { println!("Quack!"); } } |
struct Duck impl
The new function uses the Factory Design Pattern to make our codes simpler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | impl Duck { fn new(duct_type: DuckTypeEnum) -> Duck { match duct_type { DuckTypeEnum::MODEL => Duck {name: "Model Duck".to_string(), fly_behavior: Box::new(FlyNoWay{}), quack_behavior: Box::new(Quack{})}, DuckTypeEnum::MALLARD => Duck {name: "Mallard Duck".to_string(), fly_behavior: Box::new(FlyYesWay{}), quack_behavior: Box::new(Quack{})}, DuckTypeEnum::DEAD => Duck {name: "Dead Duck".to_string(), fly_behavior: Box::new(FlyNoWay{}), quack_behavior: Box::new(NoQuack{})} } } fn perform_fly(&self) { self.fly_behavior.fly(); } fn perform_quack(&self) { self.quack_behavior.quack(); } } impl Debug for Duck { fn fmt(&self, f: &mut Formatter<'_>) -> Result { return write!(f, "{}", self.name); } } |
The impl for the Debug trait displays the duck’s name via println! and a struct instance.
Usage and Demo
These codes create three types of duck, display their names, and make them behave.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | fn main() { let mut model_duck = Duck::new(DuckTypeEnum::MODEL); println!("Duck: {:?}", model_duck); model_duck.perform_fly(); model_duck.perform_quack(); println!("##########"); model_duck = Duck::new(DuckTypeEnum::MALLARD); println!("Duck: {:?}", model_duck); model_duck.perform_fly(); model_duck.perform_quack(); println!("##########"); model_duck = Duck::new(DuckTypeEnum::DEAD); println!("Duck: {:?}", model_duck); model_duck.perform_fly(); model_duck.perform_quack(); } |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Duck: Model Duck I cannot fly! Quack! ########## Duck: Mallard Duck I can fly! Quack! ########## Duck: Dead Duck I cannot fly! Dead ducks don't quack! Process finished with exit code 0 |
So, where is the part about replacing behavior with different implementation? It is in the new function for the Duck struct. Use Strategy Design pattern in Rust to quickly come up with any type of duck with correct set behavior without much code changes.
How do we change the codes if we include a RoboDuck? First, we make DuckTypeEnum::ROBO. Second, create structs for “robotic” behaviors and implement them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // ... struct RoboDuckFly {} impl FlyBehavior for RoboDuckFly { fn fly(&self) { println!("Rocket-fly!"); } } struct RoboDuckQuack {} impl QuackBehavior for RoboDuckQuack { fn quack(&self) { println!("Quack like a robot!"); } } // ... |
Third, update the new function as follows. Notice we added a match rule to create a RoboDuck by specifying the new behaviors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // ... impl Duck { fn new(duct_type: DuckTypeEnum) -> Duck { match duct_type { DuckTypeEnum::MODEL => Duck {name: "Model Duck".to_string(), fly_behavior: Box::new(FlyNoWay{}), quack_behavior: Box::new(Quack{})}, DuckTypeEnum::MALLARD => Duck {name: "Mallard Duck".to_string(), fly_behavior: Box::new(FlyYesWay{}), quack_behavior: Box::new(Quack{})}, DuckTypeEnum::DEAD => Duck {name: "Dead Duck".to_string(), fly_behavior: Box::new(FlyNoWay{}), quack_behavior: Box::new(NoQuack{})}, DuckTypeEnum::ROBOT => Duck { name: "RobotDuck".to_string(), fly_behavior: Box::new(RoboDuckFly{}), quack_behavior: Box::new(RoboDuckQuack{})}, } } // ... |
Finally, use the RoboDuck in main.rs.
1 2 3 4 5 6 | // ... model_duck = Duck::new(DuckTypeEnum::ROBOT); println!("Duck: {:?}", model_duck); model_duck.perform_fly(); model_duck.perform_quack(); // ... |
We tested the codes using Rust 1.39.0.