|
|
|
|
# Strategy (aka Policy)
|
|
|
|
|
|
|
|
|
|
## Description
|
|
|
|
|
|
|
|
|
|
The [Strategy design pattern](https://en.wikipedia.org/wiki/Strategy_pattern)
|
|
|
|
|
is a technique that enables separation of concerns.
|
|
|
|
|
It also allows to decouple software modules through [Dependency Inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle).
|
|
|
|
|
|
|
|
|
|
The basic idea behind the Strategy pattern is that, 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.
|
|
|
|
|
|
|
|
|
|
In this way, a client using the algorithm may choose a specific implementation, while the general algorithm workflow remains the same.
|
|
|
|
|
In other words, the abstract specification of the class does not depend on the specific implementation of the derived class,
|
|
|
|
|
but specific implementation must adhere to the abstract specification.
|
|
|
|
|
This is why we call it "Dependency Inversion".
|
|
|
|
|
|
|
|
|
|
## Motivation
|
|
|
|
|
|
|
|
|
|
Imagine we are working on a project that generates reports every month.
|
|
|
|
|
We need the reports to be generated in different formats (strategies), e.g.,
|
|
|
|
|
in `JSON` or `Plain Text` formats.
|
|
|
|
|
But things vary over time and we don't know what kind of requirement we may get in the future.
|
|
|
|
|
For example, we may need to generate our report in a completly new format,
|
|
|
|
|
or just modify one of the existing formats.
|
|
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
|
|
In this example our invariants (or abstractions) are `Context`, `Formatter`, and `Report`,
|
|
|
|
|
while `Text` and `Json` are our strategy structs.
|
|
|
|
|
These strategies have to implement the `Formatter` trait.
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
type Data = HashMap<String, u32>;
|
|
|
|
|
|
|
|
|
|
trait Formatter {
|
|
|
|
|
fn format(&self, data: &Data, s: &mut String);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Report;
|
|
|
|
|
|
|
|
|
|
impl Report {
|
|
|
|
|
fn generate<T: Formatter>(g: T, s: &mut String) {
|
|
|
|
|
// backend operations...
|
|
|
|
|
let mut data = HashMap::new();
|
|
|
|
|
data.insert("one".to_string(), 1);
|
|
|
|
|
data.insert("two".to_string(), 2);
|
|
|
|
|
// generate report
|
|
|
|
|
g.format(&data, s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Text;
|
|
|
|
|
impl Formatter for Text {
|
|
|
|
|
fn format(&self, data: &Data, s: &mut String) {
|
|
|
|
|
*s = data
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(key, val)| format!("{} {}\n", key, val))
|
|
|
|
|
.collect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Json;
|
|
|
|
|
impl Formatter for Json {
|
|
|
|
|
fn format(&self, data: &Data, s: &mut String) {
|
|
|
|
|
*s = String::from("[");
|
|
|
|
|
let mut iter = data.into_iter();
|
|
|
|
|
if let Some((key, val)) = iter.next() {
|
|
|
|
|
let entry = format!(r#"{{"{}":"{}"}}"#, key, val);
|
|
|
|
|
s.push_str(&entry);
|
|
|
|
|
while let Some((key, val)) = iter.next() {
|
|
|
|
|
s.push(',');
|
|
|
|
|
let entry = format!(r#"{{"{}":"{}"}}"#, key, val);
|
|
|
|
|
s.push_str(&entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.push(']');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut s = String::from("");
|
|
|
|
|
Report::generate(Text, &mut s);
|
|
|
|
|
assert!(s.contains("one 1"));
|
|
|
|
|
assert!(s.contains("two 2"));
|
|
|
|
|
|
|
|
|
|
Report::generate(Json, &mut s);
|
|
|
|
|
assert!(s.contains(r#"{"one":"1"}"#));
|
|
|
|
|
assert!(s.contains(r#"{"two":"2"}"#));
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Advantages
|
|
|
|
|
|
|
|
|
|
The main advantage is separation of concerns. For example, in this case `Report` does not know anything about specific
|
|
|
|
|
implementations of `Json` and `Text`, whereas the output implementations does not care about how data is
|
|
|
|
|
preprocessed, stored, and fetched.
|
|
|
|
|
The only thing they have to know is context and a specific trait and method to implement,
|
|
|
|
|
i.e,`Formatter` and `run`.
|
|
|
|
|
|
|
|
|
|
## Disadvantages
|
|
|
|
|
|
|
|
|
|
For each strategy there must be implemented at least one module, so number of modules
|
|
|
|
|
increases with number of strategies.
|
|
|
|
|
If there are many strategies to choose from then users have to know how strategies differ
|
|
|
|
|
from one another.
|
|
|
|
|
|
|
|
|
|
## Discussion
|
|
|
|
|
|
|
|
|
|
In the previous example all strategies are implemented in a single file.
|
|
|
|
|
Ways of providing different strategies includes:
|
|
|
|
|
|
|
|
|
|
- All in one file (as shown in this example, similar to being separated as modules)
|
|
|
|
|
- Separated as modules, E.g. `formatter::json` module, `formatter::text` module
|
|
|
|
|
- Use compiler feature flags, E.g. `json` feature, `text` feature
|
|
|
|
|
- Separated as crates, E.g. `json` crate, `text` crate
|
|
|
|
|
|
|
|
|
|
Serde crate is a good example of the `Strategy` pattern in action. Serde allows [full customization](https://serde.rs/custom-serialization.html)
|
|
|
|
|
of the serialization behavior by manually implementing `Serialize` and `Deserialize` traits for our type.
|
|
|
|
|
For example, we could easily swap `serde_json` with `serde_cbor` since they expose similar methods.
|
|
|
|
|
Having this makes the helper crate `serde_transcode` much more useful and ergonomic.
|
|
|
|
|
|
|
|
|
|
However, we don't need to use traits in order to design this pattern in Rust.
|
|
|
|
|
|
|
|
|
|
The following toy example demonstrates the idea of the Strategy pattern using Rust
|
|
|
|
|
`closures`:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
struct Adder;
|
|
|
|
|
impl Adder {
|
|
|
|
|
pub fn add<F>(x: u8, y: u8, f: F) -> u8
|
|
|
|
|
where
|
|
|
|
|
F: Fn(u8, u8) -> u8,
|
|
|
|
|
{
|
|
|
|
|
f(x, y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let arith_adder = |x, y| x + y;
|
|
|
|
|
let bool_adder = |x, y| {
|
|
|
|
|
if x == 1 || y == 1 {
|
|
|
|
|
1
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let custom_adder = |x, y| 2 * x + y;
|
|
|
|
|
|
|
|
|
|
assert_eq!(9, Adder::add(4, 5, arith_adder));
|
|
|
|
|
assert_eq!(0, Adder::add(0, 0, bool_adder));
|
|
|
|
|
assert_eq!(5, Adder::add(1, 3, custom_adder));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
In fact, Rust already uses this idea for `Options`'s `map` method:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let val = Some("Rust");
|
|
|
|
|
|
|
|
|
|
let len_strategy = |s: &str| s.len();
|
|
|
|
|
assert_eq!(4, val.map(len_strategy).unwrap());
|
|
|
|
|
|
|
|
|
|
let first_byte_strategy = |s: &str| s.bytes().next().unwrap();
|
|
|
|
|
assert_eq!(82, val.map(first_byte_strategy).unwrap());
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## See also
|
|
|
|
|
|
|
|
|
|
- [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern)
|
|
|
|
|
- [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection)
|
|
|
|
|
- [Policy Based Design](https://en.wikipedia.org/wiki/Modern_C++_Design#Policy-based_design)
|