行为型-备忘录(Memento)

4/12/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 开发仿真系统

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

这种仿真系统,在很多领域都有应用,比如:工作流系统,对同一问题制定多个流程,然后通过仿真运行,最后来确定最优的流程做为解决方案;在工业设计和制造领域,仿真系统的应用就更广泛了。

由于都是解决同一个具体的问题,这多个解决方案并不是完全不一样的,假定它们的前半部分运行是完全一样的,只是在后半部分采用了不同的解决方案,后半部分需要使用前半部分运行所产生的数据。

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

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

# 1.2 不用模式的解决方案

要保证初始数据的一致,实现思路也很简单:

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

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

/**
 * 模拟运行流程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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

客户端:

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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

运行结果:

# 1.3 有何问题

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

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

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

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

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是备忘录模式。

备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。

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

# 2.1 解决思路

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

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

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

但是这个备忘录对象需要存储在外部,为了避免让外部访问到这个对象内部的数据,备忘录模式引入了一个备忘录对象的窄接口,这个接口一般是空的,什么方法都没有,这样外部存储的地方,只是知道存储了一些备忘录接口的对象,但是由于接口是空的,它们无法通过接口去访问备忘录对象内的数据。

# 2.2 模式结构和说明

备忘录模式结构如图所示:

classDiagram Originator ..> Memento Caretaker o--> Memento class Memento{ <<interface>> } class Originator{ -String state +createMemento() Memento +setMemento(Memento) } class Caretaker{ -Memento memento +saveMemento(Memento) +retriveMemento() Memento }
  • Memento:备忘录。主要用来存储原发器对象的内部状态,但是具体需要存储哪些数据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的数据,原发器外部的对象不应该能访问到备忘录对象的内部数据。
  • Originator:原发器。使用备忘录来保存某个时刻原发器自身的状态,也可以使用备忘录来恢复内部状态。
  • Caretaker:备忘录管理者,或者称为备忘录负责人。主要负责保存备忘录对象,但是不能对备忘录对象的内容进行操作或检查。

# 2.3 示例代码

Memento接口,空接口,没有任何方法定义:

/**
 * 备忘录的窄接口,没有任何方法定义
 */
public interface Memento {
    //
}
1
2
3
4
5

原发器对象,它里面会有备忘录对象的实现,因为真正的备忘录对象当作原发器对象的一个私有内部类来实现了:

/**
 * 原发器对象
 */
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

备忘录管理者对象:

/**
 * 负责保存备忘录的对象
 */
public class Caretaker {

    /**
     * 记录被保存的备忘录对象
     */
    private Memento memento = null;

    /**
     * 保存备忘录对象
     *
     * @param memento 被保存的备忘录对象
     */
    public void saveMemento(Memento memento) {
        this.memento = memento;
    }

    /**
     * 获取被保存的备忘录对象
     *
     * @return 被保存的备忘录对象
     */
    public Memento retriveMemento() {
        return this.memento;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 2.4 重写案例

利用备忘录模式重写前面的案例。

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

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

当然为了保存这个备忘录对象,还是需要提供管理者对象的;

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

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

classDiagram FlowAMockMemento <|.. FlowAMock FlowAMockMemento <--o FlowAMementoCareTaker class FlowAMockMemento{ <<interface>> } class FlowAMock{ -String flowName -int tempResult -String tempState +FlowAMock(String) +runPhaseOne() void +schema1() void +schema2() void +createMemento() FlowAMockMemento +setMemento(FlowAMockMemento) void } class FlowAMementoCareTaker{ -FlowAMockMemento memento +saveMemento(FlowAMockMemento) +retriveMemento() FlowAMockMemento }

备忘录对象的窄接口:

/**
 * 模拟运行流程A的对象的备忘录接口,是个窄接口
 */
public interface FlowAMockMemento {
    //空的
}
1
2
3
4
5
6

新的模拟运行流程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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

实现提供保存备忘录对象的管理者:

/**
 * 负责保存模拟运行流程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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

客户端:

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();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

运行结果:

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

# 三:模式讲解

# 3.1 认识备忘录模式

1. 备忘录模式的功能

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

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

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

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

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

2. 备忘录对象

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

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

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

3. 原发器对象

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

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

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

4. 管理者对象

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

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

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

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

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

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

5. 窄接口和宽接口

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

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

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

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

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

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

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

通常情况是不会这么做的,因为这样一来,所有能拿到这个接口的对象就可以通过这个接口来访问备忘录内部的数据或是功能,这违反了备忘录模式的初衷,备忘录模式要求 "在不破坏封装性的前提下",如果这么做,那就等于是暴露了内部细节,因此,备忘录模式在实现的时候,对外多是采用窄接口,而且通常不会定义任何方法。

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

标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁的创建备忘录对象的时候,这些都会导致非常大的开销。

因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价太高,就不要选用备忘录模式,可以采用其它的替代方案。

7. 增量存储

如果需要频繁的创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。

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

# 3.2 结合原型模式

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。

通过上面的示例来说明,只需要修改原发射器对象就可以了,大致有如下变化:

首先原发器对象要实现可克隆的,示例中的状态数据简单,直接用浅拷贝就行,否则需要用深拷贝,关于深浅拷贝,具体可以查看 深浅拷贝

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

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

示例代码如下:

Not found: D:\blog\ccj-blog/docs/views/notes/designMode/file/memento/rewrite/FlowAMockPrototype.java

同样,还可以把备份的对象存储在文件、设备等

# 3.3 再次实现可撤销操作

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

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

1. 范例需求

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

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

大致实现思路如下:

# 3.4 备忘录模式的优缺点

1. 更好的封装性

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

2. 简化了原发器

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

3. 窄接口和宽接口

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

4. 可能会导致高开销

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

# 3.5 思考备忘录模式

  1. 备忘录模式的本质

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

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

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

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

备忘录模式为何要保存数据呢,目的就是为了在有需要的时候,恢复原发器对象的内部状态,所以恢复是备忘录模式的目的。

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

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

2. 何时选用备忘录模式

建议在如下情况中,选用备忘录模式:

如果必须保存一个对象在某一个时刻的全部或者部分状态,这样在以后需要的时候,可以把该对象恢复到先前的状态。可以使用备忘录模式,使用备忘录对象来封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象里面,在需要的时候,再从管理者对象里面获取备忘录对象,来恢复对象的状态。

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

# 3.6 相关模式

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

这两个模式可以组合使用。

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

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

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

这两个模式可以组合使用。

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。

# 四:JDK

# 五:参考文献

最后更新: 10/19/2022, 12:31:23 AM