當系統需要提供「復原功能」、「取消復原功能」、「回復到上一個步驟」等需要將這些資料暫時存放在記憶體內,可以採納的設計模式。
要思考的是,在確保資料不會「任意被他者」複製、備份,且同時能有順序地備份資料,供使用者想要「復原」時使用。
作法是:
以下範例以「音樂模擬器」為核心製作。
製作將被儲存的資料: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();
    }
}
製作將被儲存的資料: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 模式。