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