Delphi备忘录模式(memento模式)
作者 陈省
用完请把物品摆放回原位
小的时候,我脑袋不太好使,CPU只支持单线程,不能同时想两件事,一想多了就会混乱。比如家长给我两毛钱,让我打酱油,走在路上看见小猫小狗打架,看得高兴,就忘了打酱油的事,结果商店关门了,才想起来。拿了剪刀,剪完东西就会忘了放回去,于是每天总要有30分钟是在找东西,后来家里人每次见我拿什么东西,都要提醒一句用完别忘了把东西放回原位。
长大以后,历经千辛万苦,我终于学会写字了,知道该用笔记本把事情记下来,以免忘记,也就是从那时起,我学会了备忘录模式。
按照大师的说法,备忘录模式是”在不损害封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样日后就可以将该对象恢复到原来的状态了”。我用笔记本记录事情,实际上就是将我的大脑中的某些重要的信息(注意,不一定是全部的状态,如果把脑袋里面一些少儿不宜的东西都记录下来的话,后果一定是被暴扁一顿J),记录在笔记本上,这样我老了的时候写回忆录时就可以用了。下图是一个记笔记的的备忘录模式UML图:

当要记录事情时,调用获得备忘录的方法会返回一个备忘录接口,而将一个已经保存的备忘录接口作为参数调用我的大脑的恢复记忆方法可以恢复我的记忆。图中笔记本是用来管理多个备忘录的。
要注意的是,上面的备忘录模式有一些安全上的问题,我的大脑和备忘录之间显然是一个宽接口,也就是说备忘录可以知道我大脑里想的任何事情,可是笔记本同记忆之间也是宽接口,也就是说笔记本上可以通过备忘录知道大脑中所想的事情,可能包括我的邮箱密码,包括我的银行存折密码,这样的话笔记本是非常危险的,如果让外人获得了我的笔记本,就可能偷走我的钱。因此设计模式一书中建议我们在备忘录和笔记本之间的接口应该是个窄接口,为了实现窄接口,可以考虑将备忘录加密后记录在电脑光盘中,这样外人就无法知道内容了。
VCL中的备忘录模式
在平时写程序时,经常会用到画布对象,比如做自绘画的组件,需要在组件的画布上画一些几何图案或者文字,来模拟按钮,工具条的形状等等。为了实现丰富的绘画效果来,就需要变换画布的字体,画笔的颜色或者画刷的填充图案,来画出不同样式图案,并将其组合来产生很好的视觉效果。
比如我想先用字号为五号的宋体字写一句话,然后再用黑体的二号字写一句话,然后回头再用五号的宋体再写一句话,通常的实现方法是:
procedure TForm1.btn1Click(Sender: TObject);
begin
with Self.Canvas do
begin
//先用五号宋体写一句话
Font.Name:='宋体';
Font.Size:=11;
textout(100,100, '宋体五号');
//再用二号黑体写一句话
Font.Name:='黑体';
Font.Size:=22;
textout(100,150, '黑体二号');
//再用五号宋体写一句话
Font.Name:='宋体';
Font.Size:=11;
textout(100,200, '宋体五号');
end;
end;
但是上面代码中有一个问题就是,第一次画的时候,我用的是宋体五号,而最后一次画的时候也是用的宋体五号字,但是由于中间用了黑体来画,所以最后一次画的时候,我又给字体重新赋值一次,也就是字体状态的恢复是依靠hardcoding(硬编码来实现的),如果我后来又改了一次代码,将第一次的字体改成了魏碑,但是我却忘了将后面的字体也改成魏碑,就会造成混乱。
幸运的是,从Delphi6开始,VCL中增加了三个memento类,TPenRecall,TBrushRecall和TFontRecall,这三个类可以将字体或者画笔以及画刷的某个状态保存起来,最后可以进行恢复。TRecall类的用法非常简单,将上面的代码改造一下:
procedure TForm1.btn2Click(Sender: TObject);
var
FR: TFontRecall;
begin
with Self.Canvas do
begin
//先用五号宋体写一句话
Font.Name := '宋体';
Font.Size := 11;
textout(100, 100, '宋体五号');
FR := TFontRecall.Create(Font);
try
//再用二号黑体写一句话
Font.Name := '黑体';
Font.Size := 22;
textout(100, 150, '黑体二号');
finally
FR.Free;
end;
//再用五号宋体写一句话
textout(100, 200, '宋体五号');
end;
end;
在上面的代码中,将画布的Font字体作为参数传给TFontRecall的构造函数,创建一个当前字体的备忘录,当使用黑体画完字后需要恢复宋体时,只要将FR释放掉就可以恢复成备忘录保存时的状态了。
来看一下TFontRecall是如何实现备忘录功能的:
constructor TFontRecall.Create(AFont: TFont);
begin
inherited Create(TFont.Create, AFont);
end;
constructor TRecall.Create(AStorage, AReference: TPersistent);
begin
inherited Create;
FStorage := AStorage;
FReference := AReference;
Store;
end;
在TFontRecall构造函数中,将要备忘的字体作为参数传,以及新建的一个TFont类的实例作为参数调用基类TRecall的构造函数,在TRecall的构造函数中,将字体原件和复印件的引用保存起来,然后调用Store方法来进行原件的复制。
procedure TRecall.Store;
begin
if Assigned(FReference) then
FStorage.Assign(FReference);
end;
原件的复制很简单,就是用到了我们前面讲到的原型模式,调用复印件的Assign方法将字体的属性都克隆到复印件中。当需要通过备忘录恢复字体的状态时,当销毁TFontRecall会调用Destroy方法:
destructor TRecall.Destroy;
begin
if Assigned(FReference) then
FReference.Assign(FStorage);
Forget;
inherited;
end;
在Destroy方法中,先将复印件的内容克隆回原件以恢复原件的状态,
procedure TRecall.Forget;
begin
FReference := nil;
FreeAndNil(FStorage);
end;
然后调用Forget方法销毁复印件。由此可见memento模式经常会和原型模式配合起来使用来完成原件的状态的保存。要说明的一点是,这里TFontRecall直接对原件操作,也就是省略了标准模型中的备忘录接口,是一个宽接口,这样做的好处是比较简单,缺点是不安全,后面状态的恢复是对TFont的属性进行直接的操作。
自身备忘录模式
上面提到了TFontRecall是一种不安全的备忘录模式,那么解决安全性的问题,将某些信息封装起来,备忘录模式也可以通过将要备忘的状态保存在内部来实现,无须外部类如TFontRecall来介入。
其中一个例子就是Open Tools Api中的IOTAEditPosition接口,这个接口用以用来操作IDE中的编辑器的光标位置的,在IDE中增加删除一些文字后会改变光标的位置,但是有时又需要在改变编辑器内容后,恢复光标的原来位置,为此IOTAEditPosition提供了Save方法来备忘当前光标的位置,然后进行修改内容的操作,最后在操作完毕后,调用Restore方法来恢复光标位置,下面的代码是在Emacs键盘快捷方式模拟专家的插入文件的代码,代码位于demos\Toolsapi\editor keybinding\emacs\emacsbufferlist.pas文件中:
procedure TEmacsBinding.InsertFile(const Context: IOTAKeyContext;
KeyCode: TShortCut; var BindingResult: TKeyBindingResult);
var
EP: IOTAEditPosition;
begin
EP := Context.EditBuffer.EditPosition;
EP.Save;
try
EP.InsertFile('');
finally
EP.Restore;
end;
BindingResult := krHandled;
end;
上面的例子中,IOTAEditPosistion接口直接调用自身的方法将状态保存在内部,这样的好处就是安全,不将IOTAEditPosition的内部信息暴露给外部对象。