备忘录模式
首先看一个例子来认识备忘录模式。
银行账户
下面是一个银行账户的类, 现在我们决定只使用deposit()函数创建一个银行账户。在前面的例子中,它是void的,而现在,deposit()将返回一个Memento。然后Memento可以回滚帐户到以前的状态:
class BankAccount{int balance = 0;public:explicit BankAccount(const int balance): balance(balance) {}Memento deposit(int amount){balance += amount;return { balance };}void restore(const Memento& m){balance = m.balance;}};
至于Memento本身,我们可以采用一个简单的实现方法:
class Memento{int balance;public:Memento(int balance): balance(balance){}friend class BankAccount;};
这里有两件事需要特别注意:
Memento类是不可变的。想象一下,如果我们可以更改余额, 那么我们可以将帐户回滚到它从未处于的状态。
备忘录将BankAccount声明为一个友元类。这允许帐户实际使用balance字段。另一个同样有效的替代方法是将Memento作为BankAccount的内部类。
下面是备忘录模式的具体使用:
void memento(){BankAccount ba{ 100 };auto m1 = ba.deposit(50);auto m2 = ba.deposit(25);cout << ba << "\n"; // Balance: 175// undo to m1ba.restore(m1);cout << ba << "\n"; // Balance: 150// redoba.restore(m2);cout << ba << "\n"; // Balance: 175}
这个实现已经足够好了,只是缺少了一些东西。例如,您永远不会得到表示开盘余额的Memento,因为构造函数不能返回值。
撤销与回滚
如果我们要存储每一个由银行帐户产生的Memento,该怎么办呢? 我们将引入一个新的银行账户类BankAccount2,它将保存它生成的每一个Memento:
class BankAccount2 // supports undo/redo{int balance = 0;vector<shared_ptr<Memento>> changes;int current;public:explicit BankAccount2(const int balance) : balance(balance){changes.emplace_back(make_shared<Memento>(balance));current = 0;}
我们现在已经解决了返回初始余额的问题:初始变化的Memento也被存储。当然,这个Memento实际上并没有返回,所以为了回滚到它,我想您可以实现一些reset()函数——这完全取决于您。
我们使用shared_ptr来存储memento,也使用shared_ptr来返回它们。此外,我们使用当前字段作为进入更改列表的“指针”,如果我们决定撤消并后退一步,我们总是可以redo和恢复到我们之前的状态。
下面我们来看一下deposit()的实现:
shared_ptr<Memento> deposit(int amount){balance += amount;auto m = make_shared<Memento>(balance);changes.push_back(m);++current;return m;}
我们添加了一个方法, 该方法基于一个Memento来恢复帐户状态:
void restore(const shared_ptr<Memento>& m){if (m){balance = m->balance;changes.push_back(m);current = changes.size() - 1;}}
恢复的过程与我们之前看到的明显不同。首先,我们实际上检查shared_ptr是否已初始化。此外,当我们恢复一个memento时,我们实际上是将该memento推入更改列表,以使得一个undo操作能够正确地执行。
下面是undo()的实现:
shared_ptr<Memento> undo(){if (current > 0){--current;auto m = changes[current];balance = m->balance;return m;}return{};}
只有当current大于零时,我们才能使用undo()。此时,我们将指针后移并获取相应的changes成员,然后返回对应的balance。如果不能回滚到以前的memento,则返回一个默认构造的shared_ptr。
redo()的实现如下:
shared_ptr<Memento> redo(){if (current + 1 < changes.size()){++current;auto m = changes[current];balance = m->balance;return m;}return{};}
同样,我们需要能够执行一些redo:如果可以,我们可以安全地redo,如果不行,我们什么都不做,并返回一个空指针。把它们结合起来,我们现在可以开始使用undo/redo功能了:
BankAccount2 ba{ 100 };ba.deposit(50);ba.deposit(25); // 125cout << ba << "\n";ba.undo();cout << "Undo 1: " << ba << "\n"; // Undo 1: 150ba.undo();cout << "Undo 2: " << ba << "\n"; // Undo 2: 100ba.redo();cout << "Redo 2: " << ba << "\n"; // Redo 2: 150ba.undo(); // back to 100 again
