iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Software Development

也該是時候學學 Design Pattern 了系列 第 23

Day 23: Behavioral patterns - Memento

目的

當系統需要提供「復原功能」、「取消復原功能」、「回復到上一個步驟」等需要將這些資料暫時存放在記憶體內,可以採納的設計模式。

說明

要思考的是,在確保資料不會「任意被他者」複製、備份,且同時能有順序地備份資料,供使用者想要「復原」時使用。

作法是:

  1. 定義好需要備份的物件(稱作:Originator),這邊可以採集中式一個物件或是多個小物件。
    1. 提供儲存的方式,使用 Memento 物件達到此功能。
    2. 提供復原的方式,使用 Memento 物件達到此功能。
  2. 建立負責儲存資料的物件(稱作:Memento)
    1. 該物件的屬性會跟 Originator 一致,確保沒有資料遺漏。
    2. 該物件只會儲存一次 Originator 的資料,如果要再次儲存就重新實體化一個新的 Memento 物件。
  3. 建立負責儲存多個 Memento 物件的看守者物件(稱作:Caretaker),Originator 要執行儲存、復原時,都是跟 Caretaker 溝通。

以下範例以「音樂模擬器」為核心製作。

UML 圖

Memento Pattern UML Diagram

使用 Java 實作

製作將被儲存的資料:Solfege

public class Solfege {
    private String name;

    public Solfege(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

製作音樂模擬器:MusicSimulator(Originator 物件)

public class MusicSimulator {
    private List<Solfege> inputs = new ArrayList<>();

    public void showInputs() {
        System.out.println("輸入的音階有:");

        for (Solfege solfege : inputs) {
            System.out.print(solfege.getName());
        }

        System.out.println("\n");
    }

    public void inputSolfege(Solfege solfege) {
        inputs.add(solfege);
    }

    public MusicSimulatorMemento saveInputs() {
        System.out.println("開始備份\n");
        StringBuilder stringBuilder = new StringBuilder();

        for (Solfege input : inputs) {
            stringBuilder.append(input.getName());
        }

        return new MusicSimulatorMemento(Base64.getEncoder().encodeToString(stringBuilder.toString().getBytes()));
    }

    public void restore(MusicSimulatorMemento musicSimulatorMemento) {
        System.out.println("開始還原\n");

        if (musicSimulatorMemento == null) {
            System.out.println("沒有記錄");
        } else {
            String decodedString = new String(Base64.getDecoder().decode(musicSimulatorMemento.getBackup().getBytes()));
            inputs.clear();

            for (String inputString : decodedString.split("")) {
                inputs.add(new Solfege(inputString));
            }
        }
    }
}

製作負責儲存資料的物件:MusicSimulatorMemento(Memento物件)

public class MusicSimulatorMemento {
    private String backup;

    public MusicSimulatorMemento(String backup) {
        this.backup = backup;
    }

    public String getBackup() {
        return backup;
    }
}

製作建立負責儲存多個 Memento 的物件:MusicSimulatorCareTaker(Caretaker物件)

public class MusicSimulatorCareTaker {
    private List<MusicSimulatorMemento> saves = new ArrayList<>();

    public MusicSimulatorMemento getUndo() {
        if (saves.isEmpty()) {
            return null;
        } else {
            return saves.get(saves.size() - 1);
        }
    }

    public void setSave(MusicSimulatorMemento memento) {
        saves.add(memento);
    }
}

測試,在模擬器上輸入音符後,還原到上個狀態:MusicSimulatorMementoSample

public class MusicSimulatorMementoSample {

    public static void main(String[] args) {
        MusicSimulator musicSimulator = new MusicSimulator();

        // 輸入音階
        musicSimulator.inputSolfege(new Solfege("C"));
        musicSimulator.inputSolfege(new Solfege("D"));
        musicSimulator.inputSolfege(new Solfege("E"));
        musicSimulator.inputSolfege(new Solfege("F"));

        // 確認輸入的音階
        musicSimulator.showInputs();

        // 儲存
        MusicSimulatorCareTaker musicSimulatorCareTaker = new MusicSimulatorCareTaker();
        musicSimulatorCareTaker.setSave(musicSimulator.saveInputs());

        // 輸入新的音階
        musicSimulator.inputSolfege(new Solfege("G"));
        musicSimulator.inputSolfege(new Solfege("A"));
        musicSimulator.inputSolfege(new Solfege("B"));

        // 確認輸入的音階
        musicSimulator.showInputs();

        // 復原到上個狀態
        musicSimulator.restore(musicSimulatorCareTaker.getUndo());

        // 確認輸入的音階
        musicSimulator.showInputs();
    }

}

使用 JavaScript 實作

製作將被儲存的資料:Solfege

class Solfege {
  constructor(name) {
    /** @type {string} */
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

製作音樂模擬器:MusicSimulator(Originator 物件)

class MusicSimulator {
  constructor() {
    /** @type {Solfege[]} */
    this.inputs = [];
  }

  showInputs() {
    console.log("輸入的音階有:");

    let solfegeList = ""

    for (const solfege of this.inputs) {
      solfegeList += solfege.getName();
    }

    console.log(`${solfegeList}\n`);
  }

  /** @param {Solfege} solfege */
  inputSolfege(solfege) {
    this.inputs.push(solfege);
  }

  saveInputs() {
    console.log("開始備份\n");

    let currentInputs = "";

    for (const solfege of this.inputs) {
      currentInputs += solfege.getName();
    }

    return new MusicSimulatorMemento(Buffer.from(currentInputs, 'utf8').toString('base64'));
  }

  /** @param {MusicSimulatorMemento} musicSimulatorMemento */
  restore(musicSimulatorMemento) {
    console.log("開始還原\n");

    if (musicSimulatorMemento === null) {
      console.log("沒有記錄");
    } else {
      const decodedString = Buffer.from(musicSimulatorMemento.getBackup(), 'base64').toString('utf8');
      this.inputs = [];

      for (const inputString of decodedString.split("")) {
        this.inputs.push(new Solfege(inputString));
      }
    }
  }
}

製作負責儲存資料的物件:MusicSimulatorMemento(Memento物件)

class MusicSimulatorMemento {
  constructor(backup) {
    /** @type {string} */
    this.backup = backup;
  }

  getBackup() {
    return this.backup;
  }
}

製作建立負責儲存多個 Memento 的物件:MusicSimulatorCareTaker(Caretaker物件)

class MusicSimulatorCareTaker {
  constructor() {
    /** @type {MusicSimulatorMemento[]} */
    this.saves = [];
  }

  getUndo() {
    if (this.saves.length === 0) {
      return null;
    } else {
      return this.saves[this.saves.length - 1];
    }
  }

  /** @param {MusicSimulatorMemento} */
  setSave(memento) {
    this.saves.push(memento);
  }
}

測試,在模擬器上輸入音符後,還原到上個狀態:MusicSimulatorMementoSample

const musicSimulatorMementoSample = () => {
  const musicSimulator = new MusicSimulator();

  // 輸入音階
  musicSimulator.inputSolfege(new Solfege("C"));
  musicSimulator.inputSolfege(new Solfege("D"));
  musicSimulator.inputSolfege(new Solfege("E"));
  musicSimulator.inputSolfege(new Solfege("F"));

  // 確認輸入的音階
  musicSimulator.showInputs();

  // 儲存
  const musicSimulatorCareTaker = new MusicSimulatorCareTaker();
  musicSimulatorCareTaker.setSave(musicSimulator.saveInputs());

  // 輸入新的音階
  musicSimulator.inputSolfege(new Solfege("G"));
  musicSimulator.inputSolfege(new Solfege("A"));
  musicSimulator.inputSolfege(new Solfege("B"));

  // 確認輸入的音階
  musicSimulator.showInputs();

  // 復原到上個狀態
  musicSimulator.restore(musicSimulatorCareTaker.getUndo());

  // 確認輸入的音階
  musicSimulator.showInputs();
}

musicSimulatorMementoSample();

總結

Memento 模式常見的譯名是備忘錄,實際的行為的確與備忘錄相似,將資料「暫存」在某個地方,這邊是建立物件暫存於記憶體內。也因為是建立在記憶體內,一旦累積大量的記錄,會對記憶體造成不小的負擔,有可能會讓程式當掉。這讓我想起很久以前 Word 會忽然當掉,如果沒有存檔,那所有文字都消失了。

我在測試時注意到,「復原功能」、「取消復原功能」需要一個 Array(JS)、List(Java)來儲存,如果邏輯沒有設定好,可能在記錄的排序上出問題,這點在設計上要多加注意。

總之,這是個用於特殊情況下,同時在開發上的要注意的細節較多的模式。

明天將介紹 Behavioural patterns 的第七個模式:Observer 模式。


上一篇
Day 22: Behavioral patterns - Mediator
下一篇
Day 24: Behavioral patterns - Observer
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言