Design Patterns in Rust: Strategy
The strategy pattern is a behavorial design pattern that allows you to define a family of algorithms, encapsulate them as an object, and with the help of traits make them interchangeable.
What does it look like?
A short explanation:
- The context wants to apply some sort of algorithm, and for that purpose it contains an object which implements the Strategy interface.
- When the Strategy is needed an object of either StrategyA or StrategyB is initiated, and the algorithm method is called.
It could be argued that this is a form of dependency injection since methods are only called on an interface and not on concrete objects.
Open your terminal in an empty directory and type:
cargo new strategy_pattern
cd strategy_pattern
Then open your IDE and open main.rs in the src folder.
We will start by defining the TravelStrategy, so add to main.rs:
trait TravelStrategy {
fn travel(&self,distance: i16);
}
Then we will define the TrainStrategy:
struct TrainStrategy;
impl TravelStrategy for TrainStrategy {
fn travel(&self,distance: i16) {
println!("Travelled {} km by train",distance);
}
}
The TrainStrategy struct is empty. We implement a travel method on it, which just prints the distance travelled.
The CarStrategy is similar:
struct CarStrategy;
impl TravelStrategy for CarStrategy {
fn travel(&self,distance: i16) {
println!("Travelled {} km by car",distance);
}
}
Now we to be able to send everyone on their way, we will start by defining the TravelHub:
struct TravelHub<T:TravelStrategy> {
strategy:T,
}
We use generics here, the T:TravelStrategy means we can have any type as long as it implements the TravelStrategy trait.
Why generics ? Generics make it easier to pass in a concrete class, without resorting to Box-types or similar, and it makes the code much more readable.
Now implement the TravelHub:
impl<T:TravelStrategy> TravelHub<T> {
fn new(strategy:T)->Self {
Self {
strategy: strategy
}
}
fn customer_travel(&self,distance:i16) {
self.strategy.travel(distance);
}
}
A short explanation:
- First we define a constructor, which gets a concrete class as its parameter. This concrete class must implement the TravelStrategy trait.
- Then we have the customer_travel, which just calls the travel method on the chosen strategy.
See if it works:
fn main() {
let travelhub=TravelHub::new(TrainStrategy{});
travelhub.customer_travel(20);
}
Line by line:
- Instantiate a travelhub, with a TrainStrategy as parameter. Since TrainStrategy implements the TravelStrategy trait, we can pass this as a parameter.
- Call the customer_travel method to see if it all works
As you can see the implementation is quite simple. The use of generics makes it possible to circumvent the use of Box-like constructions. It makes the code much more readable.
This pattern could also be said to be a form of dependency injection since only methods on interfaces are called where the caller is ignorant of the underlying class.