Skip to content

Commit fabafb1

Browse files
authored
Adding Command pattern (#247)
1 parent c9c5f70 commit fabafb1

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
- [Design Patterns](./patterns/index.md)
2424
- [Behavioural](./patterns/behavioural/intro.md)
25+
- [Command](./patterns/behavioural/command.md)
2526
- [Interpreter](./patterns/behavioural/interpreter.md)
2627
- [Newtype](./patterns/behavioural/newtype.md)
2728
- [RAII Guards](./patterns/behavioural/RAII.md)

patterns/behavioural/command.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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

Comments
 (0)