iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 19
0

不違反封裝性的前提下,捕捉物件的內部狀態存在外面,以便日後回復至此一狀態。

(取自 物件導向設計模式−可再利用物件導向軟體之要素

一個玩遊戲打魔王的回憶

希望讀者也有過類似經驗,以免顯得我老了(QQ)。話說以前玩單機 RPG 的時候,像是金庸群俠傳,仙劍奇俠傳之類的遊戲,最麻煩的就是,好不容易解完了謎題,要打魔王了,才發現,等級不夠,或者是某個技能沒練滿打不過!小 case,讓我運一下苦練已久的 S(ave) / L(oad) 大法吧!你知道的,等級不夠就是回頭去衝等再來過吧。

如果應用程式也可以提供 S / L 大法呢?

想像你製作了一個簡報軟體,除了可以打上文字以外,還要可以讓使用者加入和編排幾何圖形。例如他可以加入像下面這張圖的樣子,兩個方塊有一條連接線。

投影片圖示1

有一天,使用者調整了一下這兩個圖示的編排,變成了下面這樣。

投影片圖示2

但是,他後悔了,他想要回到原本的編排,而剛剛好我們的程式提供了上一步的功能,能夠完美的恢復投影片變成

投影片圖示1

而不是

投影片圖示3

所以應該要怎麼做才能有這個效果呢?

持續地另存新檔,然後開啟舊檔

以簡報軟體來說,每一張投影片都會有該張投影片所擁有的內容,可能是文字,也可能是圖形,也會有圖形和圖形之間的連結,如果我們的程式可以把當前投影片的狀態全部記錄下來,當我們需要回復的時候,直接還原到先前紀錄的狀態就可以了。

而設計模式中的 Memento 模式,就是在為這種類型的情境所提供的套路,下面就讓我們來看看 Memento 的設計吧。

Memento 的設計概念

廢話不多說,先上圖!

22_memento_04

啥?什麼 Originator?什麼 Caretaker?這也太多新名詞了吧。Originator 就是產生 Memento 的物件,而 Caretaker 是知道什麼時候要取得 Memento 以及還原的物件。這樣說吧,以前面的簡報軟體來說,編輯器就是 Caretaker,而每一張投影片乃至於每一個圖形都可以是 Originator。

Memento 的設計

回到 Memento 的定義,有一個很重要的要點是「不違反封裝性」,我們可以思考,當我們的簡報軟體要達成上一步的時候,會是由哪個類別去完成?如果是由編輯器(Caretaker)來負責的話,貌似放了太多責任在編輯器(Caretaker)上,而且也會暴露過多投影片(Originator)的內部狀態,並且也提供了過多可以更動投影片(Originator)的介面。如果這樣設計的話,會導致程式的相依性過高,未來維護的時候就難以改動了。因此 Memento 模式在設計上,並不讓 Caretaker 知道該如何復原狀態,Caretaker 只要知道何時要恢復並呼叫誰(Originator)去恢復

另外一點是,為了保護 Memento 的狀態不被更動,Memento 其實會是個 Value Object,所有的資料都在被建構的時候傳入,除此之外,不會有任何介面可以去更動內部的資料。想想看,如果使用 S/L 大法的時候,讀取回來的遊戲進度完全不同了,會不會很驚喜(?)。(嘛,喜歡嘗試破解遊戲存擋的例外)

下面就來看看三種用 Java 實作的方式吧

基於巢狀類別的實作

22_memento_05

public class Caretaker {
  private Originator originator;
  private Stack<Memento> history;

  public Caretaker(Origniator originator) {
    this.originator = originator;
    history = new Stack<Memento>();
  }

  private void doSomething() {
    Memento m = originator.save();
    history.push(m);
  }

  private void undo() {
    Memento m = history.pop();
    originator.restore(m);
  }
}

// Imagine this is the class holds the data and states of slides
public class State {}

public class Originator {
  private State state;

  public Memento save() {
    new Memento(state);
  }

  public void restore(Memento m) {
    State state = m.getState();
    this.state = state;
    // notify state changed
  }

  public class Memento {
    private State state;

    private Memento(State state) {
      this.state = state;
    }

    private State getState() {
      return state;
    }
  }
}

基於中介類別的實作

通常為不支援巢狀類別的語言所採用,例如 PHP。

22_memento_06

public class Caretaker {
  private Originator originator;
  private Stack<Memento> history;

  public Caretaker(Origniator originator) {
    this.originator = originator;
    history = new Stack<Memento>();
  }

  private void doSomething() {
    Memento m = originator.save();
    history.push(m);
  }

  private void undo() {
    Memento m = history.pop();
    originator.restore(m);
  }
}

// Imagine this is the class holds the data and states of slides
public class State {}

public interface Memento {
}

public class Originator {
  private State state;

  public Memento save() {
    new ConcreteMemento(state);
  }

  public void restore(Memento m) {
    ConcreteMemento cm = (ConcreteMemento) m;
    State state = m.getState();
    this.state = state;
    // notify state changed
  }
}

public class ConcreteMemento implements Memento {
  private State state;

  public ConcreteMemento(State state) {
    this.state = state;
  }

  public State getState() {
    return state;
  }
}

極度嚴謹的封裝

22_memento_07

public class Caretaker {
  private Stack<Memento> history;

  public Caretaker() {
    history = new Stack<Memento>();
  }

  private void undo() {
    Memento m = history.pop();
    m.restore();
  }
}

// Imagine this is the class holds the data and states of slides
public class State {}

public interface Memento {
  public void restore();
}

public interface Originator {
  public Memento save();
}

public class ConcreteOriginator implements Originator {
  private State state;

  public Memento save() {
    new ConcreteMemento(state);
  }

  public void setState(State state) {
    this.state = state;
    // notify state changed
  }
}

public class ConcreteMemento implements Memento {
  private State state;
  private Originator originator;

  private ConcreteMemento(Originator originator, State state) {
    this.originator = originator;
    this.state = state;
  }

  public void restore() {
    originator.setState(state);
  }
}

透過這個設計,我們能夠讓 Originator 和 Memento 有更多不同的實作,卻又能夠讓外部程式使用,並且可以維持好的封裝。

Memento 的使用時機

  1. 當我們的程式需要提供能夠恢復物件先前狀態的功能時,例如編輯器的上一步,或者下一步(redo)。
  2. 使用 Memento 模式可以避免外部程式直接存取物件的狀態。

Memento 的優缺點

優點

  1. 可以恢復物件先前的狀態
  2. Originator 的程式可以更簡潔,因為記錄狀態的責任被轉移到 Caretaker 身上。

缺點

  1. 會使用更多的記憶體(RAM)來記錄物件的狀態。
  2. Caretaker 必須要關注 Originator 的生命週期,才能知道要移除不需要的 Memento 物件。
  3. 某些動態語言無法保證 Memento 物件內部的資料不會被更動,例如 PHP、JavaScript。

與其他模式的關聯

  • 某些時候,用 Prototype 模式會比 Memento 更簡易,通常是在物件的狀態沒有外部相依或者是可以很容易被重建的時候。
  • 在實作 Command 模式的 undo 功能時,可以使用 Memento 模式。這樣的話,Command 模式的命令就相當於 Memento 中的 Caretaker。

參考資料

Memento
Dive into Design Pattern - Memento

作者:Yenting


上一篇
[Design Pattern] Mediator 中介者模式
下一篇
[Design Pattern] Null Object 空物件模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言