考虑一个简单的赋值语句, 例如, meaning_of_life = 42
. 这个变量被赋值了, 但是却没有办法来记录何时, 何地发生的赋值, 我们也无法得知它以前的值是什么. 这样可能是有问题的, 如果没有变更记录, 我们就无法回滚到以前的值, 无法完成审计, 或基于历史的调试.
而命令模式的建议是不要直接使用对象的API来操作它们, 而是发过去一个命令对象, 告诉它该如何做. 一个Command
就是一个数据类, 它的成员函数描述了要做什么和如何做.
14.1 场景
我们考虑对银行账户进行建模. 账户又一个余额属性(balance)和一个透支限额(overdraft limit)属性. 我们实现两个方法: deposit()
和withdraw()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct BankAccount { int balance = 0; int overfraft_limit = -500; void deposit(int amount) { balance += amount; cout << "deposited " << amount << ", banlance is now" << balance << endl; }
void withdraw(int amount) { if( balance-amount >= overdraft_limit) { balance -= amount; cout << "withdraw" << amount << ", balance is now" << balance << endl; } } };
|
现在, 假设我们需要记录每一笔存取款的操作, 而我们又不能直接对BandAccount
代码做修改. 该怎么办?
14.2 实现命令模式
首先定义一个命令接口:
1 2 3 4
| struct Command { virtual void call() const = 0; };
|
然后定义BandAccountCommand
, 它封装银行账户的信息:
1 2 3 4 5 6 7 8 9
| struct BandAccountCommand : public Command { BandAccount& account; enum Action{ deposit, withdraw} action; int amount;
BandAccountCommand(BandAccount& account, const Action action, const int amount) : account(account), action(action), amount(amount) {} };
|
然后实现call())
方法:
1 2 3 4 5 6 7 8 9 10 11 12
| void call() const override { switch (action) { case deposit: account.deposit(amount); break; case withdraw: account.withdraw(amount); break; } }
|
我们就可以创建一个命令对象然后利用它来修改账户:
1 2 3
| BandAccount ba; Command cmd{ba, BankAccountCommand::deposit, 100}; cmd.call();
|
14.3 Undo Command
由于命令对象中封装了操作的信息, 我们很容易实现账户的回滚操作.
在继续之前, 我们需要决定是否要将undo操作集成到我们的Command
接口中. 在这里我为了简单而将它放进去了. 但是在实际使用中, 这通常是一个需要做的设计决策, 你需要考虑是否应该遵守在第一章中提到的接口隔离原则. 例如, 如果有些命令是最终命令, 无法做回滚操作, 你可能需要将Command
做进一步的细分. 例如, Callable
和Undoable
.
这里我们简单地将undo()
加到Command
中:
1 2 3 4 5
| struct Command { virtual void call() = 0; virtual void undo() = 0; };
|
下面的实现是错误的, 它错误地假设了用户的undo操作的金额和对应的操作是相同的. 例如, 如果试图取很大的金额, 会失败, 然后回滚, 你又不知道上一个操作是失败的…
1 2 3 4 5 6 7 8 9 10 11 12
| void undo() override { switch (action) { case widthdraw: account.deposit(amount); break; case deposit: account.withdraw(amount); break; } }
|
为了解决这个问题, 将withdraw()
改为返回一个成功标记:
1 2 3 4 5 6 7 8 9
| bool withdraw(int amount) { if( balnace-amount>=overdraft_limit){ balance -= amount; cout << "withdraw " << amount << ", balance now " << balance << endl; return true; } return false; }
|
然后我们要修改BankAccountCommand
类来做两件事:
- 当取款成功时, 在内部进路一个成功标记
- 当
undo()
时使用这个标记
注意, 在前面Command
的声明中, 我们将const
限定给去掉了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| struct BandAccountCommand : Command { ... bool withdraw_succeeded; BandAccountCommand(BandAccount& account, const Action action, const int amount) : ..., withdraw_succeeded{false} {}
void call() override { switch (action) { ... case withdraw: withdraw_succeeded = account.withdraw(amount); break; } }
void undo() override { switch(action) { case withdraw: if( withdraw_succeeded) account.deposit(amount); break; ... } } };
|
14.4 Composite Command
从用户A到用户B的转账活动可以用两个命令来模拟:
- 从A账户取出XX$
- 项B账户存入XX$
更好的做法是将这两个命令封装成一条命令. 这是我们在第8章Composite模式
的精髓.
首先我们定义一下组合命令的框架. 我们打算从vector<BandAccountCommand>
派生–这样做可能会有问题, 因为std::vector
没有提供虚拟析构函数. 但是对我们的场景来说这不是问题.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct CompositeBandAccountCommand : vector<BankAccountCommand>, Command { CompositeBandAccountCommand(const initializer_list<value_type>& items) : vector<BankAccountCommand>(items){}
void call() override { for(auto& cmd: *this) cmd.call(); }
void undo() override { for(auto it=rbegin(); it!=rend(); ++it) it->undo(); } }
|
CompositeBandAccountCommand
既是一个vector
也是一个Command
. 增加了一个接受初始化列表的构造函数(这很有用!)并实现了call()
和undo()
方法. 在这里, undo()
逆序方式迭代了所有的命令.
使用它:
1 2 3 4 5 6 7 8
| struct MoneyTransferCommand : CompositeBandAccountCommand { MoneyTransferCommand(BandAccount& from, BandAccount& to, int amount) : CompositeBandAccountCommand { } }
|