0%

初学 Java 设计模式(十八):实战备忘录模式 「线上刚发布就出问题怎么办?」

一、备忘录模式介绍

1. 解决的问题

主要解决在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,从而可以恢复原先保存的状态。

2. 定义

备忘录模式是一种行为设计模式,允许在不暴露对象实现细节的状态下保存和恢复对象之前的状态。

3. 应用场景

  • 当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
  • 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用备忘录模式。

二、备忘录模式优缺点

1. 优点

  • 可以在不破坏对象封装情况的前提下创建对象状态快照。
  • 可以通过让负责人维护原发器状态历史记录来简化原发器代码。

2. 缺点

  • 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
  • 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
  • 绝大部分动态编程语言(例如PHP、Python 和 JavaScript)不能确保备忘录中的状态不被修改。

三、备忘录模式应用实例:线上刚发布就出问题怎么办?

1. 实例场景

搬砖的我们经常会遇到这个情况:

老代码出了 bug ,先看文档手册,我去,这代码连个文档都没有,只能看源码了。

看完一遍逻辑,再调试个半天,应该是这里出问题了,快速修改完毕,验证一遍,nice,没什么问题。

开始联系测试验证,测试环境验证通过!

激动人心的时刻来了:提交代码合并请求 -> 编译 -> 发布。

哎,怎么回事,线上发布的容器疯狂报错,起都起不来,我的天,怎么办?

旁边的同事:别慌,先把发布回滚吧,保证线上没问题!

今天,我们就以发布回滚为例,介绍一下备忘录模式。

2. 备忘录模式实现

2.1 工程结构
1
2
3
4
5
6
7
8
9
10
11
12
13
└─ src
├─ main
│ └─ java
│ └─ org.design.pattern.memento
│ └─ model
│ ├─ ReleaseRecord.java
│ ├─ ReleaseRecordOriginator.java
│ ├─ ReleaseRecordMemento.java
│ └─ ReleaseRecordCaretaker.javaa
└─ test
└─ java
└─ org.design.pattern.memento
└─ MementoTest.java
2.2 代码实现
2.2.1 实体类

发布记录

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
/**
* 发布记录
*/
@Getter
@Setter
@AllArgsConstructor
public class ReleaseRecord {

/**
* 发布记录号
*/
private Integer id;

/**
* 发布版本
*/
private String version;

/**
* 发布容器
*/
private String cloudName;

/**
* 开发
*/
private String developerName;

/**
* 测试
*/
private String testerName;
}
2.2.2 备忘录模式相关类

发布记录原生器

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
/**
* 发布记录原生器
*/
@Getter
@Setter
public class ReleaseRecordOriginator {

/**
* 发布记录
*/
private ReleaseRecord releaseRecord;

/**
* 构造函数
*
* @param releaseRecord 发布记录
*/
public ReleaseRecordOriginator(ReleaseRecord releaseRecord) {
this.releaseRecord = releaseRecord;
}

/**
* 保存发布记录
*
* @return ReleaseRecordMemento
*/
public ReleaseRecordMemento save() {
return new ReleaseRecordMemento(this.releaseRecord);
}

/**
* 恢复发布记录
*
* @param releaseRecordMemento 发布记录备忘录
*/
public void restore(ReleaseRecordMemento releaseRecordMemento) {
this.releaseRecord = releaseRecordMemento.getReleaseRecord();
}
}

发布记录备忘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 发布记录备忘录
*/
@Getter
@Setter
public class ReleaseRecordMemento {

/**
* 发布记录
*/
private ReleaseRecord releaseRecord;

/**
* 构造函数
*
* @param releaseRecord 发布记录
*/
public ReleaseRecordMemento(ReleaseRecord releaseRecord) {
this.releaseRecord = releaseRecord;
}
}

发布记录负责人

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
/**
* 发布记录负责人
*/
public class ReleaseRecordCaretaker {

/**
* 发布记录历史记录列表
*/
private List<ReleaseRecordMemento> mementoList = new ArrayList<>();

/**
* 发布历史记录map(可根据发布记录id查询)
*/
private Map<Integer, ReleaseRecordMemento> mementoMap = new HashMap<>();

/**
* 存放发布记录
*
* @param releaseRecordMemento 发布记录备忘录
*/
public void push(ReleaseRecordMemento releaseRecordMemento) {
mementoList.add(releaseRecordMemento);
mementoMap.put(releaseRecordMemento.getReleaseRecord().getId(), releaseRecordMemento);
}

/**
* 回滚
*
* @return ReleaseRecordMemento
*/
public ReleaseRecordMemento undo() {
if (ObjectUtils.isEmpty(mementoList)) {
return null;
}
return mementoList.get(mementoList.size() - 1);
}

/**
* 根据发布记录id回滚
*
* @param releaseRecordId 发布记录id
* @return ReleaseRecordMemento
*/
public ReleaseRecordMemento undo(Integer releaseRecordId) {
if (ObjectUtils.isEmpty(mementoMap) || ObjectUtils.isEmpty(mementoMap.get(releaseRecordId))) {
return null;
}
return mementoMap.get(releaseRecordId);
}
}
2.3 测试验证
2.3.1 测试验证类
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
/***
* 备忘录模式测试
*/
@Slf4j
public class MementoTest {

@Test
public void test() {
ReleaseRecordCaretaker recordCaretaker = new ReleaseRecordCaretaker();

ReleaseRecordOriginator recordOriginator = new ReleaseRecordOriginator(
new ReleaseRecord(1, "0.1.0", "test-cloud", "yiyu", "tester")
);
recordCaretaker.push(recordOriginator.save());

recordOriginator.setReleaseRecord(
new ReleaseRecord(2, "0.1.1", "test-cloud", "yiyu", "tester")
);
recordCaretaker.push(recordOriginator.save());

// 回滚
recordOriginator.restore(recordCaretaker.undo());
log.info("历史发布回滚:{}", JSON.toJSONString(recordOriginator.getReleaseRecord()));

// 根据发布id回滚
Integer undoReleaseRecordId = 1;
recordOriginator.restore(recordCaretaker.undo(undoReleaseRecordId));
log.info("回滚发布记录id为{}的发布:{}", undoReleaseRecordId, JSON.toJSONString(recordOriginator.getReleaseRecord()));
}
}
2.3.2 测试结果
1
2
3
4
22:36:44.731 [main] INFO  o.d.pattern.memento.test.MementoTest - 历史发布回滚:{"cloudName":"test-cloud","developerName":"yiyu","id":2,"testerName":"tester","version":"0.1.1"}
22:36:44.733 [main] INFO o.d.pattern.memento.test.MementoTest - 回滚发布记录id为1的发布:{"cloudName":"test-cloud","developerName":"yiyu","id":1,"testerName":"tester","version":"0.1.0"}

Process finished with exit code 0

四、备忘录模式结构

备忘录模式-模式结构图

  1. 原发器(Originator)类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。

  2. 备忘录(Memento)是原发器状态快照的值对象(value object)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。

  3. 负责人(Caretaker)仅知道 “何时” 和 “为何” 捕捉原发器的状态,以及何时恢复状态。

    负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将栈中获取最顶部的备忘录,并将其传递给原发器的恢复(restoration)方法。

  4. 在实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可以访问备忘录的成员变量和方法,即使这些方法被声明为私有。另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能栈中保存备忘录,而不能修改其状态。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

源码地址:https://github.com/yiyufxst/design-pattern-java

参考资料:
小博哥重学设计模式:https://github.com/fuzhengwei/itstack-demo-design
深入设计模式:[https://refactoringguru.cn/design-patterns/catalog](