命令模式

说明

命令模式的基本概念是,将动作分离为单独的对象,并且作为参数传递它们

出发点

假设我们有一连串的动作或事务被封装为对象。 我们希望这些动作或命令在以后的不同时间以某种顺序执行或调用, 这些命令也可以作为某些事件的结果被触发。例如,当用户按下某个按钮,或某个数据包到达时。 此外,这些命令应该可以撤销。这对于编辑器的操作可能很有用。我们可能想存储命令日志, 这样,如果系统崩溃,我们可以在之后重新应用这些修改。

示例

定义两个数据库操作,建表加字段。每个操作都是一个命令,它知道如何撤销命令。例如,删表删字段。当用户调用数据库迁移操作时,每条命令都会按照定义的顺序执行。而当用户调用回滚操作时,整个命令集会以相反的顺序调用。

使用trait对象

我们定义了一个trait,将我们的命令封装成两个操作,executerollback。所有命令结构体必须实现这个trait。

  1. pub trait Migration {
  2. fn execute(&self) -> &str;
  3. fn rollback(&self) -> &str;
  4. }
  5. pub struct CreateTable;
  6. impl Migration for CreateTable {
  7. fn execute(&self) -> &str {
  8. "create table"
  9. }
  10. fn rollback(&self) -> &str {
  11. "drop table"
  12. }
  13. }
  14. pub struct AddField;
  15. impl Migration for AddField {
  16. fn execute(&self) -> &str {
  17. "add field"
  18. }
  19. fn rollback(&self) -> &str {
  20. "remove field"
  21. }
  22. }
  23. struct Schema {
  24. commands: Vec<Box<dyn Migration>>,
  25. }
  26. impl Schema {
  27. fn new() -> Self {
  28. Self { commands: vec![] }
  29. }
  30. fn add_migration(&mut self, cmd: Box<dyn Migration>) {
  31. self.commands.push(cmd);
  32. }
  33. fn execute(&self) -> Vec<&str> {
  34. self.commands.iter().map(|cmd| cmd.execute()).collect()
  35. }
  36. fn rollback(&self) -> Vec<&str> {
  37. self.commands
  38. .iter()
  39. .rev() // reverse iterator's direction
  40. .map(|cmd| cmd.rollback())
  41. .collect()
  42. }
  43. }
  44. fn main() {
  45. let mut schema = Schema::new();
  46. let cmd = Box::new(CreateTable);
  47. schema.add_migration(cmd);
  48. let cmd = Box::new(AddField);
  49. schema.add_migration(cmd);
  50. assert_eq!(vec!["create table", "add field"], schema.execute());
  51. assert_eq!(vec!["remove field", "drop table"], schema.rollback());
  52. }

使用函数指针

我们可以采用另一种方法。将每个单独的命令创建为不同的函数,并存储函数指针, 以便以后在不同的时间调用这些函数。因为函数指针实现了FnFnMutFnOnce这三个特性,我们也可以传递和存储闭包。

  1. type FnPtr = fn() -> String;
  2. struct Command {
  3. execute: FnPtr,
  4. rollback: FnPtr,
  5. }
  6. struct Schema {
  7. commands: Vec<Command>,
  8. }
  9. impl Schema {
  10. fn new() -> Self {
  11. Self { commands: vec![] }
  12. }
  13. fn add_migration(&mut self, execute: FnPtr, rollback: FnPtr) {
  14. self.commands.push(Command { execute, rollback });
  15. }
  16. fn execute(&self) -> Vec<String> {
  17. self.commands.iter().map(|cmd| (cmd.execute)()).collect()
  18. }
  19. fn rollback(&self) -> Vec<String> {
  20. self.commands
  21. .iter()
  22. .rev()
  23. .map(|cmd| (cmd.rollback)())
  24. .collect()
  25. }
  26. }
  27. fn add_field() -> String {
  28. "add field".to_string()
  29. }
  30. fn remove_field() -> String {
  31. "remove field".to_string()
  32. }
  33. fn main() {
  34. let mut schema = Schema::new();
  35. schema.add_migration(|| "create table".to_string(), || "drop table".to_string());
  36. schema.add_migration(add_field, remove_field);
  37. assert_eq!(vec!["create table", "add field"], schema.execute());
  38. assert_eq!(vec!["remove field", "drop table"], schema.rollback());
  39. }

使用 Fn trait对象

最后,我们可以在vector中分别存储实现的每个命令,而不是定义一个命令trait。

  1. type Migration<'a> = Box<dyn Fn() -> &'a str>;
  2. struct Schema<'a> {
  3. executes: Vec<Migration<'a>>,
  4. rollbacks: Vec<Migration<'a>>,
  5. }
  6. impl<'a> Schema<'a> {
  7. fn new() -> Self {
  8. Self {
  9. executes: vec![],
  10. rollbacks: vec![],
  11. }
  12. }
  13. fn add_migration<E, R>(&mut self, execute: E, rollback: R)
  14. where
  15. E: Fn() -> &'a str + 'static,
  16. R: Fn() -> &'a str + 'static,
  17. {
  18. self.executes.push(Box::new(execute));
  19. self.rollbacks.push(Box::new(rollback));
  20. }
  21. fn execute(&self) -> Vec<&str> {
  22. self.executes.iter().map(|cmd| cmd()).collect()
  23. }
  24. fn rollback(&self) -> Vec<&str> {
  25. self.rollbacks.iter().rev().map(|cmd| cmd()).collect()
  26. }
  27. }
  28. fn add_field() -> &'static str {
  29. "add field"
  30. }
  31. fn remove_field() -> &'static str {
  32. "remove field"
  33. }
  34. fn main() {
  35. let mut schema = Schema::new();
  36. schema.add_migration(|| "create table", || "drop table");
  37. schema.add_migration(add_field, remove_field);
  38. assert_eq!(vec!["create table", "add field"], schema.execute());
  39. assert_eq!(vec!["remove field", "drop table"], schema.rollback());
  40. }

讨论

如果我们的命令很小,可以定义成函数,或作为闭包传递,那么使用函数指针可能更好, 因为它不需要动态分发。但如果我们的命令是个完整的结构, 有一堆函数和变量被分别定义为独立的模块,那么使用trait对象会更合适。 有个应用实例是actix, 它在为例程注册handler函数时使用了trait对象。在使用Fn trait对象时, 我们可以用和函数指针相同的方式创建和使用命令。

说到性能,在性能和代码的简易性、组织性间我们总需要权衡。 静态分发可以提供更好的性能,而动态分发在我们组织应用程序时提供了灵活性。

参见