Strategy design pattern in Rust

Page content

I just followed Rust Design Pattern - Strategy (aka Policy).

Simple explanation

Make code abstract as possible.

… given an algorithm solving a particular problem, we define only the skeleton of the algorithm at an abstract level, and we separate the specific algorithm’s implementation into different parts.

The stragety pattern are used frequently with dependency inversion (it is also related with high level modules).

Example

Code

use std::collections::HashMap;

type Data = HashMap<String, u32>;

trait Formatter {
    fn format(&self, data: &Data, buf: &mut String);
}

struct Report;

impl Report {
    fn generate<T: Formatter>(g: T, s: &mut String) {
        let mut data = HashMap::new();
        data.insert("one".to_string(), 1);
        data.insert("two".to_string(), 2);
        g.format(&data, s);
    }
}

struct Text;
impl Formatter for Text {
    fn format(&self, data: &Data, buf: &mut String) {
        for (k, v) in data {
            let entry = format!("{} {}\n", k, v);
            buf.push_str(&entry);
        }
    }
}

struct Json;
impl Formatter for Json {
    fn format(&self, data: &Data, buf: &mut String) {
        buf.push('[');
        for (k, v) in data.into_iter() {
            let entry = format!(r#"{{"{}":"{}"}}"#, k, v);
            buf.push_str(&entry);
            buf.push(',');
        }
        buf.pop(); // remove extra , at the end
        buf.push(']');
    }
}

fn main() {
    let mut s = String::from("");
    Report::generate(Text, &mut s);
    // s = "two 2\none 1\n"
    assert!(s.contains("one 1"));
    assert!(s.contains("two 2"));

    s.clear(); // reuse the same buffer
    Report::generate(Json, &mut s);
    // s = [{"one":"1"},{"two":"2"}]
    assert!(s.contains(r#"{"one":"1"}"#));
    assert!(s.contains(r#"{"two":"2"}"#));
}

Quick explanation

  • In main() function, user use Report::generate function, and this is the only function user uses.
  • But the behavior of the function should be changed by the strct of its first parameter. In this case, Text or Json.
  • [Abstraction] The simple solution would be to introduce a traitFormatter which has format method. If you implement the trait for the structs, Text and Json, differently and generate function takes the Formatter-bounded struct, the only thing generater should care about is “calling format method of the first parameter struct”.
    • By virtue of trait, we don’t need to create if/else switch in generate method.
  • From generate function point, the actual type of its first parameter (Text or Json) is called stragety.
  • generate function is an abstract function because it doesn’t know how strategies implement format.

Actual use case

  • Suppose that we need another format, like CSV. This is a new strategy.
  • In that case, you need to create a new struct CSV and implement format for this struct so that generate can use this stragety.