Codementor Events

Design Patterns in Rust: Strategy

Published Apr 03, 2023

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?

strategy.drawio.png

A short explanation:

  1. The context wants to apply some sort of algorithm, and for that purpose it contains an object which implements the Strategy interface.
  2. 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:

  1. First we define a constructor, which gets a concrete class as its parameter. This concrete class must implement the TravelStrategy trait.
  2. 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:

  1. Instantiate a travelhub, with a TrainStrategy as parameter. Since TrainStrategy implements the TravelStrategy trait, we can pass this as a parameter.
  2. 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.

Discover and read more posts from Iede Snoek
get started