设计模式-行为模式之Memento

更多请移步我的博客

意图

Memento是行为模式的一种,他允许你在不暴露对象内部结构的情况下捕获其内部状态,以便稍后对象可以返回到这个状态。

问题

假设你在写一个文本编辑器。核心逻辑放在Editor主类中。另外一些特性,像文本格式化,内联图像等,放在不同的命令类中。

你决定让用户的操作变得可逆。换句话讲,要增加“撤销”功能。为了实现它,你需要在执行任何操作前保存Editor的状态。之后,如果用户决定还原他的一些操作,程序要从历史中拿出快照并恢复Editor到过去状态。

为了拷贝一个对象的状态,你必须遍历它的字段并拷贝字段的值。但是,一个对象必须要有宽松的访问它内容的方式,可以让其他对象窥视其内部并拷贝他的内容。

虽然这种方式可以简单完成这个任务并且允许每个类都能够生产编辑器的备份,但是这种方式与我们的想法相去甚远。如果你决定重构或者修改Editor类的字段,你就必须同时修改那些和Editor耦合的类。

更近一步。我们考虑下真实的Editor备份。即使最原始的编辑器也必须有一些字段来存储数据,比如真实的文本,游标坐标,当前滚动的位置等。为了创建一个备份,你必须记录所有的值并把它们放到一些容器中。

这个容器中最终放置了一些类的对象。这些类可能有许多编辑器状态的镜像字段并且没有什么方法。为了允许其他对象能够读写数据到备份对象,你肯能需要把字段声明为public。但是,这将导致与无限制的Editor类相同的问题。Editor的变化将会影响其他的类。

看起来这个问题无解了。你要么暴露一个类的内部,使得所有关联的类变得脆弱,要么限制对状态的访问,使得撤销操作难以实现。难道没有其他方法了吗?

解决

上面问题的本质是破坏了封装。当一些对象试图做比他们想象的更多的事情时,往往会发生这种情况。这些对象不是请求其他对象为他们做一些事情,而是入侵了其他对象的私有空间来搜集执行动作需要的数据。

Memento模式把创建状态快照的工作委托给状态拥有者本身,也即是Originator对象。因此,也就不需要其他对象从外部拷贝Editor的状态,Editor本身就可以创建快照,因为它自己可以访问自己的所有状态。

模式提供了一种特殊对象来存储对象状态的快照,叫做Memento。memento的内容除了创建者自己外其他的对象都无法访问。其他对象可以通过限制性接口和menento交流,该接口只允许获取快照的元数据,比如创建时间,标签等。

这些数据保护允许把memento存储在叫做Caretakers的对象中。因为他们只能通过限制性接口访问menento,caretakers无法修改存储在里面的状态。

在编辑器例子中,我们创建一个单独的History类来扮演caretakers。它把memento组织成一个栈,每个操作被执行前它会被压入新的memento。当用户触发撤销操作,History弹出栈顶memento并把它传给Editor,请求回滚。因为Editor可以完全访问memento,它将用memento的状态值覆盖当前变量值。

结构

嵌套类实现

经典的实现方式依赖编程对嵌套类的支持,许多流行的编程语言(C++,C#和Java)都支持该方式。

nestedClassStructure

  1. Originator包含复杂的状态并且你不想把它们暴露出去。它能够自己创建快照,也能够在需要的时候从快照恢复。

  2. Memento是一个值对象,用来扮演Originator状态的快照。最佳实践是将Memento做成不可变的,并且Memento只通过其构造方法接收一次数据。

  3. Caretaker不仅知道何时及为什么要捕捉Originator的状态,也知道什么时候应该恢复状态。

    caretaker能够把originator的状态存储成一个栈。当originator要回到历史时,caretaker拿到栈顶数据并把它传给originator的恢复方法。

  4. 这种实现的下,Memento是Originator的嵌套类,Memento可以访问Originator的变量和方法,即使他们被声明为private。另一方面,Caretaker被限制性访问Memento的字段和方法,这对存储memento很好但是对修改他们的状态不是很好。

中间接口实现

不支持嵌套类语言(PHP)的替代方式。

indermediateInterfaceStructure

  1. 在没有嵌套类的情况下,通过让caretaker使用有限接口和memento协作来限制其对memento的访问。

  2. 另一方面,originator可以直接和memento类协作并访问他的public方法,而caretaker不行。

更严格的封装实现

当你不想给远程的其他类通过Memento来访问Originator状态时,这种方式很有用。

stricterEncapsulationStructure

  1. 这种实现允许有多个类型的originator和memento。每个originator和其对应的memento协作。originator和memento不把他们的状态暴露给任何人。

  2. Caretaker不能间接修改memento存储的状态。甚至,它独立与originator。和originator的关联及恢复方法都移动到memento中。

  3. 每个memento都关联指定的originator。originator把它自己和状态值一起传入到memento的构造方法中。由于具体的memento和originator之间的密切关系,memento可以恢复其originator的状态。

伪代码

在这个例子中,采用Memento和Command模式来存储复杂的文本编辑器对象并在需要时恢复它。

command对象扮演caretaker,在它执行前请求editor创建一个memento。当用户需要回滚操作时,之前command关联的memento就可以反转编辑器的状态。

memento不需要有任何public的字段,set或者get方法。因此没有对象可以修改它的内容。memento和创建它的editor关联在一起,能够随意恢复它的状态。这就允许应用支持多个独立的编辑器窗口。

// Originator class should have a special method, which captures originator's
// state inside a new memento object.
class Editor is
    private field text, cursorX, cursorY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.cursorX = cursorX
        this.cursorY = cursorY

    method setSelectionWidth(width) is
        this.selectionWidth = width

    method createSnapshot(): EditorState is
        // Memento is immutable object; that is why originator passes its state
        // to memento's constructor parameters.
        return new Snapshot(this, text, cursorX, cursorY, selectionWidth)

// Memento stores past state of the editor.
class Snapshot is
    private field editor: Editor
    private field text, cursorX, cursorY, selectionWidth

    constructor Snapshot(editor, text, cursorX, cursorY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.cursorX = cursorX
        this.cursorY = cursorY
        this.selectionWidth = selectionWidth

    // At some point, old editor state can be restored using a memento object.
    method restore() is
        editor.setText(text)
        editor.setCursor(cursorX, cursorY)
        editor.setSelectionWidth(selectionWidth)

// Command object can act as a caretaker. In such case, command gets a memento
// just before it changes the originator's state. When undo is requested, it
// restores originator's state with a memento.
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.saveState()

    method undo() is
        if (backup != null)
            backup.restore()
    // ...

适用性

  • 当你需要制作一些对象的快照以便稍后恢复其状态时。

    Memento模式允许生成对象状态的完整副本,并将其与对象分开存储。虽然这种模式的“撤销”应用已经相当普遍,但在处理交易时也是不可或缺的(如果你需要在出错时会滚一个操作)。

  • 当直接访问对象的字段/setter/getter违反了其封装时。

    Memento让对象自己有能力创建自己状态的快照。其他对象不能读取这个快照,使得这个对象的状态变得安全可靠。

如何实现

  1. 确定那个类扮演Originator角色。知道程序使用这种类型的一个还是多个中心对象是很重要的(?)。

  2. 创建memento类。挨个声明Originator类需要镜像的字段。

  3. 让Memento对象不可变。他们应该通过构造方法一次性初始化字段的值。Memento类不应该有setter方法。

  4. 如果你的编程语言支持嵌套类,把Memento当作Originator的内部类。

    如果不支持,抽象出一个memento类的一个接口,并让其他要和Memento关联的对象使用这个接口。你应该在接口中添加一些操作元数据的方法,但不要暴露originator的状态。

  5. 为Originator类添加创建memento的方法。Originator应该通过传递向memento构造方法传递它属性值的方式来创建一个新的memento实例。

    这个方法的返回类型应当是之前抽象出来的接口类型(如果你提取了的话)。但是在这个方法内部,你要和具体的memento类型协作。

  6. 为Originator类增加恢复状态的方法。这个方法把memento对象当作参数之一。按照与上一步相同的逻辑分配参数类型。

  7. 对caretaker而言,不管是操作历史,命令对象,或者其他不同的实体,它都应当知道何时向originator请求一个新的memento,如何存储它以及恢复它。

  8. caretaker和originator的关联关系可以移动到memento中。在这种情况下,每个memento都有与之对应的originator。它将有责任恢复originator的状态。但这仅当memento是originator的内部类或者originator提供了相应的复写其状态的setter方法时。

优点

  • 没有破坏originator的封装。

  • 通过允许caretaker来维护originator历史状态的方式,简化了originator的代码。

缺点

  • 如果客户端频繁创建memento会浪费许多RAM。

  • caretaker需要追踪originator的生命周期以便清理过时的memento。

  • 大多数动态编程语言,像PHP,Python或者JavaScript,不能保证memento中的状态是不变的。

和其他模式的关系

  • Command和Memento可以一起使用。他们可以充当魔法token,在稍后时间被传递和调用。在Command中,token代表一个请求;在Memento中,token代表一个对象在特定时间的内部状态。多态对Command很重要,但对Memento不重要,因为它的接口很狭隘所以只能被当作值传递。

  • Memento可以和Iterator一起只用来捕捉当前迭代的状态,在有必要的时候进行回滚。

  • 如果一个对象想在历史中保存状态相对简单(没有外部资源的链接,或者这些链接很容易重新建立),Prototype可以作为Memento更简单的替代方案。

参考

翻译整理自:https://refactoring.guru/design-patterns/memento

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值