You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
patterns/src/patterns/behavioural/strategy.md

183 lines
5.8 KiB
Markdown

# 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 algorithms 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 completely new format, or just modify one of the existing
formats.
## Example
In this example our invariants (or abstractions) are `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, buf: &mut String);
}
struct Report;
impl Report {
// Write should be used but we kept it as String to ignore error handling
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, 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(',');
}
if !data.is_empty() {
buf.pop(); // remove extra , at the end
}
buf.push(']');
}
}
fn main() {
let mut s = String::from("");
Report::generate(Text, &mut s);
assert!(s.contains("one 1"));
assert!(s.contains("two 2"));
s.clear(); // reuse the same buffer
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 a specific trait to
implement and its method defining the concrete algorithm implementation
processing the result, i.e., `Formatter` and `format(...)`.
## 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)