Memento(备忘录)模式

xiaoxiao2026-04-14  6

有时候,我们需要创建的对象曾经在系统中出现过。这种情形可能出现在当我们希望支持用户执行撤销操作回退到以前某个状态的时候,以及恢复到老版本状态或者支持用户继续执行之前挂起的任务的时候。

 

   Memento模式的意图在于为对象提供状态存储和状态恢复功能

 1.经典范例:使用特备忘录实现撤销操作:

  讲抽象工厂模式的时候,介绍了一个图形化应用程序,允许用户使用材料进行操作建模试验。假设Undo(撤销)按钮尚未实现。我们可以使用Memento模式实现这个按钮的功能。

  备忘录是存储状态信息的一个对象。在可视化应用程序中,我们需要保存的状态是应用程序的状态。无论何时添加或者移动机器,都可以单击Undo按钮来撤销操作。我们需要决定如何捕获应用程序的状态。我们需要了解何时捕获这种状态,以及如何把应用程序恢复到以前保存的状态。当应用程序启动后,会出现如下界面:

 图1:当可视化应用程序启动后,工作面板是空的,Undo按钮为禁用状态

 

  最初启动时的空白状态,也是一种状态,只要可视化应用程序处于这个状态,Undo按钮都是被禁用的。在执行添加和拖动等操作之后,可视化应用程序也会变成下图:

图2:用户可以添加和重新布置可视化应用程序中的机器

 

   在这个应用程序中,需要在备忘录中保存的状态包括机器所在的位置信息。我们可以使用栈来保存备忘录,每当用户单击Undo按钮时,都会弹出一个备忘录。

 

   (1)每当用户在图形用户界面上添加或移动机器的时候,应用程序就为仿真工厂创建一个备忘录,并把它加入一个栈中。

   (2)每当用户单击撤销按钮的时候,应用程序就将栈顶部的备忘录弹出,然后将仿真对象恢复为该备忘录所记录的状态。

  

   当可视化应用程序启动之后,首先向空栈中压入一个初始的空备忘录,并且保证绝不会将该备忘录从栈中弹出,从而确保该栈的顶部总是有一个有效的备忘录。当栈中仅含有一个备忘录的时候,应用程序将禁用Undo按钮。

 

   我们也许是在单个类中编写这个程序的代码,但期望能够添加支持建模和其他用户需求的功能。最后,应用程序可能会变得比较庞大,所以使用MVC设计是比较明智的。下图说明了如何把工厂建模功能迁移到单独的类中。

 

图3:设计思路把应用程序分为多个独立的类,

 

 

分别支持工厂建模、提供GUI元素,以及处理用户单击操作 

 

 

  这种设计思路可以让开发者首先集中开发没有GUI控件和不依赖GUI的FactoryModel类。

  FactoryModel类是我们设计思路的核心。该类负责维护机器的当前位置,以及维护以前配置的备忘录

  每次客户要求工厂添加或者移动一个机器时,工厂都会创建一个副本----当前机器的备忘录,并把其压入备忘录栈中。在本例中,我们不需要特殊的Memento类。每个元素只是点的列表:在特定时间机器位置的列表。

 

  工厂模型必须提供有关事件,支持客户关注对工厂状态变化的兴趣。应用程序GUI可以把用户所作变化通知状态模型。假设你希望工厂允许客户注册添加机器和拖动机器等事件。下图给出了FactoryModel类的设计。

图4:FactoryModel类维护工厂配置的栈,并允许客户记录变更

  上图的设计需要FactoryModel类为客户提供注册多个事件的能力,比如添加机器事件。任何已经注册的ChangeListener对象都会接收到这种变更。

 

package com.oozinoz.visualization; //... public class FactoryModel { private Stack mementos; private ArrayList listeners = new ArrayList(); public FactoryModel() { mementos = new Stack(); mementos.push(new ArrayList()); } //... }

 构造器最初将工厂的初始配置设置为一个空白列表。类中其他方法维护机器配置备忘录的栈,并在变更出现时触发设定的事件。比如,为了把某机器添加到当前配置,客户可以调用如下方法:

 

public void add(Point location) { List oldLocs = (List)mementos.peek(); List newLocs = new ArrayList(oldLocs); newLocs.add(0,location); mementos.push(newLocs); notifyListener(); }

 

  上述代码创建机器位置的新列表,并把这个列表压入工厂模型维护的备忘录栈中。稍微不同的是这部分代码确保这个新机器位置位于列表第一位。因为工厂平面图可能相互重叠,所以,这个新机器的位置信息应该出现在其他机器信息的前面。

    

   在接收到工厂模型的任何事件时,注册变化通知的客户也许可以通过完全重绘来更新工厂模型的视图。工厂模型的最新配置始终适用于getLocations(),其代码如下所示:

public List getLocation() { return (List)mementos.peek(); }

factoryModel类的undo()方法允许客户把机器位置模型恢复为以前的版本。当这些代码执行时,它也会调用notifyListeners()方法。

 

突破题:请编写FactoryModel类undo()方法的代码。

答:代码如下:

 

public boolean canUndo() { return mementos.size()>1; } public void undo() { if(!canUndo()) return; mementos.pop(); notifyListeners(); }

     如果栈恢复到最初状态,只有一个备忘录了,那么上述代码会忽略undo()请求。栈的最顶层始终是当前状态,undo()代码的行为仅仅是从栈弹出顶层的备忘录。

     当编写createMemento()方法时,应该保证该方法返回重新构造接收对象所需的全部信息。在本例中,一个机器模拟器可以从克隆体重新构造自己;工厂模拟器可以从机器模拟器克隆体列表重新构造自己。

 

  通过注册为监听者,可提供重绘客户的工厂视图的方法,这样感兴趣的客户可以提供撤销操作。Visualization类就是这样的一个客户。

 

  图3的MVC设计思路能够把转换用户动作的任务与维护GUI的任务相分离。Visualization类创建GUI控件,但不处理传递给中介者的GUI事件。VisMediator类把GUI事件实现为工厂模型中对应的变化。当工厂模型变化时,GUI也许也需要更新。Visualization类注册FactoryModel类提供的通知消息。请注意职责划分

(1)Visualization类把工厂事件转换为GUI变化

(2)VisMediator类把GUI事件转换为工厂变化

 

图5更加清晰地显示三个类之间的协作关系:

图5:VisMediator把GUI事件转换为工厂模型变化,Visualization类处理工厂事件以更新GUI

 

  假设当用户拖动某机器图片时,错误地将其放到其他位置,然后单击Undo按钮。为了处理这次单击任务,Visualization把按钮事件通知注册给中介者。Visualization类中Undo按钮的代码如下所示:

protected JButton undoButton() { if(undoButton == null) { undoButton = ui.createButtonCancel(); undoButton.setText("Undo"); undoButton.setEnabled(false); undoButton.addActionListener(mediator.undoAction()); } return undoButton; }

这段代码将处理单击操作的职责传递给中介者,中介者再通知任何请求的变化的工厂模型。中介者可以用下列代码将撤销请求转化为工厂变化:

private void undo(ActionEvent e) { factoryModel.undo(); }

本方法中,factoryModel变量是Visualization类创建的factoryModel的实例,该变量把工厂模型传入VisMediator类的构造器。我们已经检查FactoryModel类的pop()命令。图6将显示当用户单击Undo按钮后消息的流动过程。

图6:当用户单击Undo按钮后消息的流动过程

 

  当FactoryModel类弹出其当前配置时,以前存储为备忘录的配置会显露出来,undo()方法会通知任何ChangeListeners。Visualization类在其构造器中完成注册,代码如下:

public Visualization(UI ui) { super(new BorderLayout()); this.ui = ui; mediator = new VisMediator(factoryModel); factoryModel.addChangeListener(this); add(machinePanel(),BorderLayout.CENTER); add(buttonPanel(),BorderLayout.SOUTH); }

对于工厂模型中每个机器的位置,图形化程序都维护一个Component对象,这个对象是使用createPictureBox()方法创建的。stateChanged()方法必须从机器面板清除所有图片框控件,并在工厂模型当前位置集合重新添加新图片框。如果栈中只有一个备忘录,stateChanged()方法必须禁用Undo按钮。

 

突破题:请写出Visualization类的stateChanged()方法。

答:代码如下:、

public void stateChanged(ChangeEvent e) { machinePanel().removeAll(); List locations = factoryModel.getLocations(); for(int i=0;i<locations.size();i++) { Point p = (Point)locations.get(i); machinePanel().add(createPictureBox(p)); } undoButton().setEnabled(factoryModel.canUndo()); repaint(); }

借助于备忘录模式,你可以存储和恢复对象的状态。Mementos模式最常见的用法是在应用程序中提供提供撤销操作。在有些应用程序中,比如工厂图形化程序,存储信息的仓库可能是其它对象。在有些情况下,你也许需要持久保存备忘录。

 

2.备忘录的持久性: 

  备忘录是一个小型的数据仓库,用来存储对象的状态。我们通常可以用另一个对象、字符串或者文件来创建备忘录。在设计备忘录的时候,应当事先预测对象状态可能在备忘录中存储的时候,因为这个时间将影响我们的设计策略。这个时间可能是一会儿、几个小时、几天或者几年。

 

突破题:有时候,我们会将一个备忘录存放在一个文件中,而不是存放在一个对象中,请给出这样做的两个理由。

答:将备忘录作为对象来保存,必须保证,当用户需要恢复最初对象的时候,这个应用程序仍在运行机制。而在下列情况下,我们必须把备忘录保存在持久性存储器中:

(1)我们希望在系统崩溃之后仍然能够恢复某个对象的状态。

(2)用户在某个时候可能会暂停某项任务的执行,退出系统,用户需要在将来某个时候继续执行该项任务。

(3)我们需要在另外一台计算机上重新构造该对象。

 

3.跨越会话的持久性备忘录:

 用户运行一个程序,执行程序中的事务,最后退出,这个过程被称为一次会话(session)。假如用户期望能够保存某次会话中的工厂仿真对象,然后在另一次会话中恢复该对象。这种功能通常被称作持久性存储(persistent storage)。Memento模式正好可以帮助实现这种功能。持久性存储可以说是前面的撤销操作的一个自然扩展。

 假设由Visualization类派生出一个Visualization2类,该类具有一个菜单栏,并含有一个File(文件)菜单,其中提供save as(另存为)...和restore from(恢复为...)...命令。这两个命令分别对应代码中的两个方法:

package com.oozinoz.visualization; import javax.swing.*; import com.oozinoz.ui.SwingFacade; import com.oozinoz.ui.UI; public class Visualization2 extends Visualization { public Visualizationi2(UI ui) { super(ui); } public JMenuBar menus() { JMenubar menuBar = new JMenuBar(); JMenu menu = new JMenu("file"); menuBar.add(menu); JMenuItem menuItem = new JMenuItem("Save As..."); menuItem.addActionListener(mediator.saveAction()); menu.add(menuItem); menuItem = new JMenuItem("Restore From..."); menuItem.addActionListener(mediator.restoreAction()); menu.add(menuItem); return menuBar; } public static void main(String[] args) { Visualization2 panel = new Visualization2(UI.NORMAL); JFrame frame = SwingFacade.launch(panel,"Operational Model"); frame.setJMenuBar(panel.menus()); frame.setVisible(true); } }

为了完全实现上述代码,VisMediator类需要添加saveActioin()和restoreAction()方法。当用户选择菜单项时,MenuItem对象会导致这些动作被调用。当Visualization2类运行时,GUI如图7所示:

新增的File菜单使得用户可保存当前机器位置,以便于后期恢复

 

  存储对象(比如工厂模型的机器配置)和简易方式是串行化。VisMediator类的saveAction()方法程序代码如下所示:

public ActionListener saveAction() { return new ActionListener() { public void actionPerformed(ActionEvent e) { try { VisMediator.this.save((Component)e.getSource()); } catch(Exception ex) { System.out.println("Failed save:"+ex.getMessage()); } } } } public void save(Component source) throws Exception { JFileChooser dialog = new JFileChooser(); dialog.showSaveDialog(source); if(dialog.getSelectedFile()==null) return; FileOutputStream out = null; ObjectOutputStream s = null; try{ out = new FileOutputStream(dialog.getSelectedFile()); s = new ObjectOutputStream(out); s.writeObject(factoryModel.getLocation()); }finally{ if(s!=null) s.close(); }

 

突破题:请补充VisMediator类中restoreAction()方法的代码。

答:代码如下:

 

public ActionListener restoreAction() { return new ActionListener(){ public void actionPerformed(ActionEvent e){ try{ VisMediator.this.restore((Component)e.getSource()); }catch(Exception ex){ System.out.println(""); } } }; } public void restore(Component source) throws Exception{ JFileChooser dialog = new JFileChooser(); dialog.showSaveDialog(source); if(dialog.getSelectedFile()==null) return; fileInputStream in = null; ObjectInputStream s = null; try { in = new FileInputStream(dialog.getSelectedFile()); s = new ObjectOutputStream(in); ArrayList list = (ArrayList)s.readObject(); //可能读入多个对象 factoryModel.setLocations(list); }finally{ if(s!=null) s.close(); } }

这部分代码几乎是saveAction()及save()方法的影子,尽管restore()方法必须让工厂模型推进被恢复的位置列表。

 

《设计模式〉一书对Memento模式的目标陈述如下:在不违反封装前提下,捕获和外化对象的内部状态,以便于对象可以在以后进行状态恢复。

 

突破题:在这种情况下,我们可以使用Java串行机制把备忘录写入二进制文件中。假设我们把信息以XML格式(文本文件)保存起来。请自己思考,并简单陈述把备忘录保存为文本格式是否会违反封装性。

答:封装会限制对对象状态和操作的访问。以文本方式来保存对象数据,诸如工厂位置点的集合,会导致任何人使用文本编辑器就可以更改对象的状态。因此以XML形式保存对象在某种程序上是违反封装原则的。

     在持久性存储器中保存对象,违反封装原则的问题还需要实践进一步验证。因为具体危害都与具体应用环境有关。为了消除这种威胁,也许会限制对数据的访问,比如限制对关系型数据库的访问。同时,你也许会使用加密技术,这在传输敏感的HTML文本时很常见。这个问题的重点不是音讯封装和备忘录是否在模式中使用,而是在进行数据存储和传输时,如何确保数据的完整性。

 

  当开发者说希望借助于串行化或者XML文件来保存和恢复对象的数据时,你应该确切理解其语言中的含义。这就涉及到设计模式中的重点。必须使用公共词汇,这样我们才能切实讨论设计模式概念及其应用。

 

4.小结:

  备忘录模式可以帮助我们获取对象的状态,以便于将来把对象恢复为以前状态。对象状态的存储方法很多,具体采用哪种方法,要取决于对象状态需保存的时间。有些情况下,只要单击几下鼠标和键盘之后,就需要恢复一个对象;而另一些情况下,有可能要经过几天或者几年才需要恢复一个对象。在应用程序会话期间,保存和恢复对象最常见的理由是支持撤销操作。在这种情况下,我们可以将对象的状态存储在另一个对象中。为了支持对象跨多个会话的持久性存储,可以使用对象串行化或者其他方式来保存备忘录。

转载请注明原文地址: https://www.6miu.com/read-5047368.html

最新回复(0)