【白话设计模式二十】备忘录模式(Memento)

#0 系列目录#web

#1 场景问题# ##1.1 开发仿真系统## 考虑这样一个仿真应用,功能是:模拟运行针对某个具体问题的多个解决方案,记录运行过程的各类数据,在模拟运行完成事后,好对这多个解决方案进行比较和评价,从而选定最优的解决方案。数据库

这种仿真系统,在不少领域都有应用,好比:工做流系统,对同一问题制定多个流程,而后经过仿真运行,最后来肯定最优的流程作为解决方案;在工业设计和制造领域,仿真系统的应用就更普遍了。设计模式

因为都是解决同一个具体的问题,这多个解决方案并非彻底不同的,假定它们的前半部分运行是彻底同样的,只是在后半部分采用了不一样的解决方案,后半部分须要使用前半部分运行所产生的数据。数组

因为要模拟运行多个解决方案,并且最后要根据运行结果来进行评价,这就意味着每一个方案的后半部分的初始数据应该是同样,也就是说在运行每一个方案后半部分以前,要保证数据都是由前半部分运行所产生的数据,固然,我们这里并不具体的去深刻到底有哪些解决方案,也不去深刻到底有哪些状态数据,这里只是示意一下。缓存

那么,这样的系统该如何实现呢?尤为是每一个方案运行须要的初始数据应该同样,要如何来保证呢?session

##1.2 不用模式的解决方案## 要保证初始数据的一致,实现思路也很简单:数据结构

首先模拟运行流程第一个阶段,获得后阶段各个方案运行须要的数据,并把数据保存下来,以备后用;学习

每次在模拟运行某一个方案以前,用保存的数据去从新设置模拟运行流程的对象,这样运行后面不一样的方案时,对于这些方案,初始数据就是同样的了;测试

根据上面的思路,来写出仿真运行的示意代码,示例代码以下:ui

/**
 * 模拟运行流程A,只是一个示意,代指某个具体流程
 */
public class FlowAMock {
    /**
     * 流程名称,不须要外部存储的状态数据
     */
    private String flowName;
    /**
     * 示意,代指某个中间结果,须要外部存储的状态数据
     */
    private int tempResult;
    /**
     * 示意,代指某个中间结果,须要外部存储的状态数据
     */
    private String tempState;
    /**
     * 构造方法,传入流程名称
     * @param flowName 流程名称
     */
    public FlowAMock(String flowName){
       this.flowName = flowName;
    }
   
    public String getTempState() {
       return tempState;
    }
    public void setTempState(String tempState) {
       this.tempState = tempState;
    }
    public int getTempResult() {
       return tempResult;
    }
    public void setTempResult(int tempResult) {
       this.tempResult = tempResult;
    }
   
    /**
     * 示意,运行流程的第一个阶段
     */
    public void runPhaseOne(){
       //在这个阶段,可能产生了中间结果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一来运行流程后半部分
     */
    public void schema1(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二来运行流程后半部分
     */
    public void schema2(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
}

看看如何使用这个模拟流程的对象,写个客户端来测试一下。示例代码以下:

public class Client {
    public static void main(String[] args) {
        // 建立模拟运行流程的对象
        FlowAMock mock = new FlowAMock("TestFlow");
        //运行流程的第一个阶段
        mock.runPhaseOne();
        //获得第一个阶段运行所产生的数据,后面要用
        int tempResult = mock.getTempResult();
        String tempState = mock.getTempState();
      
        //按照方案一来运行流程后半部分
        mock.schema1();
      
        //把第一个阶段运行所产生的数据从新设置回去
        mock.setTempResult(tempResult);
        mock.setTempState(tempState);
      
        //按照方案二来运行流程后半部分
        mock.schema2();
    }
}

运行结果以下:

PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3

仔细看,上面结果中框住的部分,是同样的值,这说明运行时,它们的初始数据是同样的,基本知足了功能要求。

##1.3 有何问题## 看起来实现很简单,是吧,想想有没有什么问题呢?

上面的实现有一个不太好的地方,那就是数据是一个一个零散着在外部存放的,若是须要外部存放的数据多了,会显得很杂乱。这个好解决,只须要定义一个数据对象来封装这些须要外部存放的数据就能够了,上面那样作是故意的,好提醒你们这个问题。这个就不去示例了。

还有一个严重的问题,那就是:为了把运行期间的数据放到外部存储起来,模拟流程的对象被迫把内部数据结构开放出来,这暴露了对象的实现细节,并且也破坏了对象的封装性。原本这些数据只是模拟流程的对象内部数据,应该是不对外的。

那么究竟如何实现这样的功能会比较好呢?

#2 解决方案# ##2.1 备忘录模式来解决## 来解决上述问题的一个合理的解决方案就是备忘录模式。那么什么是备忘录模式呢?

  1. 备忘录模式定义

输入图片说明

一个备忘录是一个对象,它存储另外一个对象在某个瞬间的内部状态,后者被称为备忘录的原发器。

  1. 应用备忘录模式来解决的思路

仔细分析上面的示例功能,须要在运行期间捕获模拟流程运行的对象的内部状态,这些须要捕获的内部状态就是它运行第一个阶段产生的内部数据,而且在该对象以外来保存这些状态,由于在后面它有不一样的运行方案。可是这些不一样的运行方案须要的初始数据是同样的,都是流程在第一个阶段运行所产生的数据,这就要求运行每一个方案后半部分前,要把该对象的状态恢复回到第一个阶段运行结束时候的状态

在这个示例中出现的、须要解决的问题就是:如何可以在不破坏对象的封装性的前提下,来保存和恢复对象的状态

看起来跟备忘录模式要解决的问题是如此的贴切,简直备忘录模式像是专为这个应用打造的同样。那么使用备忘录模式如何来解决这个问题呢?

备忘录模式引入一个存储状态的备忘录对象,为了让外部没法访问这个对象的值,通常把这个对象实现成为须要保存数据的对象的内部类,一般仍是私有的,这样一来,除了这个须要保存数据的对象,外部没法访问到这个备忘录对象的数据,这就保证了对象的封装性不被破坏

可是这个备忘录对象须要存储在外部,为了不让外部访问到这个对象内部的数据,备忘录模式引入了一个备忘录对象的窄接口,这个接口通常是空的,什么方法都没有,这样外部存储的地方,只是知道存储了一些备忘录接口的对象,可是因为接口是空的,它们没法经过接口去访问备忘录对象内的数据

##2.2 模式结构和说明## 备忘录模式结构如图19.1所示:

输入图片说明

Memento:备忘录。主要用来存储原发器对象的内部状态,可是具体须要存储哪些数据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的数据,原发器外部的对象不该该能访问到备忘录对象的内部数据。

Originator:原发器。使用备忘录来保存某个时刻原发器自身的状态,也可使用备忘录来恢复内部状态

Caretaker:备忘录管理者,或者称为备忘录负责人。主要负责保存备忘录对象,可是不能对备忘录对象的内容进行操做或检查

##2.3 备忘录模式示例代码##

  1. 先看看备忘录对象的窄接口,就是那个Memento接口,这个实现最简单,是个空的接口,没有任何方法定义,示例代码以下:
/**
 * 备忘录的窄接口,没有任何方法定义
 */
public interface Memento {
    //
}
  1. 看看原发器对象,它里面会有备忘录对象的实现,由于真正的备忘录对象看成原发器对象的一个私有内部类来实现了。示例代码以下:
/**
 * 原发器对象
 */
public class Originator {
    /**
     * 示意,表示原发器的状态
     */
    private String state = "";
    /**
     * 建立保存原发器对象的状态的备忘录对象
     * @return 建立好的备忘录对象
     */
    public Memento createMemento() {
       return new MementoImpl(state);
    }
    /**
     * 从新设置原发器对象的状态,让其回到备忘录对象记录的状态
     * @param memento 记录有原发器状态的备忘录对象
     */
    public void setMemento(Memento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.state = mementoImpl.getState();
    }
    /**
     * 真正的备忘录对象,实现备忘录窄接口
     * 实现成私有的内部类,不让外部访问
     */
    private static class MementoImpl implements Memento{
       /**
        * 示意,表示须要保存的状态
        */
       private String state = "";
       public MementoImpl(String state){
           this.state = state;
       }
       public String getState() {
           return state;
       }
    }
}
  1. 接下来看看备忘录管理者对象,示例代码以下:
/**
 * 负责保存备忘录的对象
 */
public class Caretaker{
    /**
     * 记录被保存的备忘录对象
     */
    private Memento memento = null;
    /**
     * 保存备忘录对象
     * @param memento 被保存的备忘录对象
     */
    public void saveMemento(Memento memento){
       this.memento = memento;
    }
    /**
     * 获取被保存的备忘录对象
     * @return 被保存的备忘录对象
     */
    public Memento retriveMemento(){
       return this.memento;
    }
}

##2.4 使用备忘录模式重写示例## 学习了备忘录模式的基本知识事后,来尝试一下,使用备忘录模式把前面的示例重写一下,好看看如何使用备忘录模式。

首先,那个模拟流程运行的对象,就至关于备忘录模式中的原发器;

而它要保存的数据,原来是零散的,如今作一个备忘录对象来存储这些数据,而且把这个备忘录对象实现成为内部类;

固然为了保存这个备忘录对象,仍是须要提供管理者对象的;

为了和管理者对象交互,管理者须要知道保存对象的类型,那就提供一个备忘录对象的窄接口来供管理者使用,至关于标识了类型

此时程序的结构如图19.2所示:

输入图片说明

  1. 先来看看备忘录对象的窄接口吧,示例代码以下:
/**
 * 模拟运行流程A的对象的备忘录接口,是个窄接口
 */
public interface FlowAMockMemento {
    //空的
}
  1. 再来看看新的模拟运行流程A的对象,至关于原发器对象了,它的变化比较多,大体有以下变化:

首先这个对象原来暴露出去的内部状态,不用再暴露出去了,也就是内部状态不用再对外提供getter/setter方法了;

在这个对象里面提供一个私有的备忘录对象,里面封装想要保存的内部状态,同时让这个备忘录对象实现备忘录对象的窄接口;

在这个对象里面提供建立备忘录对象,和根据备忘录对象恢复内部状态的方法;

具体的示例代码以下:

/**
 * 模拟运行流程A,只是一个示意,代指某个具体流程
 */
public class FlowAMock {
    /**
     * 流程名称,不须要外部存储的状态数据
     */
    private String flowName;
    /**
     * 示意,代指某个中间结果,须要外部存储的状态数据
     */
    private int tempResult;
    /**
     * 示意,代指某个中间结果,须要外部存储的状态数据
     */
    private String tempState;
    /**
     * 构造方法,传入流程名称
     * @param flowName 流程名称
     */
    public FlowAMock(String flowName){
       this.flowName = flowName;
    }
    /**
     * 示意,运行流程的第一个阶段
     */
    public void runPhaseOne(){
       //在这个阶段,可能产生了中间结果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一来运行流程后半部分
     */
    public void schema1(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二来运行流程后半部分
     */
    public void schema2(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
    /**
     * 建立保存原发器对象的状态的备忘录对象
     * @return 建立好的备忘录对象
     */
    public FlowAMockMemento createMemento() {
       return new MementoImpl(this.tempResult,this.tempState);
    }
    /**
     * 从新设置原发器对象的状态,让其回到备忘录对象记录的状态
     * @param memento 记录有原发器状态的备忘录对象
     */
    public void setMemento(FlowAMockMemento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.tempResult = mementoImpl.getTempResult();
       this.tempState = mementoImpl.getTempState();
    }
    /**
     * 真正的备忘录对象,实现备忘录窄接口
     * 实现成私有的内部类,不让外部访问
     */
    private static class MementoImpl implements FlowAMockMemento{
       /**
        * 示意,保存某个中间结果
        */
       private int tempResult;
       /**
        * 示意,保存某个中间结果
        */
       private String tempState;
       public MementoImpl(int tempResult,String tempState){
           this.tempResult = tempResult;
           this.tempState = tempState;
       }
       public int getTempResult() {
           return tempResult;
       }
       public String getTempState() {
           return tempState;
       }
    }
}
  1. 接下来要来实现提供保存备忘录对象的管理者了,示例代码以下:
/**
 * 负责保存模拟运行流程A的对象的备忘录对象
 */
public class FlowAMementoCareTaker {
    /**
     * 记录被保存的备忘录对象
     */
    private FlowAMockMemento memento = null;
    /**
     * 保存备忘录对象
     * @param memento 被保存的备忘录对象
     */
    public void saveMemento(FlowAMockMemento memento){
       this.memento = memento;
    }
    /**
     * 获取被保存的备忘录对象
     * @return 被保存的备忘录对象
     */
    public FlowAMockMemento retriveMemento(){
       return this.memento;
    }
}
  1. 最后来看看,如何使用上面按照备忘录模式实现的这些对象呢,写个新的客户端来测试一下,示例代码以下:
public class Client {
    public static void main(String[] args) {
       // 建立模拟运行流程的对象
       FlowAMock mock = new FlowAMock("TestFlow");
       //运行流程的第一个阶段
       mock.runPhaseOne();     
       //建立一个管理者
       FlowAMementoCareTaker careTaker = new FlowAMementoCareTaker();
       //建立此时对象的备忘录对象,并保存到管理者对象那里,后面要用
       FlowAMockMemento memento = mock.createMemento();
       careTaker.saveMemento(memento);
     
       //按照方案一来运行流程后半部分
       mock.schema1();
     
       //从管理者获取备忘录对象,而后设置回去,
       //让模拟运行流程的对象本身恢复本身的内部状态
       mock.setMemento(careTaker.retriveMemento());
     
       //按照方案二来运行流程后半部分
       mock.schema2();
    }
}

运行结果跟前面的示例是同样的,结果以下:

PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3

好好体会一下上面的示例,因为备忘录对象是一个私有的内部类,外面只能经过备忘录对象的窄接口来获取备忘录对象,而这个接口没有任何方法,仅仅起到了一个标识对象类型的做用,从而保证内部的数据不会被外部获取或是操做,保证了原发器对象的封装性,也就再也不暴露原发器对象的内部结构了

#3 模式讲解# ##3.1 认识备忘录模式##

  1. 备忘录模式的功能

备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态。这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不该该暴露的细节;另外一个是捕获的是对象的内部状态,并且一般仍是运行期间某个时刻,对象的内部状态

为何要捕获这个对象的内部状态呢?捕获这个内部状态有什么用呢?

是要在之后的某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的,前面保存状态就是为了后面恢复,虽然不是必定要恢复,可是目的是为了恢复。这也是不少人理解备忘录模式的时候,忽视掉的地方,他们太关注备忘,而忽视了恢复,这是不全面的理解。

捕获的状态存放在哪里呢?

备忘录模式中,捕获的内部状态,存储在备忘录对象中而备忘录对象,一般会被存储在原发器对象以外,也就是被保存状态的对象的外部,一般是存放在管理者对象哪里。

  1. 备忘录对象

在备忘录模式中,备忘录对象,一般就是用来记录原发器须要保存的状态的对象,简单点的实现,也就是个封装数据的对象。

可是这个备忘录对象和普通的封装数据的对象仍是有区别的,主要就是这个备忘录对象,通常只让原发器对象来操做,而不是像普通的封装数据的对象那样,谁均可以使用。为了保证这一点,一般会把备忘录对象做为原发器对象的内部类来实现,并且会实现成私有的,这就断绝了外部来访问这个备忘录对象的途径

可是备忘录对象须要保存在原发器对象以外,为了与外部交互,一般备忘录对象都会实现一个窄接口,来标识对象的类型

  1. 原发器对象

原发器对象,就是须要被保存状态的对象,也是有可能须要恢复状态的对象原发器通常会包含备忘录对象的实现

一般原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法里面,原发器对象会建立备忘录对象,把须要保存的状态数据设置到备忘录对象中,而后把备忘录对象提供给管理者对象来保存。

固然,原发器对象也应该提供这样的方法:按照外部要求来恢复内部状态到某个备忘录对象记录的状态

  1. 管理者对象

在备忘录模式中,管理者对象,主要是负责保存备忘录对象,这里有几点要讲一下。

并不必定要特别的作出一个管理者对象来,广义地说,调用原发器得到备忘录对象后,备忘录对象放在哪里,哪一个对象就能够算是管理者对象

管理者对象并非只能管理一个备忘录对象,一个管理者对象能够管理不少的备忘录对象,虽然前面的示例中是保存一个备忘录对象,别忘了那只是个示意,并非只能实现成那样。

狭义的管理者对象,是只管理同一类的备忘录对象,可是广义管理者对象是能够管理不一样类型的备忘录对象的

管理者对象须要实现的基本功能主要就是:存入备忘录对象、保存备忘录对象、获取备忘录对象,若是从功能上看,就是一个缓存功能的实现,或者是一个简单的对象实例池的实现

管理者虽然能存取备忘录对象,可是不能访问备忘录对象内部的数据

  1. 窄接口和宽接口

在备忘录模式中,为了控制对备忘录对象的访问,出现了窄接口和宽接口的概念。

窄接口:管理者只能看到备忘录的窄接口,窄接口的实现里面一般没有任何的方法,只是一个类型标识,窄接口使得管理者只能将备忘录传递给其它对象。

宽接口:原发器可以看到一个宽接口,容许它访问所需的全部数据,来返回到先前的状态。理想情况是:只容许生成备忘录的原发器来访问该备忘录的内部状态,一般实现成为原发器内的一个私有内部类。

在前面的示例中,定义了一个名称为FlowAMockMemento的接口,里面没有定义任何方法,而后让备忘录来实现这个接口,从而标识备忘录就是这么一个FlowAMockMemento的类型,这个接口就是窄接口

在前面的实现中,备忘录对象是实如今原发器内的一个私有内部类,只有原发器对象能访问它,原发器能够访问到备忘录对象全部的内部状态,这就是宽接口

这也算是备忘录模式的标准实现方式,那就是窄接口没有任何的方法,把备忘录对象实现成为原发器对象的私有内部类

那么能不能在窄接口里面提供备忘录对象对外的方法,变相对外提供一个“宽”点的接口呢?

一般状况是不会这么作的,由于这样一来,全部能拿到这个接口的对象就能够经过这个接口来访问备忘录内部的数据或是功能,这违反了备忘录模式的初衷,备忘录模式要求“在不破坏封装性的前提下”,若是这么作,那就等因而暴露了内部细节,所以,备忘录模式在实现的时候,对外可能是采用窄接口,并且一般不会定义任何方法。

  1. 使用备忘录的潜在代价

标准的备忘录模式的实现机制是依靠缓存来实现的,所以,当须要备忘的数据量较大时,或者是存储的备忘录对象数据量不大可是数量不少的时候,或者是用户很频繁的建立备忘录对象的时候,这些都会致使很是大的开销。

所以在使用备忘录模式的时候,必定要好好思考应用的环境,若是使用的代价过高,就不要选用备忘录模式,能够采用其它的替代方案。

  1. 增量存储

若是须要频繁的建立备忘录对象,并且建立和应用备忘录对象来恢复状态的顺序是可控的,那么可让备忘录进行增量存储,也就是备忘录能够仅仅存储原发器内部相对于上一次存储状态后的增量改变

好比:在命令模式实现可撤销命令的实现中,就可使用备忘录来保存每一个命令对应的状态,而后在撤销命令的时候,使用备忘录来恢复这些状态。因为命令的历史列表是按照命令操做的顺序来存放的,也是按照这个历史列表来进行取消和重作的,所以顺序是可控的。那么这种状况,还可让备忘录对象只存储一个命令所产生的增量改变而不是它所影响的每个对象的完整状态。

  1. 备忘录模式调用顺序示意图

在使用备忘录模式的时候,分红了两个阶段,第一个阶段是建立备忘录对象的阶段,第二个阶段是使用备忘录对象来恢复原发器对象的状态的阶段。它们的调用顺序是不同的,下面分开用图来示意一下。

先看建立备忘录对象的阶段,调用顺序如图19.3所示:

输入图片说明

再看看使用备忘录对象来恢复原发器对象的状态的阶段,调用顺序如图19.4所示:

输入图片说明

##3.2 结合原型模式## 在原发器对象建立备忘录对象的时候,若是原发器对象中所有或者大部分的状态都须要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例

仍是经过示例来讲明。只须要修改原发器对象就能够了,大体有以下变化:

首先原发器对象要实现可克隆的,好在这个原发器对象的状态数据都很简单,都是基本数据类型,因此直接用默认的克隆方法就能够了,不用本身实现克隆,更不涉及深度克隆,不然,正确实现深度克隆仍是个问题;

备忘录对象的实现要修改,只须要存储原发器对象克隆出来的实例对象就能够了;

相应的建立和设置备忘录对象的地方都要作修改;

示例代码以下:

/**
 * 模拟运行流程A,只是一个示意,代指某个具体流程
 */
public class FlowAMockPrototype implements Cloneable {
    private String flowName;
    private int tempResult;
    private String tempState;
    public FlowAMockPrototype(String flowName){
       this.flowName = flowName;
    }
   
    public void runPhaseOne(){
       //在这个阶段,可能产生了中间结果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    public void schema1(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
   
    public void schema2(){
       //示意,须要使用第一个阶段产生的数据
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
    /**
     * 建立保存原发器对象的状态的备忘录对象
     * @return 建立好的备忘录对象
     */
    public FlowAMockMemento createMemento() {
       try {
           return new MementoImplPrototype((FlowAMockPrototype) this.clone());
       } catch (CloneNotSupportedException e) {
           e.printStackTrace();
       }
       return null;
    }
    /**
     * 从新设置原发器对象的状态,让其回到备忘录对象记录的状态
     * @param memento 记录有原发器状态的备忘录对象
     */
    public void setMemento(FlowAMockMemento memento) {
       MementoImplPrototype mementoImpl = (MementoImplPrototype)memento;
       this.tempResult = mementoImpl.getFlowAMock().tempResult;
       this.tempState = mementoImpl.getFlowAMock().tempState;
    }
    /**
     * 真正的备忘录对象,实现备忘录窄接口,实现成私有的内部类,不让外部访问
     */
    private static class MementoImplPrototype implements FlowAMockMemento{
       private FlowAMockPrototype flowAMock = null;
      
       public MementoImplPrototype(FlowAMockPrototype f){
           this.flowAMock = f;
       }
 
       public FlowAMockPrototype getFlowAMock() {
           return flowAMock;
       }
    }
}

好了,结合原型模式来实现备忘录模式的示例就写好了,在前面的客户测试程序中,建立原发器对象的时候,使用这个新实现的原发器对象就能够了。去测试和体会一下,看看是否能正确实现须要的功能。

不过要注意一点,就是若是克隆对象很是复杂,或者须要不少层次的深度克隆,实现克隆的时候会比较麻烦

##3.3 离线存储## 标准的备忘录模式,没有讨论离线存储的实现。

事实上,从备忘录模式的功能和实现上,是能够把备忘录的数据实现成为离线存储的,也就是不只限于存储于内存中,能够把这些备忘数据存储到文件中、xml中、数据库中,从而支持跨越会话的备份和恢复功能

离线存储甚至能帮助应对应用崩溃,而后关闭重启的状况,应用重启事后,从离线存储里面获取相应的数据,而后从新设置状态,恢复到崩溃前的状态。

固然,并非全部的备忘数据都须要离线存储,通常来说,须要存储很长时间、或者须要支持跨越会话的备份和恢复功能、或者是但愿系统关闭后还能被保存的备忘数据,这些状况建议采用离线存储。

离线存储的实现也很简单,就之前面模拟运行流程的应用来讲,若是要实现离线存储,主要须要修改管理者对象,把它保存备忘录对象的方法,实现成为保存到文件中,而恢复备忘录对象实现成为读取文件就能够了。对于其它相关对象,主要是要实现序列化,只有可序列化的对象才能被存储到文件中。

若是实现保存备忘录对象到文件,就不用在内存中保存了,去掉用来“记录被保存的备忘录对象”的这个属性。示例代码以下:

/**
 * 负责在文件中保存模拟运行流程A的对象的备忘录对象
 */
public class FlowAMementoFileCareTaker {
    /**
     * 保存备忘录对象
     * @param memento 被保存的备忘录对象
     */
    public void saveMemento(FlowAMockMemento memento){
       //写到文件中
       ObjectOutputStream out = null;
       try{
           out = new ObjectOutputStream(
                  new BufferedOutputStream(
                         new FileOutputStream("FlowAMemento")
                  )
           );
           out.writeObject(memento);
       }catch(Exception err){
           err.printStackTrace();
       }finally{
           try {
              out.close();
           } catch (IOException e) {
              e.printStackTrace();
           }
       }
    }
    /**
     * 获取被保存的备忘录对象
     * @return 被保存的备忘录对象
     */
    public FlowAMockMemento retriveMemento(){
       FlowAMockMemento memento = null;
       //从文件中获取备忘录数据
       ObjectInputStream in = null;
       try{
           in = new ObjectInputStream(
                  new BufferedInputStream(
                         new FileInputStream("FlowAMemento")
                  )
           );
           memento = (FlowAMockMemento)in.readObject();
       }catch(Exception err){
           err.printStackTrace();
       }finally{
           try {
              in.close();
           } catch (IOException e) {
              e.printStackTrace();
           }
       }
       return memento;
    }
}

同时须要让备忘录对象的窄接口继承可序列化接口,示例代码以下:

/**
 * 模拟运行流程A的对象的备忘录接口,是个窄接口
 */
public interface FlowAMockMemento extends Serializable  {
}

还有FlowAMock对象,也须要实现可序列化示例代码以下:

/**
 * 模拟运行流程A,只是一个示意,代指某个具体流程
 */
public class FlowAMock implements Serializable  {
    //中间的实现省略了
}

好了,保存到文件的存储就实现好了,在前面的客户测试程序中,建立管理者对象的时候,使用这个新实现的管理者对象就能够了。去测试和体会一下。

##3.4 再次实现可撤销操做## 在命令模式中,讲到了可撤销的操做,在那里讲到:有两种基本的思路来实现可撤销的操做,一种是补偿式或者反操做式:好比被撤销的操做是加的功能,那撤消的实现就变成减的功能;同理被撤销的操做是打开的功能,那么撤销的实现就变成关闭的功能。

另一种方式是存储恢复式,意思就是把操做前的状态记录下来,而后要撤销操做的时候就直接恢复回去就能够了。

这里就该来实现第二种方式,就是存储恢复式,为了让你们更好的理解可撤销操做的功能,仍是用原来的那个例子,对比学习会比较清楚。

这也至关因而命令模式和备忘录模式结合的一个例子,并且因为命令列表的存在,对应保存的备忘录对象也是多个

  1. 范例需求

考虑一个计算器的功能,最简单的那种,只能实现加减法运算,如今要让这个计算器支持可撤销的操做。

  1. 存储恢复式的解决方案

存储恢复式的实现,可使用备忘录模式,大体实现的思路以下:

把原来的运算类,就是那个Operation类,看成原发器,原来的内部状态result,就只提供一个getter方法,来让外部获取运算的结果;

在这个原发器里面,实现一个私有的备忘录对象;

把原来的计算器类,就是Calculator类,看成管理者,把命令对应的备忘录对象保存在这里。当须要撤销操做的时候,就把相应的备忘录对象设置回到原发器去,恢复原发器的状态;

(1)定义备忘录对象的窄接口,示例代码以下:

public interface Memento {
    //空的
}

(2)定义命令的接口,有几点修改:

修改原来的undo方法,传入备忘录对象

添加一个redo方法,传入备忘录对象

添加一个createMemento的方法,获取须要被保存的备忘录对象

示例代码以下:

/**
 * 定义一个命令的接口
 */
public interface Command {
    /**
     * 执行命令
     */
    public void execute();
    /**
     * 撤销命令,恢复到备忘录对象记录的状态
     * @param m 备忘录对象
     */
    public void undo(Memento m);
    /**
     * 重作命令,恢复到备忘录对象记录的状态
     * @param m 备忘录对象
     */
    public void redo(Memento m);
    /**
     * 建立保存原发器对象的状态的备忘录对象
     * @return 建立好的备忘录对象
     */
    public Memento createMemento();
}

(3)再来定义操做运算的接口,至关于计算器类这个原发器对外提供的接口,它须要作以下的调整:

去掉原有的setResult方法,内部状态,不容许外部操做

添加一个createMemento的方法,获取须要保存的备忘录对象

添加一个setMemento的方法,来从新设置原发器对象的状态

示例代码以下:

/**
 * 操做运算的接口
 */
public interface OperationApi {
    /**
     * 获取计算完成后的结果
     * @return 计算完成后的结果
     */
    public int getResult();
    /**
     * 执行加法
     * @param num 须要加的数
     */
    public void add(int num);
    /**
     * 执行减法
     * @param num 须要减的数
     */
    public void substract(int num);
    /**
     * 建立保存原发器对象的状态的备忘录对象
     * @return 建立好的备忘录对象
     */
    public Memento createMemento();
    /**
     * 从新设置原发器对象的状态,让其回到备忘录对象记录的状态
     * @param memento 记录有原发器状态的备忘录对象
     */
    public void setMemento(Memento memento);
}

(4)因为如今撤销和恢复操做是经过使用备忘录对象,直接来恢复原发器的状态,所以就再也不须要按照操做类型来区分了,对于全部的命令实现,它们的撤销和重作都是同样的。原来的实现是要区分的,若是是撤销加的操做,那就是减,而撤销减的操做,那就是加。如今就不区分了,统一使用备忘录对象来恢复。

所以,实现一个全部命令的公共对象,在里面把公共功能都实现了,这样每一个命令在实现的时候就简单了。顺便把设置持有者的公共实现也放到这个公共对象里面来,这样各个命令对象就不用再实现这个方法了,示例代码以下:

/**
 * 命令对象的公共对象,实现各个命令对象的公共方法
 */
public abstract class AbstractCommand implements Command{
    /**
     * 具体的功能实现,这里无论
     */
    public abstract void execute();
    /**
     * 持有真正的命令实现者对象
     */
    protected OperationApi operation = null;
    public void setOperation(OperationApi operation) {
       this.operation = operation;
    }
    public Memento createMemento() {
       return this.operation.createMemento();
    }
    public void redo(Memento m) {
       this.operation.setMemento(m);
    }
    public void undo(Memento m) {
       this.operation.setMemento(m);
    }
}

(5)有了公共的命令实现对象,各个具体命令的实现就简单了,实现加法命令的对象实现,再也不直接实现Command接口了,而是继承命令的公共对象,这样只须要实现跟本身命令相关的业务方法就行了,示例代码以下:

public class AddCommand extends AbstractCommand{
    private int opeNum;
    public AddCommand(int opeNum){
       this.opeNum = opeNum;
    }
    public void execute() {
       this.operation.add(opeNum);
    }
}

看看减法命令的实现,跟加法命令的实现差很少,示例代码以下:

public class SubstractCommand extends AbstractCommand{
    private int opeNum;
    public SubstractCommand(int opeNum){
       this.opeNum = opeNum;
    }
    public void execute() {
       this.operation.substract(opeNum);
    }
}

(6)接下来看看运算类的实现,至关因而原发器对象,它的实现有以下改变:

再也不提供setResult方法,内部状态,不容许外部来操做

添加了createMemento和setMemento方法的实现

添加实现了一个私有的备忘录对象

示例代码以下:

/**
 * 运算类,真正实现加减法运算
 */
public class Operation implements OperationApi{
    /**
     * 记录运算的结果
     */
    private int result;
    public int getResult() {
       return result;
    }
    public void add(int num){
       result += num;
    }
    public void substract(int num){
       result -= num;
    }
    public Memento createMemento() {
       MementoImpl m = new MementoImpl(result);
       return m;
    }
    public void setMemento(Memento memento) {
       MementoImpl m = (MementoImpl)memento;
       this.result = m.getResult();
    }
    /**
     * 备忘录对象
     */
    private static class MementoImpl implements Memento{
       private int result = 0;
       public MementoImpl(int result){
           this.result = result;
       }
       public int getResult() {
           return result;
       }
    }
}

(7)接下来该看看如何具体的使用备忘录对象来实现撤销操做和重作操做了。一样在计算器类里面实现,这个时候,计算器类就至关因而备忘录模式管理者对象。

实现思路:因为对于每一个命令对象,撤销和重作的状态是不同的,撤销是回到命令操做前的状态,而重作是回到命令操做后的状态,所以对每个命令,使用一个备忘录对象的数组来记录对应的状态。

这些备忘录对象是跟命令对象相对应的,所以也跟命令历史记录同样,设立相应的历史记录,它的顺序跟命令彻底对应起来。在操做命令的历史记录的同时,对应操做相应的备忘录对象记录。

示例代码以下:

/**
 * 计算器类,计算器上有加法按钮、减法按钮,还有撤销和恢复的按钮
 */
public class Calculator {
    /**
     * 命令的操做的历史记录,在撤销时候用
     */
    private List<Command> undoCmds = new ArrayList<Command>();
    /**
     * 命令被撤销的历史记录,在恢复时候用
     */
    private List<Command> redoCmds = new ArrayList<Command>();
    /**
     * 命令操做对应的备忘录对象的历史记录,在撤销时候用,
     * 数组有两个元素,第一个是命令执行前的状态,第二个是命令执行后的状态
     */
    private List<Memento[]> undoMementos = new ArrayList<Memento[]>();
    /**
     * 被撤销命令对应的备忘录对象的历史记录,在恢复时候用,
     * 数组有两个元素,第一个是命令执行前的状态,第二个是命令执行后的状态
     */
    private List<Memento[]> redoMementos = new ArrayList<Memento[]>();
  
    private Command addCmd = null;
    private Command substractCmd = null;
    public void setAddCmd(Command addCmd) {
       this.addCmd = addCmd;
    }
    public void setSubstractCmd(Command substractCmd) {
       this.substractCmd = substractCmd;
    }  

    public void addPressed(){
       //获取对应的备忘录对象,并保存在相应的历史记录里面
       Memento m1 = this.addCmd.createMemento();
     
       //执行命令
       this.addCmd.execute();
       //把操做记录到历史记录里面
       undoCmds.add(this.addCmd);

       //获取执行命令后的备忘录对象
       Memento m2 = this.addCmd.createMemento();
       //设置到撤销的历史记录里面
       this.undoMementos.add(new Memento[]{m1,m2});
    }
    public void substractPressed(){
       //获取对应的备忘录对象,并保存在相应的历史记录里面    
       Memento m1 = this.substractCmd.createMemento();
     
       //执行命令
       this.substractCmd.execute();
       //把操做记录到历史记录里面
       undoCmds.add(this.substractCmd);
     
       //获取执行命令后的备忘录对象
       Memento m2 = this.substractCmd.createMemento();
       //设置到撤销的历史记录里面
       this.undoMementos.add(new Memento[]{m1,m2});
    }
    public void undoPressed(){
       if(undoCmds.size()>0){
           //取出最后一个命令来撤销
           Command cmd = undoCmds.get(undoCmds.size()-1);
           //获取对应的备忘录对象
           Memento[] ms = undoMementos.get(undoCmds.size()-1);
          
           //撤销
           cmd.undo(ms[0]);
         
           //若是还有恢复的功能,那就把这个命令记录到恢复的历史记录里面
           redoCmds.add(cmd);
           //把相应的备忘录对象也添加过去
           redoMementos.add(ms);
         
           //而后把最后一个命令删除掉,
           undoCmds.remove(cmd);
           //把相应的备忘录对象也删除掉
           undoMementos.remove(ms);
       }else{
           System.out.println("很抱歉,没有可撤销的命令");
       }
    }
    public void redoPressed(){
       if(redoCmds.size()>0){
           //取出最后一个命令来重作
           Command cmd = redoCmds.get(redoCmds.size()-1);
           //获取对应的备忘录对象
           Memento[] ms = redoMementos.get(redoCmds.size()-1);
         
           //重作
           cmd.redo(ms[1]);
         
           //把这个命令记录到可撤销的历史记录里面
           undoCmds.add(cmd);
           //把相应的备忘录对象也添加过去
           undoMementos.add(ms);
           //而后把最后一个命令删除掉
           redoCmds.remove(cmd);
           //把相应的备忘录对象也删除掉
           redoMementos.remove(ms);
       }else{
           System.out.println("很抱歉,没有可恢复的命令");
       }
    }
}

(8)客户端跟之前的实现没有什么变化,示例代码以下:

public class Client {
    public static void main(String[] args) {
       //1:组装命令和接收者
       //建立接收者
       OperationApi operation = new Operation();
       //建立命令
       AddCommand addCmd = new AddCommand(5);
       SubstractCommand substractCmd = new SubstractCommand(3);
       //组装命令和接收者
       addCmd.setOperation(operation);
       substractCmd.setOperation(operation);
     
       //2:把命令设置到持有者,就是计算器里面
       Calculator calculator = new Calculator();
       calculator.setAddCmd(addCmd);
       calculator.setSubstractCmd(substractCmd);
     
       //3:模拟按下按钮,测试一下
       calculator.addPressed();
       System.out.println("一次加法运算后的结果为:" +operation.getResult());
       calculator.substractPressed();
       System.out.println("一次减法运算后的结果为:" +operation.getResult());
     
       //测试撤消
       calculator.undoPressed();
       System.out.println("撤销一次后的结果为:" +operation.getResult());
       calculator.undoPressed();
       System.out.println("再撤销一次后的结果为:" +operation.getResult());
     
       //测试恢复
       calculator.redoPressed();
       System.out.println("恢复操做一次后的结果为:" +operation.getResult());
       calculator.redoPressed();
       System.out.println("再恢复操做一次后的结果为:" +operation.getResult());
    }
}

运行结果,示例以下:

一次加法运算后的结果为:5
一次减法运算后的结果为:2
撤销一次后的结果为:5
再撤销一次后的结果为:0
恢复操做一次后的结果为:5
再恢复操做一次后的结果为:2

跟前面采用补偿式或者反操做式获得的结果是同样的。好好体会一下,对比两种实现方式,看看都是怎么实现的。顺便也体会一下命令模式和备忘录模式是如何结合起来实现功能的。

##3.5 备忘录模式的优缺点##

  1. 更好的封装性

备忘录模式经过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,可是因为备忘录对象的窄接口并不提供任何方法,这样有效的保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部

  1. 简化了原发器

备忘录模式中,备忘录对象被保存到原发器对象以外,让客户来管理他们请求的状态,从而让原发器对象获得简化。

  1. 窄接口和宽接口

备忘录模式,经过引入窄接口和宽接口,使得不一样的地方,对备忘录对象的访问是不同的。窄接口保证了只有原发器才能够访问备忘录对象的状态

  1. 可能会致使高开销

备忘录模式基本的功能,就是对备忘录对象的存储和恢复,它的基本实现方式就是缓存备忘录对象。这样一来,若是须要缓存的数据量很大,或者是特别频繁的建立备忘录对象,开销是很大的。

##3.6 思考备忘录模式##

  1. 备忘录模式的本质

备忘录模式的本质:保存和恢复内部状态。

保存是手段,恢复才是目的,备忘录模式备忘些什么东西呢?

就是原发器对象的内部状态,备忘录模式备忘的就是这些内部状态,这些内部状态是不对外的,只有原发器对象才可以进行操做。

标准的备忘录模式保存数据的手段是:经过内存缓存,广义的备忘录模式实现的时候,能够采用离线存储的方式,把这些数据保存到文件或者数据库等地方

备忘录模式为什么要保存数据呢,目的就是为了在有须要的时候,恢复原发器对象的内部状态,因此恢复是备忘录模式的目的

根据备忘录模式的本质,从广义上讲,进行数据库存取操做;或者是web应用中的request、session、servletContext等的attribute数据存取;更进一步,大多数基于缓存功能的数据操做均可以视为广义的备忘录模式。不过广义到这个地步,还提备忘录模式已经没有什么意义了,因此对于备忘录模式仍是多从狭义上来讲

事实上,对于备忘录模式最主要的一个点,就是封装状态的备忘录对象,不该该被除了原发器对象以外的对象访问,至于如何存储那都是小事情。由于备忘录模式要解决的主要问题就是:在不破坏对象封装性的前提下,来保存和恢复对象的内部状态。这是一个很主要的判断点,若是备忘录对象可让原发器对象外的对象访问的话,那就算是广义的备忘录模式了,其实提不提备忘录模式已经没有太大的意义了

  1. 什么时候选用备忘录模式

建议在以下状况中,选用备忘录模式:

若是必须保存一个对象在某一个时刻的所有或者部分状态,这样在之后须要的时候,能够把该对象恢复到先前的状态。可使用备忘录模式,使用备忘录对象来封装和保存须要保存的内部状态,而后把备忘录对象保存到管理者对象里面,在须要的时候,再从管理者对象里面获取备忘录对象,来恢复对象的状态。

若是须要保存一个对象的内部状态,可是若是用接口来让其它对象直接获得这些须要保存的状态,将会暴露对象的实现细节并破坏对象的封装性。可使用备忘录模式,把备忘录对象实现成为原发器对象的内部类,并且仍是私有的,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了须要保存的状态,又不会暴露原发器对象的内部实现细节。

##3.7 相关模式##

  1. 备忘录模式和命令模式

这两个模式能够组合使用。

命令模式实现中,在实现命令的撤销和重作的时候,可使用备忘录模式,在命令操做的时候记录下操做先后的状态,而后在命令撤销和重作的时候,直接使用相应的备忘录对象来恢复状态就能够了。

在这种撤销的执行顺序和重作执行顺序可控的状况下,备忘录对象还能够采用增量式记录的方式,能够减小缓存的数据量。

  1. 备忘录模式和原型模式

这两个模式能够组合使用。

在原发器对象建立备忘录对象的时候,若是原发器对象中所有或者大部分的状态都须要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例,这个在前面已经示例过了,这里就不赘述了。

相关文章
相关标签/搜索