|
| 1 | +# Command |
| 2 | + |
| 3 | +## Description |
| 4 | + |
| 5 | +The basic idea of the Command pattern is to separate out actions into its own |
| 6 | +objects and pass them as parameters. |
| 7 | + |
| 8 | +## Motivation |
| 9 | + |
| 10 | +Suppose we have a sequence of actions or transactions encapsulated as objects. |
| 11 | +We want these actions or commands to be executed or invoked in some order later |
| 12 | +at different time. These commands may also be triggered as a result of some event. |
| 13 | +For example, when a user pushes a button, or on arrival of a data packet. |
| 14 | +In addition, these commands might be undoable. This may come in useful for |
| 15 | +operations of an editor. We might want to store logs of executed commands so that |
| 16 | +we could reapply the changes later if the system crashes. |
| 17 | + |
| 18 | +## Example |
| 19 | + |
| 20 | +Define two database operations `create table` and `add field`. Each of these |
| 21 | +operations is a command which knows how to undo the command, e.g., `drop table` |
| 22 | +and `remove field`. When a user invokes a database migration operation then each |
| 23 | +command is executed in the defined order, and when the user invokes the rollback |
| 24 | +operation then the whole set of commands is invoked in reverse order. |
| 25 | + |
| 26 | +## Approach: Using trait objects |
| 27 | + |
| 28 | +We define a common trait which encapsulates our command with two operations |
| 29 | +`execute` and `rollback`. All command `structs` must implement this trait. |
| 30 | + |
| 31 | +```rust |
| 32 | +pub trait Migration { |
| 33 | + fn execute(&self) -> &str; |
| 34 | + fn rollback(&self) -> &str; |
| 35 | +} |
| 36 | + |
| 37 | +pub struct CreateTable; |
| 38 | +impl Migration for CreateTable { |
| 39 | + fn execute(&self) -> &str { |
| 40 | + "create table" |
| 41 | + } |
| 42 | + fn rollback(&self) -> &str { |
| 43 | + "drop table" |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +pub struct AddField; |
| 48 | +impl Migration for AddField { |
| 49 | + fn execute(&self) -> &str { |
| 50 | + "add field" |
| 51 | + } |
| 52 | + fn rollback(&self) -> &str { |
| 53 | + "remove field" |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +struct Schema { |
| 58 | + commands: Vec<Box<dyn Migration>>, |
| 59 | +} |
| 60 | + |
| 61 | +impl Schema { |
| 62 | + fn new() -> Self { |
| 63 | + Self { commands: vec![] } |
| 64 | + } |
| 65 | + |
| 66 | + fn add_migration(&mut self, cmd: Box<dyn Migration>) { |
| 67 | + self.commands.push(cmd); |
| 68 | + } |
| 69 | + |
| 70 | + fn execute(&self) -> Vec<&str> { |
| 71 | + self.commands.iter().map(|cmd| cmd.execute()).collect() |
| 72 | + } |
| 73 | + fn rollback(&self) -> Vec<&str> { |
| 74 | + self.commands |
| 75 | + .iter() |
| 76 | + .rev() // reverse iterator's direction |
| 77 | + .map(|cmd| cmd.rollback()) |
| 78 | + .collect() |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +fn main() { |
| 83 | + let mut schema = Schema::new(); |
| 84 | + |
| 85 | + let cmd = Box::new(CreateTable); |
| 86 | + schema.add_migration(cmd); |
| 87 | + let cmd = Box::new(AddField); |
| 88 | + schema.add_migration(cmd); |
| 89 | + |
| 90 | + assert_eq!(vec!["create table", "add field"], schema.execute()); |
| 91 | + assert_eq!(vec!["remove field", "drop table"], schema.rollback()); |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +## Approach: Using function pointers |
| 96 | + |
| 97 | +We could follow another approach by creating each individual command as |
| 98 | +a different function and store function pointers to invoke these functions later |
| 99 | +at a different time. Since function pointers implement all three traits `Fn`, |
| 100 | +`FnMut`, and `FnOnce` we could as well pass and store closures instead of |
| 101 | +function pointers. |
| 102 | + |
| 103 | +```rust |
| 104 | +type FnPtr = fn() -> String; |
| 105 | +struct Command { |
| 106 | + execute: FnPtr, |
| 107 | + rollback: FnPtr, |
| 108 | +} |
| 109 | + |
| 110 | +struct Schema { |
| 111 | + commands: Vec<Command>, |
| 112 | +} |
| 113 | + |
| 114 | +impl Schema { |
| 115 | + fn new() -> Self { |
| 116 | + Self { commands: vec![] } |
| 117 | + } |
| 118 | + fn add_migration(&mut self, execute: FnPtr, rollback: FnPtr) { |
| 119 | + self.commands.push(Command { execute, rollback }); |
| 120 | + } |
| 121 | + fn execute(&self) -> Vec<String> { |
| 122 | + self.commands.iter().map(|cmd| (cmd.execute)()).collect() |
| 123 | + } |
| 124 | + fn rollback(&self) -> Vec<String> { |
| 125 | + self.commands |
| 126 | + .iter() |
| 127 | + .rev() |
| 128 | + .map(|cmd| (cmd.rollback)()) |
| 129 | + .collect() |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +fn add_field() -> String { |
| 134 | + "add field".to_string() |
| 135 | +} |
| 136 | + |
| 137 | +fn remove_field() -> String { |
| 138 | + "remove field".to_string() |
| 139 | +} |
| 140 | + |
| 141 | +fn main() { |
| 142 | + let mut schema = Schema::new(); |
| 143 | + schema.add_migration(|| "create table".to_string(), || "drop table".to_string()); |
| 144 | + schema.add_migration(add_field, remove_field); |
| 145 | + assert_eq!(vec!["create table", "add field"], schema.execute()); |
| 146 | + assert_eq!(vec!["remove field", "drop table"], schema.rollback()); |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +## Approach: Using `Fn` trait objects |
| 151 | + |
| 152 | +Finally, instead of defining a common command trait we could store |
| 153 | +each command implementing the `Fn` trait separately in vectors. |
| 154 | + |
| 155 | +```rust |
| 156 | +type Migration<'a> = Box<dyn Fn() -> &'a str>; |
| 157 | + |
| 158 | +struct Schema<'a> { |
| 159 | + executes: Vec<Migration<'a>>, |
| 160 | + rollbacks: Vec<Migration<'a>>, |
| 161 | +} |
| 162 | + |
| 163 | +impl<'a> Schema<'a> { |
| 164 | + fn new() -> Self { |
| 165 | + Self { |
| 166 | + executes: vec![], |
| 167 | + rollbacks: vec![], |
| 168 | + } |
| 169 | + } |
| 170 | + fn add_migration<E, R>(&mut self, execute: E, rollback: R) |
| 171 | + where |
| 172 | + E: Fn() -> &'a str + 'static, |
| 173 | + R: Fn() -> &'a str + 'static, |
| 174 | + { |
| 175 | + self.executes.push(Box::new(execute)); |
| 176 | + self.rollbacks.push(Box::new(rollback)); |
| 177 | + } |
| 178 | + fn execute(&self) -> Vec<&str> { |
| 179 | + self.executes.iter().map(|cmd| cmd()).collect() |
| 180 | + } |
| 181 | + fn rollback(&self) -> Vec<&str> { |
| 182 | + self.rollbacks.iter().rev().map(|cmd| cmd()).collect() |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +fn add_field() -> &'static str { |
| 187 | + "add field" |
| 188 | +} |
| 189 | + |
| 190 | +fn remove_field() -> &'static str { |
| 191 | + "remove field" |
| 192 | +} |
| 193 | + |
| 194 | +fn main() { |
| 195 | + let mut schema = Schema::new(); |
| 196 | + schema.add_migration(|| "create table", || "drop table"); |
| 197 | + schema.add_migration(add_field, remove_field); |
| 198 | + assert_eq!(vec!["create table", "add field"], schema.execute()); |
| 199 | + assert_eq!(vec!["remove field", "drop table"], schema.rollback()); |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +## Discussion |
| 204 | + |
| 205 | +If our commands are small and may be defined as functions or passed as a closure |
| 206 | +then using function pointers might be preferable since it does not exploit |
| 207 | +dynamic dispatch. But if our command is a whole struct with a bunch of functions |
| 208 | +and variables defined as seperate module then using trait objects would be |
| 209 | +more suitable. A case of application can be found in [`actix`](https://actix.rs/), |
| 210 | +which uses trait objects when it registers a handler function for routes. |
| 211 | +In case of using `Fn` trait objects we can create and use commands in the same |
| 212 | +way as we used in case of function pointers. |
| 213 | + |
| 214 | +As performance, there is always a trade-off between performance and code |
| 215 | +simplicity and organisation. Static dispatch gives faster performance, while |
| 216 | +dynamic dispatch provides flexibility when we structure our application. |
| 217 | + |
| 218 | +## See also |
| 219 | + |
| 220 | +- [Command pattern](https://en.wikipedia.org/wiki/Command_pattern) |
| 221 | + |
| 222 | +- [Another example for the `command` pattern](https://web.archive.org/web/20210223131236/https://chercher.tech/rust/command-design-pattern-rust) |
0 commit comments