forked from Fomalhauthmj/patterns
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Strategy pattern (rust-unofficial#146)
- Loading branch information
1 parent
264cddf
commit 497e56e
Showing
3 changed files
with
178 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
# Generated output of mdbook | ||
/book | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
# 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) |