备忘录模式(Memento Pattern): 一个需要注意方法权限的模式

  1. 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》

设计模式用前须知

  • 设计模式中一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

备忘录模式常见错误实现

了解过备忘录模式的 Java 同学可以先看一下如下备忘录模式的 Mementor 实现举例, 是否能发现其中的明显错误。 如果不能, 则更有必要阅读本文

  • Originator
public class Memento {
   private String state;

   public Memento(String state){
      this.state = state;
   }

   public String getState(){
      return state;
   }    
}

备忘录模式(Memento Pattern)

  • 设计意图

    • GoF: 在不违反封装的情况下, 在不破坏封装的前提下, 捕获一个对象的内部状态,并在该对象之外保存这个状态, 使得对象在必要时可以恢复到原先保存的状态。
  • GoF 举例

    • 考虑一个支持连接图形对象的图形编辑器, 一个用户可以把两个长方形通过一条线连接起来, 当用户移动任意一个长方形时, 编辑器会拉伸连接线, 保持两个长方形的连接状态。
      这里写图片描述
    • 一个著名的解决对象连接关系的方法是使用约束求解系统。 我们可以把这个功能封装在一个 ConstraintSolver 对象中。 ConstraintSolver 会在连接建立时记录连接关系, 生成描述这些连接关系的数学等式, 每当用户建立一个连接或修改图像时, ConstraintSolver 会求解这些等式。 ConstraintSolver 会使用计算结果来对图形进行重排, 使得他们保持适当的连接关系。 (可以简单地将 ConstraintSolver 看做一个帮助你解决对象移动时, 连接关系应该如何处理的黑盒组件
    • 在这样的一个系统中支持撤销操作并不像想象的那么容易。 撤销一次移动的操作 ( 如下图) , 最容易想到的方法是把对象的移动距离存储下来, 撤销时将对象向相反的方向移动相同的距离。 然而, 这种方法并不能保证所有的对象都出现在它们移动前所在的位置。 正如下图, 移动会导致连接线被拉长, 简单地把每个图形对象移动回原来的位置并不能达到我们想要的效果。
    • 从宽泛的角度来看, ConstraintSolver 的公共接口可能并不允许精确地撤销它对其他对象的影响。撤销机制必须和 ConstraintSolver 更加紧密地搭配在一起使用才能把多个对象恢复到上一步的状态。然而, 我们同时又不希望把 ConstraintSolver 的内部机制暴露给撤销机制, 这样会增加撤销机制的复杂度, 也不利于解耦。
  • 解决方案:

    • 备忘录模式( Memento Pattern) 中, 一个 memento 是一个可以存储另一个对象( 称为 originator)内部状态快照的对象。 撤销操作机制工作时, 当需要保存一个操作前 originator 的状态时, 会向 originator 发送请求, 获得一个 memento 对象。 originator 会创建并初始化一个 memento, 里面填入包含 originator 当前状态的信息。 注意, 只有 originator 可以向 memento 对象中存储和提取信息! memento 对象对于其他的对象来说是不透明的。
      • memento 对象的访问权限非常重要, 如果 memento 对象的内容可以被任意对象修改, 会很容易使得状态信息出错,导致 originator 无法恢复。
    • 对应到上文中的图形编辑器的例子, ConstraintSolver 就是一个 originator 。 撤销移动操作的过程可以通过如下的方式完成 :

      1. 图形编辑器 editor 向 ConstraintSolver 发起获得 memento 对象的请求。
      2. ConstriantSolver 创建并返回一个 memento 对象(这里可以是一个类名为 SolverState 的实例对象) 。 一个 SolverState 对象包含了用于描述 ConstraintSolver 当前状态信息的数据结构, 记录了 ConstraintSolver 内部的等式和变量。
      3. 当用户发起撤销移动操作(undo move)的请求时 , editor 可以把之前获得的 SolverState 返回给 ConstraintSolver。
      4. ConstraintSolver 可以根据 SolverState 的状态信息, 把其内部结构更改回之前的状态。
    • 这种设计使得 ConstraintSolver 把回退到上一个状态的信息委托给其他对象管理, 同时避免了暴露其内部结构和内容。

  • 应用场景

    • 在如下两个条件都满足时使用备忘录模式 (Memento Pattern)
      • 当一个对象的全部状态或部分状态的快照需要被保存,并可能会被用于恢复该对象至快照状态时。
      • 使用直接获取对象状态的接口会暴露其内部实现细节, 破坏对象的封装。
  • 结构图
    这里写图片描述

    • 图片中的实心三角箭头且箭头末尾没有圆圈的, 代表着单一的引用关系, 但是被引用的对象也有可能被其他对象引用。
    • 图片中的实心三角箭头且箭头末尾有空心菱形的, 代表着一对多的引用关系。菱形的一端持有三角箭头所致对象的多个引用。 例如 , Caretaker 中可以保存一个 Memento 数组。
    • 图片中的末端有圆圈的虚线是一个对方法体内容用伪代码说明的关系
  • 交互过程
    这里写图片描述

    1. aCaretaker 调用 anOriginator 的createMemento() 方法
    2. anOriginator 创建一个 Memento 对象 aMemento, 并把自己的状态信息保存到 aMemento 中.
    3. aCaretaker 需要恢复 anOriginator 的状态时, 调用 anOriginator 的 setMemento (aMenmento ) 方法, 把 aMenmento 传递给 anOriginator, anOriginator 再从 aMemento 中提取其状态信息, 将自己恢复到特定的状态。

Memento 的一个常见的错误实现方式

  • Mementor 错误实现
public class Memento {
   private String state;

   public Memento(String state){
      this.state = state;
   }

   public String getState(){
      return state;
   }    
}

正如文章一开始提到的, 这种定义 Memento 的方式是错误的, 原因就是没有注意到 Memento 模式中, 对于对象访问权限的限制。

只有 originator 可以向 memento 对象中存储和提取信息! memento 对象对于其他的对象来说是不透明的。

在 C++ 中, 有 friend 修饰符, 可以定义一个类的友类, 也就是可以将 Originator 定义为 Memento 的一个友类, 使得 Memento 中的私有方法和私有成员仅对 Originator 可见, 对其他的类不可见。 从而限制了访问权限啊。

class State;
class Originator {
    public:
        Memento* CreateMemento();
        void SetMemento(const Memento*);
        // ...
    private:
        State* _state;
        // internal data structures
        // ...
};
class Memento {
    public:
        // narrow public interface
        virtual ~Memento();
    private:
        // private members accessible only to Originator
        friend class Originator;
        Memento();
        void SetState(State*);
        State* GetState();
        // ...
    private:
        State* _state;
// ...
};

在 Java 中并没有 和 C++ 中 friend 修饰符同样效果的修饰符, 但是我们可以利用其包级别的访问权限达到同样的效果。

public class Memento {
   private String state;

   Memento(String state){
      this.state = state;
   }

   String getState(){
      return state;
   }

}

Java 中方法的默认访问级别是包级别,也就是说, 对于上述代码, 只有同包的类才能 调用 new Memento() 或 getState() , 所以需要将 Caretaker 放在和 Memento 不同的包中 , Originator 和 Memento 放在同一包中。

当然, 访问权限的设置还有其他方式, 可以参考 再谈备忘录模式 (memento pattern) Java 一文

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的备忘录模式的例子: 首先,我们需要创建一个备忘录类,用于保存对象的状态: ```java public class Memento { private String state; public Memento(String state) { this.state = state; } public String getState() { return state; } } ``` 然后,我们需要创建一个原始对象类,用于保存状态并创建备忘录: ```java public class Originator { private String state; public void setState(String state) { this.state = state; } public String getState() { return state; } public Memento createMemento() { return new Memento(state); } public void restoreMemento(Memento memento) { state = memento.getState(); } } ``` 接下来,我们需要创建一个负责保存和恢复备忘录的管理类: ```java public class Caretaker { private List<Memento> mementos = new ArrayList<>(); public void addMemento(Memento memento) { mementos.add(memento); } public Memento getMemento(int index) { return mementos.get(index); } } ``` 最后,我们可以使用备忘录模式来保存和恢复对象的状态: ```java public class Main { public static void main(String[] args) { Originator originator = new Originator(); Caretaker caretaker = new Caretaker(); originator.setState("State 1"); caretaker.addMemento(originator.createMemento()); originator.setState("State 2"); caretaker.addMemento(originator.createMemento()); originator.restoreMemento(caretaker.getMemento(0)); System.out.println(originator.getState()); // Output: State 1 originator.restoreMemento(caretaker.getMemento(1)); System.out.println(originator.getState()); // Output: State 2 } } ``` 在这个例子中,我们创建了一个原始对象类 `Originator`,一个备忘录类 `Memento`,以及一个用于保存和恢复备忘录的管理类 `Caretaker`。我们创建了两个备忘录,保存了 `Originator` 对象的两个不同状态,然后通过 `Caretaker` 对象来获取并恢复这些状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值