考虑一个简单的赋值语句, 例如, 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做进一步的细分. 例如, CallableUndoable.

这里我们简单地将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的转账活动可以用两个命令来模拟:

  1. 从A账户取出XX$
  2. 项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
{

}
}