Memento 备忘录模式

一.意图

Memento是一种行为设计模式,允许你保存和恢复对象的前一状态,而不暴露其实现细节。

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。------《设计模式》GoF

二.问题

想象你正在创建一个文本编辑器应用。除了简单的文本编辑,编辑器还可以格式化文本、插入内联图片等。

某个时候,你们决定允许用户撤销对文本所做的任何作。多年来,这一功能变得非常普遍,如今人们期望每个应用都具备此功能。在实施方面,你选择了直接的方式。在执行任何作之前,应用会记录所有对象的状态并将其保存到某个存储中。之后,当用户决定回退某个动作时,应用会从历史中获取最新的快照,并用它恢复所有对象的状态。

暂且不考虑这个问题,假设我们的对象表现得像嬉皮士:更喜欢开放关系,保持国家公开。虽然这种方法能解决眼前的问题,并让你随意生成对象状态快照,但仍存在一些严重的问题。将来你可能会决定重构一些编辑器类,或者添加或移除部分字段。听起来很简单,但这也需要更改负责复制受影响对象状态的类。

但还有更多。让我们来看看编辑器状态的实际"快照"。它包含哪些数据?至少必须包含实际文本、光标坐标、当前滚动位置等。要做快照,你需要收集这些值并放入某种容器中。

很可能,你会把很多这些容器对象存储在某个代表历史的列表里。因此,容器很可能会成为同一类的对象。类几乎没有方法,但会有许多字段镜像编辑器的状态。为了让其他对象能够写入和读取快照的数据,你可能需要将其字段设置为公开。那样会暴露编辑的所有状态,无论是否私密。其他类会依赖快照类的每一个小变化,而这些变化本应发生在私有字段和方法内,而不会影响外部类。

看起来我们已经走到死胡同:要么暴露所有类的内部细节,使它们变得过于脆弱,要么限制对其状态的访问,使得无法生成快照。还有其他实现"撤销"的方法吗?

三.解决方案

我们刚才遇到的所有问题都是封装损坏引起的。有些物体试图做超出预期的作。为了收集执行某些动作所需的数据,它们入侵了其他对象的私有空间,而不是让这些对象执行实际动作。

Memento 模式将创建状态快照的任务委托给该状态的实际所有者,即发起对象。因此,编辑器类本身可以创建快照,而不是其他对象试图从"外部"复制编辑器的状态。

该模式建议将物体状态的副本存储在一个称为 memento 的特殊对象中。纪念品的内容对其他物体只有制造它的那个无法访问。其他对象必须通过有限的接口与memomentos通信,该接口可能允许获取快照的元数据(创建时间、执行作名称等),但无法获取快照中原始对象的状态。

这种限制性策略允许你将纪念品存储在其他对象中,通常称为看护者。由于看护者只通过有限界面处理纪念品,无法篡改纪念品内部的状态。同时,发起者可以访问纪念品内部的所有字段,允许随意恢复之前的状态。

在我们的文本编辑器示例中,我们可以创建一个独立的历史类作为守护者。每当编辑器准备执行作时,存储在看护者的备忘堆会不断增加。你甚至可以在应用界面中渲染这个堆栈,向用户展示之前执行作的历史。

当用户触发撤销时,历史记录会从堆栈中抓取最新的 memento,并传回给编辑者,请求回滚。由于编辑器拥有对纪念品的完全访问权限,它会根据从纪念品中取出的值来更改自身状态。

四.结构

基于嵌套类的实现

该模式的经典实现依赖于对嵌套类的支持,嵌套类在许多流行编程语言中(如 C++、C# 和 Java)中都已提供。

基于中间接口的实现

还有一种替代实现,适合不支持嵌套类的编程语言(是的,PHP,我说的是你)。

采用更严格封装的实现

还有一种实现,在你不想让其他职业通过纪念品访问原始者状态的任何可能性时都很有用。

五.适合应用场景

  1. 当你想生成物体状态快照以恢复之前的状态时,可以使用 Memento 模式。

    记忆碎片模式允许你完整复制物体的状态,包括私有字段,并将它们与对象分开存储。虽然大多数人因为"撤销"的使用场景而记住了这个模式,但在处理交易时(比如需要在错误时回滚作时),它同样不可或缺。

  2. 当直接访问对象的字段/getter/setter违反封装时,使用该模式。

    记忆碎片让物体本身负责创建其状态快照。没有其他对象能读取快照,使得原始对象的状态数据安全可靠。

六.实现方式

  1. 确定哪个职业将扮演发起者的角色。了解程序是使用一个中心对象还是多个较小对象非常重要。

  2. 创建纪念品职业。逐一声明一组字段,这些字段与起源类内声明的字段相呼应。

  3. 让纪念品职业变得不可变。memomento应仅通过构造函数接受一次数据。班级不应该有二传。

  4. 如果你的编程语言支持嵌套类,就把 memento 嵌套在 originator 里。如果没有,从memento类中提取一个空白接口,让所有其他对象都用它来引用memento。你可以在接口中添加一些元数据作,但不能暴露发起者的状态。

  5. 给起源类添加生成纪念品的方法。发起者应通过纪念物构造子的一个或多个参数将状态传递给纪念品。

    方法的返回类型应是你在前一步提取的接口(假设你已经提取了它)。在底层,纪念品制作方法应直接与纪念品类别配合使用。

  6. 添加一种方法将发起者的状态恢复到其类。它应该接受一个纪念物作为论据。如果你在前一步提取了接口,就把它设置为参数的类型。在这种情况下,你需要将接收对象类型化到memento类,因为发起者需要对该对象进行完全访问权限。

  7. 看护者,无论代表命令对象、历史,还是完全不同的事物,都应知道何时向原始者请求新的纪念品,如何存储它们,以及何时用特定纪念品恢复原始者。

  8. 看护者与起始者之间的连接可以转移到 memento 类中。在这种情况下,每个 memento 必须连接到创建它的起源者。恢复方法也会迁移到 memento 类。然而,这一切只有在 memento 类嵌套到起源者中,或者起源类提供了足够的 setter 来覆盖其状态时才有意义。

七.优缺点

  1. 优点:

    • 你可以生成对象状态的快照而不违反封装。

    • 你可以通过让看护人维护发起人状态的历史来简化发起人的代码。

  2. 缺点

    • 如果客户端频繁创建纪念品,应用可能会占用大量内存。

    • 看护者应追踪发起者的生命周期,以便销毁过时的纪念品。

    • 大多数动态编程语言,如PHP、Python和JavaScript,无法保证纪念品中的状态保持不变。

八.与其他模式的关系

  • 实现"撤销"时,你可以同时使用Command和Memento。在这种情况下,命令负责对目标对象执行各种作,而 mementos 则在命令执行前保存该对象的状态。

  • 你可以用Memento和Iterator一起捕捉当前迭代状态,必要时回滚。

  • 有时候Prototype可以成为Memento更简单的替代品。如果你想存储在历史中的对象状态相当简单,没有外部资源链接,或者链接容易重新建立,这种方法就适用。

九.示例代码

复制代码
/**
 * The Memento interface provides a way to retrieve the memento's metadata, such
 * as creation date or name. However, it doesn't expose the Originator's state.
 */
class Memento {
 public:
  virtual ~Memento() {}
  virtual std::string GetName() const = 0;
  virtual std::string date() const = 0;
  virtual std::string state() const = 0;
};
​
/**
 * The Concrete Memento contains the infrastructure for storing the Originator's
 * state.
 */
class ConcreteMemento : public Memento {
 private:
  std::string state_;
  std::string date_;
​
 public:
  ConcreteMemento(std::string state) : state_(state) {
    this->state_ = state;
    std::time_t now = std::time(0);
    this->date_ = std::ctime(&now);
  }
  /**
   * The Originator uses this method when restoring its state.
   */
  std::string state() const override {
    return this->state_;
  }
  /**
   * The rest of the methods are used by the Caretaker to display metadata.
   */
  std::string GetName() const override {
    return this->date_ + " / (" + this->state_.substr(0, 9) + "...)";
  }
  std::string date() const override {
    return this->date_;
  }
};
​
/**
 * The Originator holds some important state that may change over time. It also
 * defines a method for saving the state inside a memento and another method for
 * restoring the state from it.
 */
class Originator {
  /**
   * @var string For the sake of simplicity, the originator's state is stored
   * inside a single variable.
   */
 private:
  std::string state_;
​
  std::string GenerateRandomString(int length = 10) {
    const char alphanum[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";
    int stringLength = sizeof(alphanum) - 1;
​
    std::string random_string;
    for (int i = 0; i < length; i++) {
      random_string += alphanum[std::rand() % stringLength];
    }
    return random_string;
  }
​
 public:
  Originator(std::string state) : state_(state) {
    std::cout << "Originator: My initial state is: " << this->state_ << "\n";
  }
  /**
   * The Originator's business logic may affect its internal state. Therefore,
   * the client should backup the state before launching methods of the business
   * logic via the save() method.
   */
  void DoSomething() {
    std::cout << "Originator: I'm doing something important.\n";
    this->state_ = this->GenerateRandomString(30);
    std::cout << "Originator: and my state has changed to: " << this->state_ << "\n";
  }
​
  /**
   * Saves the current state inside a memento.
   */
  Memento *Save() {
    return new ConcreteMemento(this->state_);
  }
  /**
   * Restores the Originator's state from a memento object.
   */
  void Restore(Memento *memento) {
    this->state_ = memento->state();
    std::cout << "Originator: My state has changed to: " << this->state_ << "\n";
    delete memento;
  }
};
​
/**
 * The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
 * doesn't have access to the originator's state, stored inside the memento. It
 * works with all mementos via the base Memento interface.
 */
class Caretaker {
  /**
   * @var Memento[]
   */
 private:
  std::vector<Memento *> mementos_;
​
  /**
   * @var Originator
   */
  Originator *originator_;
​
 public:
     Caretaker(Originator* originator) : originator_(originator) {
     }
​
     ~Caretaker() {
         for (auto m : mementos_) delete m;
     }
​
  void Backup() {
    std::cout << "\nCaretaker: Saving Originator's state...\n";
    this->mementos_.push_back(this->originator_->Save());
  }
  void Undo() {
    if (!this->mementos_.size()) {
      return;
    }
    Memento *memento = this->mementos_.back();
    this->mementos_.pop_back();
    std::cout << "Caretaker: Restoring state to: " << memento->GetName() << "\n";
    try {
      this->originator_->Restore(memento);
    } catch (...) {
      this->Undo();
    }
  }
  void ShowHistory() const {
    std::cout << "Caretaker: Here's the list of mementos:\n";
    for (Memento *memento : this->mementos_) {
      std::cout << memento->GetName() << "\n";
    }
  }
};
/**
 * Client code.
 */
​
void ClientCode() {
  Originator *originator = new Originator("Super-duper-super-puper-super.");
  Caretaker *caretaker = new Caretaker(originator);
  caretaker->Backup();
  originator->DoSomething();
  caretaker->Backup();
  originator->DoSomething();
  caretaker->Backup();
  originator->DoSomething();
  std::cout << "\n";
  caretaker->ShowHistory();
  std::cout << "\nClient: Now, let's rollback!\n\n";
  caretaker->Undo();
  std::cout << "\nClient: Once more!\n\n";
  caretaker->Undo();
​
  delete originator;
  delete caretaker;
}
​
int main() {
  std::srand(static_cast<unsigned int>(std::time(NULL)));
  ClientCode();
  return 0;
}

执行结果

复制代码
Originator: My initial state is: Super-duper-super-puper-super.
​
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: uOInE8wmckHYPwZS7PtUTwuwZfCIbz
​
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: te6RGmykRpbqaWo5MEwjji1fpM1t5D
​
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: hX5xWDVljcQ9ydD7StUfbBt5Z7pcSN
​
Caretaker: Here's the list of mementos:
Sat Oct 19 18:09:37 2019
 / (Super-dup...)
Sat Oct 19 18:09:37 2019
 / (uOInE8wmc...)
Sat Oct 19 18:09:37 2019
 / (te6RGmykR...)
​
Client: Now, let's rollback!
​
Caretaker: Restoring state to: Sat Oct 19 18:09:37 2019
 / (te6RGmykR...)
Originator: My state has changed to: te6RGmykRpbqaWo5MEwjji1fpM1t5D
​
Client: Once more!
​
Caretaker: Restoring state to: Sat Oct 19 18:09:37 2019
 / (uOInE8wmc...)
Originator: My state has changed to: uOInE8wmckHYPwZS7PtUTwuwZfCIbz
相关推荐
懵萌长颈鹿13 小时前
备忘录模式 (Memento Pattern)
备忘录模式
Engineer邓祥浩14 小时前
设计模式学习(21) 23-19 备忘录模式
学习·设计模式·备忘录模式
小码过河.9 天前
设计模式——备忘录模式
设计模式·备忘录模式
会员果汁17 天前
14.设计模式-备忘录模式
设计模式·备忘录模式
JavaBoy_XJ24 天前
行为型-备忘录模式
备忘录模式
__万波__25 天前
二十三种设计模式(十九)--备忘录模式
java·设计模式·备忘录模式
老朱佩琪!1 个月前
Unity备忘录模式
java·unity·备忘录模式
Yeniden1 个月前
Deepeek用大白话讲解 --> 备忘录模式(企业级场景1,撤销重做2,状态保存3,游戏存档4)
游戏·备忘录模式
QQ 19226382 个月前
三自由度汽车操纵侧翻模型仿真 有说明文档 学习资料:附带整理好的Word理论分析说明文档
备忘录模式