天天看點

C++設計模式(17)——備忘錄模式

亦稱: 快照、Snapshot、Memento

意圖

備忘錄模式是一種行為設計模式, 允許在不暴露對象實作細節的情況下儲存和恢複對象之前的狀态。

C++設計模式(17)——備忘錄模式

問題

假如你正在開發一款文字編輯器應用程式。 除了簡單的文字編輯功能外, 編輯器中還要有設定文本格式和插入内嵌圖檔等功能。

後來, 你決定讓使用者能撤銷施加在文本上的任何操作。 這項功能在過去幾年裡變得十分普遍, 是以使用者期待任何程式都有這項功能。 你選擇采用直接的方式來實作該功能: 程式在執行任何操作前會記錄所有的對象狀态, 并将其儲存下來。 當使用者此後需要撤銷某個操作時, 程式将從曆史記錄中擷取最近的快照, 然後使用它來恢複所有對象的狀态。

C++設計模式(17)——備忘錄模式

程式在執行操作前儲存所有對象的狀态快照, 稍後可通過快照将對象恢複到之前的狀态。

讓我們來思考一下這些狀态快照。 首先, 到底該如何生成一個快照呢? 很可能你會需要周遊對象的所有成員變量并将其數值複制儲存。 但隻有當對象對其内容沒有嚴格通路權限限制的情況下, 你才能使用該方式。 不過很遺憾, 絕大部分對象會使用私有成員變量來存儲重要資料, 這樣别人就無法輕易檢視其中的内容。

現在我們暫時忽略這個問題, 假設對象都像嬉皮士一樣: 喜歡開放式的關系并會公開其所有狀态。 盡管這種方式能夠解決目前問題, 讓你可随時生成對象的狀态快照, 但這種方式仍存在一些嚴重問題。 未來你可能會添加或删除一些成員變量。 這聽上去很簡單, 但需要對負責複制受影響對象狀态的類進行更改。

C++設計模式(17)——備忘錄模式

如何複制對象的私有狀态?

還有更多問題。 讓我們來考慮編輯器 (Editor) 狀态的實際 “快照”, 它需要包含哪些資料? 至少必須包含實際的文本、 光标坐标和目前滾動條位置等。 你需要收集這些資料并将其放入特定容器中, 才能生成快照。

你很可能會将大量的容器對象存儲在曆史記錄清單中。 這樣一來, 容器最終大機率會成為同一個類的對象。 這個類中幾乎沒有任何方法, 但有許多與編輯器狀态一一對應的成員變量。 為了讓其他對象能儲存或讀取快照, 你很可能需要将快照的成員變量設為公有。 無論這些狀态是否私有, 其都将暴露一切編輯器狀态。 其他類會對快照類的每個小改動産生依賴, 除非這些改動僅存在于私有成員變量或方法中, 而不會影響外部類。

我們似乎走進了一條死胡同: 要麼會暴露類的所有内部細節而使其過于脆弱; 要麼會限制對其狀态的通路權限而無法生成快照。 那麼, 我們還有其他方式來實作 “撤銷” 功能嗎?

解決方案

我們剛才遇到的所有問題都是封裝 “破損” 造成的。 一些對象試圖超出其職責範圍的工作。 由于在執行某些行為時需要擷取資料, 是以它們侵入了其他對象的私有空間, 而不是讓這些對象來完成實際的工作。

備忘錄模式将建立狀态快照 (Snapshot) 的工作委派給實際狀态的擁有者原發器 (Originator) 對象。 這樣其他對象就不再需要從 “外部” 複制編輯器狀态了, 編輯器類擁有其狀态的完全通路權, 是以可以自行生成快照。

模式建議将對象狀态的副本存儲在一個名為備忘錄 (Memento) 的特殊對象中。 除了建立備忘錄的對象外, 任何對象都不能通路備忘錄的内容。 其他對象必須使用受限接口與備忘錄進行互動, 它們可以擷取快照的中繼資料 (建立時間和操作名稱等), 但不能擷取快照中原始對象的狀态。

C++設計模式(17)——備忘錄模式

原發器擁有對備忘錄的完全通路權限, 負責人則隻能通路中繼資料。

這種限制政策允許你将備忘錄儲存在通常被稱為負責人 (Caretakers) 的對象中。 由于負責人僅通過受限接口與備忘錄互動, 故其無法修改存儲在備忘錄内部的狀态。 同時, 原發器擁有對備忘錄所有成員的通路權限, 進而能随時恢複其以前的狀态。

在文字編輯器的示例中, 我們可以建立一個獨立的曆史 (History) 類作為負責人。 編輯器每次執行操作前, 存儲在負責人中的備忘錄棧都會生長。 你甚至可以在應用的 UI 中渲染該棧, 為使用者顯示之前的操作曆史。

當使用者觸發撤銷操作時, 曆史類将從棧中取回最近的備忘錄, 并将其傳遞給編輯器以請求進行復原。 由于編輯器擁有對備忘錄的完全通路權限, 是以它可以使用從備忘錄中擷取的數值來替換自身的狀态。

備忘錄模式結構

基于嵌套類的實作

該模式的經典實作方式依賴于許多流行程式設計語言 (例如 C++、 C# 和 Java) 所支援的嵌套類。

C++設計模式(17)——備忘錄模式

1、原發器 (Originator) 類可以生成自身狀态的快照, 也可以在需要時通過快照恢複自身狀态。

2、備忘錄 (Memento) 是原發器狀态快照的值對象 (value object)。 通常做法是将備忘錄設為不可變的, 并通過構造函數一次性傳遞資料。

3、負責人 (Caretaker) 僅知道 “何時” 和 “為何” 捕捉原發器的狀态, 以及何時恢複狀态。

負責人通過儲存備忘錄棧來記錄原發器的曆史狀态。 當原發器需要回溯曆史狀态時, 負責人将從棧中擷取最頂部的備忘錄, 并将其傳遞給原發器的恢複 (restoration) 方法。

**4、**在該實作方法中, 備忘錄類将被嵌套在原發器中。 這樣原發器就可通路備忘錄的成員變量和方法, 即使這些方法被聲明為私有。 另一方面, 負責人對于備忘錄的成員變量和方法的通路權限非常有限: 它們隻能在棧中儲存備忘錄, 而不能修改其狀态。

基于中間接口的實作

另外一種實作方法适用于不支援嵌套類的程式設計語言 (沒錯, 我說的就是 PHP)。

C++設計模式(17)——備忘錄模式

1、在沒有嵌套類的情況下, 你可以規定負責人僅可通過明确聲明的中間接口與備忘錄互動, 該接口僅聲明與備忘錄中繼資料相關的方法, 限制其對備忘錄成員變量的直接通路權限。

2、另一方面, 原發器可以直接與備忘錄對象進行互動, 通路備忘錄類中聲明的成員變量和方法。 這種方式的缺點在于你需要将備忘錄的所有成員變量聲明為公有。

封裝更加嚴格的實作

如果你不想讓其他類有任何機會通過備忘錄來通路原發器的狀态, 那麼還有另一種可用的實作方式。

C++設計模式(17)——備忘錄模式

1、這種實作方式允許存在多種不同類型的原發器和備忘錄。 每種原發器都和其相應的備忘錄類進行互動。 原發器和備忘錄都不會将其狀态暴露給其他類。

2、負責人此時被明确禁止修改存儲在備忘錄中的狀态。 但負責人類将獨立于原發器, 因為此時恢複方法被定義在了備忘錄類中。

3、每個備忘錄将與建立了自身的原發器連接配接。 原發器會将自己及狀态傳遞給備忘錄的構造函數。 由于這些類之間的緊密聯系, 隻要原發器定義了合适的設定器 (setter), 備忘錄就能恢複其狀态。

僞代碼

本例結合使用了指令模式與備忘錄模式, 可儲存複雜文字編輯器的狀态快照, 并能在需要時從快照中恢複之前的狀态。

C++設計模式(17)——備忘錄模式

儲存文字編輯器狀态的快照。

指令 (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、負責人與原發器之間的連接配接可以移動到備忘錄類中。 在本例中, 每個備忘錄都必須與建立自己的原發器相連接配接。 恢複方法也可以移動到備忘錄類中, 但隻有當備忘錄類嵌套在原發器中, 或者原發器類提供了足夠多的設定器并可對其狀态進行重寫時, 這種方式才能實作。

備忘錄模式優缺點

C++設計模式(17)——備忘錄模式

與其他模式的關系

你可以同時使用指令模式和備忘錄模式來實作 “撤銷”。 在這種情況下, 指令用于對目标對象執行各種不同的操作, 備忘錄用來儲存一條指令執行前該對象的狀态。

你可以同時使用備忘錄和疊代器模式來擷取目前疊代器的狀态, 并且在需要的時候進行復原。

有時候原型模式可以作為備忘錄的一個簡化版本, 其條件是你需要在曆史記錄中存儲的對象的狀态比較簡單, 不需要連結其他外部資源, 或者連結可以友善地重建。

C++ 備忘錄模式講解和代碼示例

備忘錄是一種行為設計模式, 允許生成對象狀态的快照并在以後将其還原。

備忘錄不會影響它所處理的對象的内部結構, 也不會影響快照中儲存的資料。

使用示例: 備忘錄的基本功能可用序列化來實作, 這在 C++ 語言中很常見。 盡管備忘錄不是生成對象狀态快照的唯一或最有效的方法, 但它能在保護原始對象的結構不暴露給其他對象的情況下儲存對象狀态的備份。

概念示例

本例說明了備忘錄設計模式的結構并重點回答了下面的問題:

它由哪些類組成?

這些類扮演了哪些角色?

模式中的各個元素會以何種方式互相關聯?

main.cc: 概念示例

/**
 * 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";
  }
};

/**
 * 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;
}
           

Output.txt: 執行結果

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
           

來源:https://refactoringguru.cn/design-patterns/memento

僅供學習,非商業用途,侵删

繼續閱讀