备忘录模式其实就是给我们的应用程序一次撤销的机会。使用过word的人肯定会知道伟大的“Ctrl+Z”,用过PS的人更是不会忘记,应该来说基本上所有的带编辑功能的软件毫无例外都提供了撤销的功能,撤销功能给了我们1次或N次返回的机会,准确地说应该是恢复之前状态的机会。我们自己开发的软件有时候有需要撤销的功能,比如在网络通信中,常常会因为不可预知的错误就导致程序出错,这时候,要是能恢复到上一个正确的状态就太好了,这样可以省去不上功夫。我们今天要讨论的备忘录模式就是用来解决这个问题的。
经典的备忘录模式如下图所示:
注:本图来自《设计模式_基于C#的工程化实现及扩展》
首先,还是让我来解释一下这幅图的意思:Originator(原发器)就是我们的业务模型,它本身可能有非常多的字段(或叫做变量)。一个字段的值发生了改变,我们可以认为Originator的状态就发生了变化。Originator里面有一个IMemento类型的字段用来记录状态的变化(它相当于一个记事本,用来备忘的)。IMemento聚合与Caretaker,Caretaker也就是负责人,用来决定什么时候将IMemento(备忘录)对象中的状态还原到Originator(原发器)中。打个形象的比喻,Originator就好比一个图书馆,图书馆里面有很多书,每一本书都有接还状态(后面我们会提到IState用来表示状态),IMemento就想到于图书馆管理员手中的接还本(或称之为备忘录),Caretaker就相当于图书馆管理员,图书馆管理员(Caretaker)保存有一本(或N本)备忘录。当一本书(假设是BookA)借出去的时候,图书馆(Originator)的BookA的状态(IState)就发生了变化,变成借出状态。等到还书的时候,图书馆管理员(Caretaker)再通过备忘录(IMemeto)对象将图书馆(Originator)还原成原始的状态。需要说明的是,这个比喻并不恰当,因为书的借出、归还就两种状态,但是实际应用中,因为Originator中的变量的取值范围的大小,Originator中的某一个变量可以有很多状态,这时候才是备忘录发挥作用的时候。
So,还是让我们再看看用实际的代码来说明一下备忘录模式。
1、备忘录模式里面涉及到的类型有Caretaker(负责人类型)、Originator(原发器类型)、IMemento(备忘录类型)、IState(状态类型),在下面的代码中,我们将不会涉及到实际的Caretaker类型。我们先定义状态类型(IState),我们下面只是单纯地定义一个接口,实际开发过程中可以为IState接口定义一些方法、属性。
/// <summary> /// 状态类型 /// </summary> public interface IState { } /// <summary> /// 备忘录 /// </summary> /// <typeparam name="T">状态类型</typeparam> public interface IMemento<T> where T : IState { /// <summary> /// 备忘录中的状态 /// </summary> T State { get; set; } } /// <summary> /// 原发器 /// </summary> /// <typeparam name="T">状态类型</typeparam> /// <typeparam name="M">备忘录类型</typeparam> public interface IOriginator<T, M> where T : IState where M : IMemento<T>,new() { /// <summary> /// 备忘录 /// </summary> IMemento<T>, Memento { get; set; }//以便后面取出对原发器进行还原 }
2、定义原发器、备忘录抽象基类
/// <summary> /// 备忘录基类 /// </summary> /// <typeparam name="T">状态类型</typeparam> public abstract class MementoBase<T>:IMemento<T> where T:IState { protected T state; public virtual T State { get { return this.state; } set { this.state = value; } } }
/// <summary> /// 原发器基类 /// </summary> /// <typeparam name="T">状态类型</typeparam> /// <typeparam name="M">备忘录类型</typeparam> public abstract class OriginatorBase<T, M> : IOriginator<T, M> where T : IState where M : IMemento<T>,new() { protected T state; public IMemento<T> Memento { get { M m = new M(); m.State = this.state; return m; } set { if (value == null) throw new ArgumentNullException("没有传入IMemento实例化对象"); this.state = value.State; } } }
3、我们在定义业务逻辑中使用的备忘录和原发器,有了前面的基础,现在我们就好办了。我们将要实现定定光标位置的软件,在必要的时候,我们要将现在的坐标恢复到上一个光标的位置。我们将定义一个Position类型来存储光标的X、Y轴数值(也就是状态,所以Position实现IState),用一个实体类型的Originator(原发器)实现对光标的操作。
/// <summary> /// 坐标 /// </summary> public class Position : IState { public int X { get; set; } public int Y { get; set; } }
/// <summary> /// 备忘录实体类 /// </summary> public class Memento:MementoBase<Position>{}
/// <summary> /// 原发器实体类 /// </summary> public class Originator:OriginatorBase<Position,Memento> { /// <summary> /// 更新 /// </summary> /// <param name="x">x</param> public void Update(int x) { base.state.X = x; }//共客户程序使用的非备忘录相关操作 public void DecreaseX() { base.state.X--; } public void IncreaseY() { base.state.Y++; } public Position Current { get { return base.state} } }
4、定义完了接口和抽象类,现在我们来看看如何实际地应用备忘录模式
[TestMethod()] public void test() { //定义一个IMemento类型用来先保存原先的状态 IMemento<Position> Old_Data; //定义一个Originator实体类,用来操作数据 Originator originator = new Originator(); Old_Data = originator.Memento; //调用Originator的DecreaseX()来使原来为0的X=-1 originator.DecreaseX(); //调用originator.IncreaseY();来时原来为0的Y=1 originator.IncreaseY(); Assert.AreEqual(-1, originator.Current.X); Assert.AreEqual(1, originator.Current.Y); //利用备忘录将Originator还原 originator.Memento = Old_Data;//直接指定现在进行还原 Assert.AreEqual(0, originator.Current.X); Assert.AreEqual(0, originator.Current.Y); }
在上面代码中,我们并没有引入caretaker,而是直接在代码中指定还原点(见红色注释),实际应用中可能要再创建一个caretaker类,以便在系统出现异常或者在某种特定的情况下对状态进行还原。
进一步的讨论
有同学会说,为了备份一个类里面的一些状态,有必要搞得这么复杂吗?还定义了IState、IOriginator、IMemento三种接口,更恐怖的是还有对应的抽象基类和实体类。其实,这的确增加了需要代码的复杂程度,如果需要备份的状态几乎不会发生变化,那么其实简单地在业务逻辑对象(Originator)里面增加一些字段或一个类简单地记录一下就行。但是如果原发器中有不同的状态,我们有时候只需要记录其中一个变量的状态或几个变量的状态,或者要记录的状态有时候会因为业务规则而发生改变,我们又不想让这个改变影响太多的代码,那么如同以上代码,我们将状态和备忘录抽象为ISate和IMemento,这样就可以非常方便地对会变动的状态进行调整。