架构师内功心法,参与富文本编辑器开发的备忘录模式详解

备忘录模式(Memento Pattern)又称为快照模式(Snapshot Pattern)或者令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

在软件系统中,备忘录模式为我们提供了一种“后悔药”的机制,它通过存储系统各个历史状态的快照,使得我们可以在任意时刻将系统回滚到某一个历史状态。

一、备忘录模式的应用场景

我们机会天天都在使用备忘录模式,比如使用Git、SVN提供一种代码版本撤回的功能。还有游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。


备忘录模式适用于以下两个场景:

  • 需要保存历史快照的场景;
  • 希望在对象之外保存状态,且除了自己其它类对象无法访问状态保存具体内容。

备忘录模式主要包含三种角色:

  • 发起人角色(Orgainator):负责创建一个备忘录,记录自身需要保存的状态,具备状态回滚功能;
  • 备忘录角色(Memento):用于存储发起人的内部状态,且可以防止发起人以外的对象进行访问;
  • 备忘录管理员(Caretaker):负责存储,提供管理备忘录,无法对备忘录内容进行操作和访问。

1.1 利用压栈管理落地备忘录模式

我们在网页上写文章或者博客都使用过富文本编辑器,它会附带草稿箱、撤销等这样的功能。

下面使用代码来实现这样的功能。假设我们需要发布一篇文章,这篇文章的编辑过程需要花很长的时间,编辑的过程中会不停的撤销,保存草稿、修改。首先创建发起人角色编辑器 Editor 类:

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
public class Editor {

private String title;

private String content;

private String imgs;

public Editor(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}

public ArticleMemento save2Memento() {
ArticleMemento articleMemento =
new ArticleMemento(this.title, this.content, this.imgs);
return articleMemento;
}

public void undoFromMemento(ArticleMemento articleMemento) {
this.title = articleMemento.getTitle();
this.content = articleMemento.getContent();
this.imgs = articleMemento.getImgs();
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getImgs() {
return imgs;
}

public void setImgs(String imgs) {
this.imgs = imgs;
}

@Override
public String toString() {
return "Editor{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}

然后创建备忘录角色 ArticleMemento 类:

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
public class ArticleMemento {

private String title;

private String content;

private String imgs;

public ArticleMemento(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getImgs() {
return imgs;
}

public void setImgs(String imgs) {
this.imgs = imgs;
}

@Override
public String toString() {
return "ArticleMemento{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}

创建备忘录管理角色草稿箱 DraftBox 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DraftBox {

private final Stack<ArticleMemento> STACK = new Stack<>();

public ArticleMemento getMemento() {
ArticleMemento articleMemento = STACK.pop();
return articleMemento;
}

public void addMemento(ArticleMemento articleMemento) {
STACK.push(articleMemento);
}

}

草稿箱的Stack类是Vector的一个子类,它实现了一个标准的后进先出的栈。

二、备忘录模式在源码中的体现

备忘录模式在框架源码中的应用还是比较少见的,主要还是结合具体的应用场景来使用。spring中的webfolw源码StateManageableMessageContext接口,我们来看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface StateManageableMessageContext extends MessageContext {

/**
* Create a serializable memento, or token representing a snapshot of the internal state of this message context.
* @return the messages memento
*/
public Serializable createMessagesMemento();

/**
* Set the state of this context from the memento provided. After this call, the messages in this context will match
* what is encapsulated inside the memento. Any previous state will be overridden.
* @param messagesMemento the messages memento
*/
public void restoreMessages(Serializable messagesMemento);

/**
* Configure the message source used to resolve messages added to this context. May be set at any time to change how
* coded messages are resolved.
* @param messageSource the message source
* @see MessageContext#addMessage(MessageResolver)
*/
public void setMessageSource(MessageSource messageSource);
}

createMessagesMemento()创建一个消息备忘录。可以看一下实现类:

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
public class DefaultMessageContext implements StateManageableMessageContext {

private static final Log logger = LogFactory.getLog(DefaultMessageContext.class);

private MessageSource messageSource;

@SuppressWarnings("serial")
private Map<Object, List<Message>> sourceMessages = new AbstractCachingMapDecorator<Object, List<Message>>(
new LinkedHashMap<Object, List<Message>>()) {

protected List<Message> create(Object source) {
return new ArrayList<Message>();
}
};

/**
* Creates a new default message context. Defaults to a message source that simply resolves default text and cannot
* resolve localized message codes.
*/
public DefaultMessageContext() {
init(null);
}

/**
* Creates a new default message context.
* @param messageSource the message source to resolve messages added to this context
*/
public DefaultMessageContext(MessageSource messageSource) {
init(messageSource);
}

public MessageSource getMessageSource() {
return messageSource;
}

// implementing message context

public Message[] getAllMessages() {
List<Message> messages = new ArrayList<Message>();
for (List<Message> list : sourceMessages.values()) {
messages.addAll(list);
}
return messages.toArray(new Message[messages.size()]);
}

public Message[] getMessagesBySource(Object source) {
List<Message> messages = sourceMessages.get(source);
return messages.toArray(new Message[messages.size()]);
}

public Message[] getMessagesByCriteria(MessageCriteria criteria) {
List<Message> messages = new ArrayList<Message>();
for (List<Message> sourceMessages : this.sourceMessages.values()) {
for (Message message : sourceMessages) {
if (criteria.test(message)) {
messages.add(message);
}
}
}
return messages.toArray(new Message[messages.size()]);
}

public boolean hasErrorMessages() {
for (List<Message> sourceMessages : this.sourceMessages.values()) {
for (Message message : sourceMessages) {
if (message.getSeverity() == Severity.ERROR) {
return true;
}
}
}
return false;
}

public void addMessage(MessageResolver messageResolver) {
Locale currentLocale = LocaleContextHolder.getLocale();
if (logger.isDebugEnabled()) {
logger.debug("Resolving message using " + messageResolver);
}
Message message = messageResolver.resolveMessage(messageSource, currentLocale);
List<Message> messages = sourceMessages.get(message.getSource());
if (logger.isDebugEnabled()) {
logger.debug("Adding resolved message " + message);
}
messages.add(message);
}

public void clearMessages() {
sourceMessages.clear();
}

// implementing state manageable message context

public Serializable createMessagesMemento() {
return new LinkedHashMap<Object, List<Message>>(sourceMessages);
}

@SuppressWarnings("unchecked")
public void restoreMessages(Serializable messagesMemento) {
sourceMessages.putAll((Map<Object, List<Message>>) messagesMemento);
}

public void setMessageSource(MessageSource messageSource) {
if (messageSource == null) {
messageSource = new DefaultTextFallbackMessageSource();
}
this.messageSource = messageSource;
}

// internal helpers

private void init(MessageSource messageSource) {
setMessageSource(messageSource);
// create the 'null' source message list eagerly to ensure global messages are indexed first
this.sourceMessages.get(null);
}

public String toString() {
return new ToStringCreator(this).append("sourceMessages", sourceMessages).toString();
}

private static class DefaultTextFallbackMessageSource extends AbstractMessageSource {
protected MessageFormat resolveCode(String code, Locale locale) {
return null;
}
}
}

主要逻辑就相当于是给Message留一个备份,以备恢复之用。

三、备忘录模式的优缺点

优点:

  • 简化发起人职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
  • 提供状态回滚功能。

缺点:
消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。