备忘录模式(Memento Pattern)又称为快照模式(Snapshot Pattern)或者令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
在软件系统中,备忘录模式为我们提供了一种“后悔药”的机制,它通过存储系统各个历史状态的快照,使得我们可以在任意时刻将系统回滚到某一个历史状态。
一、备忘录模式的应用场景
我们机会天天都在使用备忘录模式,比如使用Git、SVN提供一种代码版本撤回的功能。还有游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。
备忘录模式适用于以下两个场景:
- 需要保存历史快照的场景;
- 希望在对象之外保存状态,且除了自己其它类对象无法访问状态保存具体内容。
备忘录模式主要包含三种角色:
- 发起人角色(Orgainator):负责创建一个备忘录,记录自身需要保存的状态,具备状态回滚功能;
- 备忘录角色(Memento):用于存储发起人的内部状态,且可以防止发起人以外的对象进行访问;
- 备忘录管理员(Caretaker):负责存储,提供管理备忘录,无法对备忘录内容进行操作和访问。
1.1 利用压栈管理落地备忘录模式
我们在网页上写文章或者博客都使用过富文本编辑器,它会附带草稿箱、撤销等这样的功能。
下面使用代码来实现这样的功能。假设我们需要发布一篇文章,这篇文章的编辑过程需要花很长的时间,编辑的过程中会不停的撤销,保存草稿、修改。首先创建发起人角色编辑器 Editor 类:
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 类:
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 类:
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接口,我们来看它的源码:
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()创建一个消息备忘录。可以看一下实现类:
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留一个备份,以备恢复之用。
三、备忘录模式的优缺点
优点:
- 简化发起人职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
- 提供状态回滚功能。
缺点: 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。