引荐阅览: 聊聊在生产环境中运用Docker的最佳实践 – 掘金 ()

备忘录形式

亦称: 快照、Snapshot、Memento

目的

备忘录形式是一种行为规划形式, 答应在不露出目标完成细节的情况下保存和康复目标之前的状况。

2023 跟我一起学设计模式:备忘录模式

问题

假如你正在开发一款文字编辑器运用程序。 除了简略的文字编辑功用外, 编辑器中还要有设置文本格式和插入内嵌图片等功用。

后来, 你决定让用户能吊销施加在文本上的任何操作。 这项功用在过去几年里变得十分遍及, 因而用户等待任何程序都有这项功用。 你选择采用直接的办法来完成该功用: 程序在履行任何操作前会记录悉数的目标状况, 并将其保存下来。 当用户尔后需求吊销某个操作时, 程序将从历史记录中获取最近的快照, 然后运用它来康复悉数目标的状况。

2023 跟我一起学设计模式:备忘录模式

程序在履行操作前保存悉数目标的状况快照, 稍后可经过快照将目标康复到之前的状况。

让咱们来思考一下这些状况快照。 首要, 到底该怎么生成一个快照呢? 很可能你会需求遍历目标的悉数成员变量并将其数值仿制保存。 但只有当目标对其内容没有严厉拜访权限约束的情况下, 你才干运用该办法。 不过很遗憾, 绝大部分目标会运用私有成员变量来存储重要数据, 这样别人就无法轻易查看其中的内容。

现在咱们暂时疏忽这个问题, 假定目标都像嬉皮士相同: 喜爱开放式的关系并会揭露其悉数状况。 尽管这种办法能够处理当时问题, 让你可随时生成目标的状况快照, 但这种办法仍存在一些严重问题。 未来你可能会增加或删去一些成员变量。 这听上去很简略, 但需求对负责仿制受影响目标状况的类进行更改。

2023 跟我一起学设计模式:备忘录模式

怎么仿制目标的私有状况?

还有更多问题。 让咱们来考虑编辑器 (Editor) 状况的实践 “快照”, 它需求包含哪些数据? 至少有必要包含实践的文本、 光标坐标和当时滚动条位置等。 你需求收集这些数据并将其放入特定容器中, 才干生成快照。

你很可能会将大量的容器目标存储在历史记录列表中。 这样一来, 容器终究大概率会成为同一个类的目标。 这个类中几乎没有任何办法, 但有许多与编辑器状况一一对应的成员变量。 为了让其他目标能保存或读取快照, 你很可能需求将快照的成员变量设为公有。 无论这些状况是否私有, 其都将露出悉数编辑器状况。 其他类会对快照类的每个小改动发生依靠, 除非这些改动仅存在于私有成员变量或办法中, 而不会影响外部类。

咱们好像走进了一条死胡同: 要么会露出类的悉数内部细节而使其过于脆弱; 要么会约束对其状况的拜访权限而无法生成快照。 那么, 咱们还有其他办法来完成 “吊销” 功用吗?

处理方案

咱们方才遇到的悉数问题都是封装 “破损” 形成的。 一些目标企图超出其责任规模的作业。 由于在履行某些行为时需求获取数据, 所以它们侵入了其他目标的私有空间, 而不是让这些目标来完成实践的作业。

备忘录形式将创立状况快照 (Snapshot) 的作业委派给实践状况的具有者原发器 (Originator) 目标。 这样其他目标就不再需求从 “外部” 仿制编辑器状况了, 编辑器类具有其状况的彻底拜访权, 因而能够自行生成快照。

形式建议将目标状况的副本存储在一个名为备忘录 (Memento) 的特别目标中。 除了创立备忘录的目标外, 任何目标都不能拜访备忘录的内容。 其他目标有必要运用受限接口与备忘录进行交互, 它们能够获取快照的元数据 (创立时间和操作称号等), 但不能获取快照中原始目标的状况。

2023 跟我一起学设计模式:备忘录模式

原发器具有对备忘录的彻底拜访权限, 负责人则只能拜访元数据。

这种约束战略答应你将备忘录保存在一般被称为负责人 (Caretakers) 的目标中。 由于负责人仅经过受限接口与备忘录互动, 故其无法修正存储在备忘录内部的状况。 一起, 原发器具有对备忘录悉数成员的拜访权限, 从而能随时康复其以前的状况。

在文字编辑器的示例中, 咱们能够创立一个独立的历史 (History) 类作为负责人。 编辑器每次履行操作前, 存储在负责人中的备忘录栈都会生长。 你乃至能够在运用的 UI 中烘托该栈, 为用户显示之前的操作历史。

当用户触发吊销操作时, 历史类将从栈中取回最近的备忘录, 并将其传递给编辑器以恳求进行回滚。 由于编辑器具有对备忘录的彻底拜访权限, 因而它能够运用从备忘录中获取的数值来替换本身的状况。

备忘录形式结构

根据嵌套类的完成

该形式的经典完成办法依靠于许多盛行编程语言 (例如 C++、 C# 和 Java) 所支撑的嵌套类。

2023 跟我一起学设计模式:备忘录模式

  1. 原发器 (Originator) 类能够生成本身状况的快照, 也能够在需求时经过快照康复本身状况。

  2. 备忘录 (Memento) 是原发器状况快照的值目标 (value object)。 一般做法是将备忘录设为不可变的, 并经过结构函数一次性传递数据。

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

    负责人经过保存备忘录栈来记录原发器的历史状况。 当原发器需求回溯历史状况时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的康复 (restoration) 办法。

  4. 在该完成办法中, 备忘录类将被嵌套在原发器中。 这样原发器就可拜访备忘录的成员变量和办法, 即便这些办法被声明为私有。 另一方面, 负责人对于备忘录的成员变量和办法的拜访权限十分有限: 它们只能在栈中保存备忘录, 而不能修正其状况。

根据中间接口的完成

另外一种完成办法适用于不支撑嵌套类的编程语言 (没错, 我说的便是 PHP)。

2023 跟我一起学设计模式:备忘录模式

  1. 在没有嵌套类的情况下, 你能够规则负责人仅可经过清晰声明的中间接口与备忘录互动, 该接口仅声明与备忘录元数据相关的办法, 约束其对备忘录成员变量的直接拜访权限。
  2. 另一方面, 原发器能够直接与备忘录目标进行交互, 拜访备忘录类中声明的成员变量和办法。 这种办法的缺陷在于你需求将备忘录的悉数成员变量声明为公有。

封装愈加严厉的完成

假如你不想让其他类有任何机会经过备忘录来拜访原发器的状况, 那么还有另一种可用的完成办法。

2023 跟我一起学设计模式:备忘录模式

  1. 这种完成办法答应存在多种不同类型的原发器和备忘录。 每种原发器都和其相应的备忘录类进行交互。 原发器和备忘录都不会将其状况露出给其他类。
  2. 负责人此刻被清晰禁止修正存储在备忘录中的状况。 但负责人类将独立于原发器, 由于此刻康复办法被定义在了备忘录类中。
  3. 每个备忘录将与创立了本身的原发器连接。 原发器会将自己及状况传递给备忘录的结构函数。 由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能康复其状况。

伪代码

本例结合运用了指令形式与备忘录形式, 可保存杂乱文字编辑器的状况快照, 并能在需求时从快照中康复之前的状况。

2023 跟我一起学设计模式:备忘录模式

保存文字编辑器状况的快照。

指令 (command) 目标将作为负责人, 它们会在履行与指令相关的操作前获取编辑器的备忘录。 当用户企图吊销最近的指令时, 编辑器能够运用保存在指令中的备忘录来将本身回滚到之前的状况。

备忘录类没有声明任何公有的成员变量、 获取器 (getter) 和设置器, 因而没有目标能够修正其内容。 备忘录与创立自己的编辑器相连接, 这使得备忘录能够经过编辑器目标的设置器传递数据, 康复与其相连接的编辑器的状况。 由于备忘录与特定的编辑器目标相连接, 程序能够运用中心化的吊销栈完成对多个独立编辑器窗口的支撑。

// 原发器中包含了一些可能会随时间变化的重要数据。它还定义了在备忘录中保存
// 本身状况的办法,以及从备忘录中康复状况的办法。
class Editor is
    private field text, curX, curY, selectionWidth
    method setText(text) is
        this.text = text
    method setCursor(x, y) is
        this.curX = x
        this.curY = y
    method setSelectionWidth(width) is
        this.selectionWidth = width
    // 在备忘录中保存当时的状况。
    method createSnapshot():Snapshot is
        // 备忘录是不可变的目标;因而原发器会将本身状况作为参数传递给备忘
        // 录的结构函数。
        return new Snapshot(this, text, curX, curY, selectionWidth)
// 备忘录类保存有编辑器的过往状况。
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth
    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = x
        this.curY = y
        this.selectionWidth = selectionWidth
    // 在某一时间,编辑器之前的状况能够运用备忘录目标来康复。
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)
// 指令目标可作为负责人。在这种情况下,指令会在修正原发器状况之前获取一个
// 备忘录。当需求吊销时,它会从备忘录中康复原发器的状况。
class Command is
    private field backup: Snapshot
    method makeBackup() is
        backup = editor.createSnapshot()
    method undo() is
        if (backup != null)
            backup.restore()
    // ……

备忘录形式合适运用场景

当你需求创立目标状况快照来康复其之前的状况时, 能够运用备忘录形式。

备忘录形式答应你仿制目标中的悉数状况 (包括私有成员变量), 并将其独立于目标进行保存。 尽管大部分人由于 “吊销” 这个用例才记得该形式, 但其实它在处理事务 (比如需求在呈现错误时回滚一个操作) 的过程中也必不可少。

当直接拜访目标的成员变量、 获取器或设置器将导致封装被打破时, 能够运用该形式。

备忘录让目标自行负责创立其状况的快照。 任何其他目标都不能读取快照, 这有效地保证了数据的安全性。

完成办法

  1. 确认担任原发器人物的类。 重要的是清晰程序运用的一个原发器中心目标, 还是多个较小的目标。

  2. 创立备忘录类。 逐个声明对应每个原发器成员变量的备忘录成员变量。

  3. 将备忘录类设为不可变。 备忘录只能经过结构函数一次性接收数据。 该类中不能包含设置器。

  4. 假如你所运用的编程语言支撑嵌套类, 则可将备忘录嵌套在原发器中; 假如不支撑, 那么你可从备忘录类中抽取一个空接口, 然后让其他悉数目标经过接口来引证备忘录。 你可在该接口中增加一些元数据操作, 但不能露出原发器的状况。

  5. 在原发器中增加一个创立备忘录的办法。 原发器有必要经过备忘录结构函数的一个或多个实践参数来将本身状况传递给备忘录。

    该办法回来结果的类型有必要是你在上一步中抽取的接口 (假如你已经抽取了)。 实践上, 创立备忘录的办法有必要直接与备忘录类进行交互。

  6. 在原发器类中增加一个用于康复本身状况的办法。 该办法承受备忘录目标作为参数。 假如你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需求将输入目标强制转换为备忘录, 由于原发器需求具有对该目标的彻底拜访权限。

  7. 无论负责人是指令目标、 历史记录或其他彻底不同的东西, 它都有必要要知道何时向原发器恳求新的备忘录、 怎么存储备忘录以及何时运用特定备忘录来对原发器进行康复。

  8. 负责人与原发器之间的连接能够移动到备忘录类中。 在本例中, 每个备忘录都有必要与创立自己的原发器相连接。 康复办法也能够移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或许原发器类供给了足够多的设置器并可对其状况进行重写时, 这种办法才干完成。

备忘录形式优缺陷

  • 你能够在不破坏目标封装情况的前提下创立目标状况快照。

  • 你能够经过让负责人保护原发器状况历史记录来简化原发器代码。

  • 假如客户端过于频繁地创立备忘录, 程序将消耗大量内存。

  • 负责人有必要完整跟踪原发器的生命周期, 这样才干销毁弃用的备忘录。

  • 绝大部分动态编程语言 (例如 PHP、 PythonJavaScript) 不能确保备忘录中的状况不被修正。

代码示例

  • Go 备忘录形式讲解和代码示例 – 掘金 ()