策略模式

说明

策略模式是支持关注点分离的一门技术。 它还支持通过 依赖倒置来分离软件模块。

策略模式背后的基本思想是,给定一个解决特定问题的算法,我们仅在抽象层次上定义算法的框架,并将指定的算法实现分成不同的部分。

这样,使用该算法的客户端可以选择特定的实现,而通用的算法工作流可以保持不变。换句话说,类的抽象规范不依赖于派生类的具体实现,而是具体实现必须遵循抽象规范。这就是我们为什么叫它“依赖倒置”。

出发点

想象一下我们正在开发一个需要每个月生成报告的项目。我们需要用不同格式生成报告(不同策略)例如用JSON或者富文本。但是事物是在发展的,我们也不知道未来有什么需求。例如,我们也许需要用一种全新的格式生成报告,或者是修改我们已有的一种格式。

代码示例

在这个例子中我们的不变量(或者说抽象)是Context,FormatterReport,同时TextJson是我们的策略结构体。这些策略都要实现Formatter特性。

  1. use std::collections::HashMap;
  2. type Data = HashMap<String, u32>;
  3. trait Formatter {
  4. fn format(&self, data: &Data, buf: &mut String);
  5. }
  6. struct Report;
  7. impl Report {
  8. // Write should be used but we kept it as String to ignore error handling
  9. fn generate<T: Formatter>(g: T, s: &mut String) {
  10. // backend operations...
  11. let mut data = HashMap::new();
  12. data.insert("one".to_string(), 1);
  13. data.insert("two".to_string(), 2);
  14. // generate report
  15. g.format(&data, s);
  16. }
  17. }
  18. struct Text;
  19. impl Formatter for Text {
  20. fn format(&self, data: &Data, buf: &mut String) {
  21. for (k, v) in data {
  22. let entry = format!("{} {}\n", k, v);
  23. buf.push_str(&entry);
  24. }
  25. }
  26. }
  27. struct Json;
  28. impl Formatter for Json {
  29. fn format(&self, data: &Data, buf: &mut String) {
  30. buf.push('[');
  31. for (k, v) in data.into_iter() {
  32. let entry = format!(r#"{{"{}":"{}"}}"#, k, v);
  33. buf.push_str(&entry);
  34. buf.push(',');
  35. }
  36. buf.pop(); // remove extra , at the end
  37. buf.push(']');
  38. }
  39. }
  40. fn main() {
  41. let mut s = String::from("");
  42. Report::generate(Text, &mut s);
  43. assert!(s.contains("one 1"));
  44. assert!(s.contains("two 2"));
  45. s.clear(); // reuse the same buffer
  46. Report::generate(Json, &mut s);
  47. assert!(s.contains(r#"{"one":"1"}"#));
  48. assert!(s.contains(r#"{"two":"2"}"#));
  49. }

优点

主要的优点是分离关注点。举例来说,在这个例子里Report并不知道JsonText的特定实现,尽管输出的实现并不关心数据是如何被预处理、存储和抓取的。它仅仅需要知道上下文和需要实现的特定的特性和方法,就像Formatterrun

缺点

对于每个策略,必须至少实现一个模块,因此模块的数量会随着策略数量增加。如果有很多策略可供选择,那么用户就必须知道策略之间的区别。

讨论

在前面的例子中所有的策略实现都在一个文件中。提供不同策略的方式包括:

  • 所有都在一个文件中(如本例所示,类似于被分离为模块)
  • 分离成模块,例如formatter::json模块、formatter::text模块
  • 使用编译器特性标志,例如json特性、text特性
  • 分离成不同的库,例如json库、text

Serde库是策略模式的一个实践的好例子。Serde通过手动实现SerializeDeserialize特性支持完全定制化序列化的行为。例如,我们可以轻松替换serde_jsonserde_cbor因为它们暴露相似的方法。有了它,库serde_transcode更有用和符合人体工程学。

不过,我们在Rust中不需要特性来实现这个模式。

下面这个玩具例子演示了用Rust的闭包来实现策略模式的思路:

  1. struct Adder;
  2. impl Adder {
  3. pub fn add<F>(x: u8, y: u8, f: F) -> u8
  4. where
  5. F: Fn(u8, u8) -> u8,
  6. {
  7. f(x, y)
  8. }
  9. }
  10. fn main() {
  11. let arith_adder = |x, y| x + y;
  12. let bool_adder = |x, y| {
  13. if x == 1 || y == 1 {
  14. 1
  15. } else {
  16. 0
  17. }
  18. };
  19. let custom_adder = |x, y| 2 * x + y;
  20. assert_eq!(9, Adder::add(4, 5, arith_adder));
  21. assert_eq!(0, Adder::add(0, 0, bool_adder));
  22. assert_eq!(5, Adder::add(1, 3, custom_adder));
  23. }

事实上,Rust已经将这个思路用于Optionmap方法:

  1. fn main() {
  2. let val = Some("Rust");
  3. let len_strategy = |s: &str| s.len();
  4. assert_eq!(4, val.map(len_strategy).unwrap());
  5. let first_byte_strategy = |s: &str| s.bytes().next().unwrap();
  6. assert_eq!(82, val.map(first_byte_strategy).unwrap());
  7. }

See also