Aka. Token
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存其状态。这样以后就可以将该对象恢复到原先保存的状态。
“Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.” – GoF
动机
有时候需要记录一个对象的内部状态。比如要实现checkpoint或者undo这样的机制,可以让使用者从临时性的操作跳出来或者需要修复错误的时候,你必须将状态信息保存在某个地方,以便在进行某些操作后,将对象恢复到原来的状态。但通常情况下,对象封装了状态(即私有成员变量),因此其他的对象无法访问这些状态,而且也不可能将这些状态保存在对象之外。如果将这些状态设置成公有的,又会违反面向对象封装性的原则,同时也会削弱应用的可靠性和可扩展性。
在软件构建过程中, 某些对象的状态在转换过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个时刻的状态。如果使用一些共有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。Memento设计模式就可以实现对象状态的良好保存与恢复,但同时又不会因此而破坏对象本身的封装性。
UML类图:
角色
- Memento
1. 保存Originator对象的内部状态。
2. 除Originator外,其他对象均不能访问Memento对象。
- Originator
1. 创建一个包含其当前内部状态快照的Memento对象。
2. 使用Memento对象来恢复其内部状态。
- Caretaker
1. Memento对象的容器。在C++中一般用stack来实现。
2. 从不对Memento对象的内容进行检查或操作。
示例代码:
// Memento.h
#include <iostream>
#include <stack>
using namespace std;
// CMemento类,用来保存CRectangle的状态
class CMemento
{
private: // 所有的成员变量和成员函数都是私有的
int topx; // 因此除友元类CRectangle外,其他对象都无法访问
int topy;
int width;
int height;
private:
CMemento()
{
}
//private: // 1. 如果编写了显式拷贝构造函数,那么,它必须是公有的,
// CMemento(const CMemento& memo) // 否则CMementoStack将无法调用该拷贝构造函数。
// { // 2. 如果没有显式的拷贝构造函数,那么缺省的拷贝构造函数总是公有的。
// topx = memo.topx; // 3. 在Memento模式中,如果仅考虑保存一次状态,则
// topy = memo.topy; // CMementoStack是不必要的,那么拷贝构造函数,可以
// width = memo.width; // 显式地声明为private的,尽管在CRectangle的create_memento
// height = memo.height; // 成员函数中也会调用CMemento的拷贝构造函数,但CRectangle
// } // 是CMemento的友元类,因此不存在这方面的限制。
private:
void set_state(int topx, int topy, int width, int height) // 保存CRectangle的状态
{
this->topx = topx;
this->topy = topy;
this->width = width;
this->height = height;
}
friend class CRectangle; // 友元类CRectangle,可以访问CMemento中的所有内容
};
// CRectangle类。一个矩形,需要保存状态改变的类
class CRectangle
{
private:
int topx; // 矩形左上角的x坐标
int topy; // 矩形左上角的y坐标
int width; // 矩形的宽
int height; // 矩形的高
public:
CRectangle(int topx, int topy, int width, int height):topx(topx), topy(topy), width(width), height(height)
{
}
// 模拟移动矩形的位置到指定的点,即改变了矩形的状态
void move_to(int topx, int topy)
{
this->topx = topx;
this->topy = topy;
}
// 模拟改变矩形的长和宽,即改变了矩形的状态
void change_width_height(int width, int height)
{
this->width = width;
this->height = height;
}
// 将矩形恢复到memo中所保存的状态
void set_memento(CMemento memo)
{
this->topx = memo.topx;
this->topy = memo.topy;
this->width = memo.width;
this->height = memo.height;
}
// 将矩形的状态保存到一个CMemento对象
CMemento create_memento()
{
CMemento cm;
cm.set_state(this->topx, this->topy, this->width, this->height);
return cm;
}
// 输出矩形的状态信息
void print_info()
{
cout << "Top left point's x coordinate: " << topx << endl;
cout << "Top left point's y coordinate: " << topy << endl;
cout << "The width is: " << width << endl;
cout << "The height is: " << height << endl;
}
};
// CMemento对象的容器,可以用来保存多个CMemento对象,通常用stack来实现
class CMementoStack
{
private:
stack<CMemento> stk;
public:
void add_memento(CMemento memo)
{
stk.push(memo); //将CMemento对象压入栈中
}
CMemento get_memento()
{
CMemento cm = stk.top(); // 取得CMemento对象。这个过程会用到CMemento类的拷贝构造函数,
// 由于CMemento对象中的成员变量均是普通类型(非指针、非类对象),
// 因此使用默认的拷贝构造函数即可
stk.pop(); // 删除已经取得的CMemento对象
return cm;
}
};
// Memento.cpp
#include "Memento.h"
int main(int argc, char **argv)
{
CRectangle cr(10, 10, 100, 100);
CMementoStack cs;
cout << "Initial states: " << endl;
cr.print_info();
CMemento cm0 = cr.create_memento(); // 将状态保存到CMemento对象
cs.add_memento(cm0); // 将CMemento对象压栈
// 第一次改变状态
cr.change_width_height(200, 200); // 改变矩形的高度和宽度
cr.move_to(20, 20); // 改变矩形的位置
cout << "\nAfter 1st states changed: " << endl;
cr.print_info();
CMemento cm1 = cr.create_memento(); // 将状态保存到CMemento对象
cs.add_memento(cm1); // 将CMemento对象压栈
// 第二次改变状态
cr.change_width_height(300, 300); // 改变矩形的高度和宽度
cr.move_to(30, 30); // 改变矩形的位置
cout << "\nAfter 2nd states changed: " << endl;
cr.print_info();
// ... 这里不再压栈
// 恢复到第一次状态的改变
cr.set_memento(cs.get_memento());
cout << "\nStates restored to 1st change: " << endl;
cr.print_info();
// 恢复到初始状态
cr.set_memento(cs.get_memento());
cout << "\nStates restored to initial: " << endl;
cr.print_info();
}
运行结果:
Initial states:
Top left point's x coordinate: 10
Top left point's y coordinate: 10
The width is: 100
The height is: 100
After 1st states changed:
Top left point's x coordinate: 20
Top left point's y coordinate: 20
The width is: 200
The height is: 200
After 2nd states changed:
Top left point's x coordinate: 30
Top left point's y coordinate: 30
The width is: 300
The height is: 300
States restored to 1st change:
Top left point's x coordinate: 20
Top left point's y coordinate: 20
The width is: 200
The height is: 200
States restored to initial:
Top left point's x coordinate: 10
Top left point's y coordinate: 10
The width is: 100
The height is: 100
结果符合预期。
补充说明:
1. 未设置其成员变量为public的前提下,在对象的外部保存一个其状态,颇需技巧,而且用不同语言来实现的时候也有所不同。
a. 对于C++,通常使用友元类来实现。
b. 对于C#,使用internal关键字。
c. 对于Java,使用package protected访问控制。
与其处于相同包中的子类,和处于相同包中的其它类均可以访问pakcage protected的对象或变量。
Java中的访问权限有public,private,protected和默认的包访问权限,如果类中的属性方法没有显示的指明访问权
限,则具有包访问权限,很多人也称它为friendly访问权限,也有人称为packeged权限,而packaged和friendly
这两个关键字在实际中都是不存在的。在Java中,访问权限修饰符权限从高到低是public,protected,package
protected,private。
d. C++, C#和Java均可使用内部类的方式来实现类似的功能,不过对于“将状态存储于对象之外”而言,稍嫌勉强。
2. 关于拷贝构造函数,请看:http://patmusing.blog.163.com/blog/static/1358349602009113061024796/
3. 关于友元类,请看:http://patmusing.blog.163.com/blog/static/1358349602010182331153/