備忘錄模式在不破壞封裝性前提下,捕獲物件內部狀態並外部化保存,以便日後恢復。本文實現文本編輯器「撤銷」功能,展示 Originator (發起人)、Memento (備忘錄)、Caretaker (管理者) 三角色協作,並討論狀態序列化、記憶體優化與多級撤銷/重做擴展。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Originator │────────▶│ Memento │◀───────▶│ Caretaker │
│ (發起人) │ 創建/恢復 │ (備忘錄) │ 存儲/管理 │ (管理者) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 內部狀態 │ 不可變快照 │ 棧/列表存儲
▼ ▼ ▼
Word 物件 WordMemento List
class Word {
private String content;
private String image;
private String table;
public Word(String content, String image, String table) {
this.content = content;
this.image = image;
this.table = table;
}
// 創建備忘錄:將當前狀態封裝為不可變快照
public WordMemento createMemento() {
return new WordMemento(content, image, table);
}
// 恢復狀態
public void restore(WordMemento memento) {
this.content = memento.getContent();
this.image = memento.getImage();
this.table = memento.getTable();
}
// 業務方法
public void setContent(String content) { this.content = content; }
public void setImage(String image) { this.image = image; }
public void setTable(String table) { this.table = table; }
@Override public String toString() {
return "Word{content='" + content + "', image='" + image + "', table='" + table + "'}";
}
}
final class WordMemento {
private final String content;
private final String image;
private final String table;
public WordMemento(String content, String image, String table) {
this.content = content;
this.image = image;
this.table = table;
}
// 僅提供 getter,無 setter,保證不可變
public String getContent() { return content; }
public String getImage() { return image; }
public String getTable() { return table; }
}
class Caretaker {
private final List history = new ArrayList<>();
private int currentIndex = -1; // 當前狀態指針
public void save(WordMemento memento) {
// 截斷當前指針後的歷史 (新分支覆蓋重做棧)
if (currentIndex < history.size() - 1) {
history.subList(currentIndex + 1, history.size()).clear();
}
history.add(memento);
currentIndex++;
}
public WordMemento undo() {
if (currentIndex <= 0) {
System.out.println("無法撤銷:已到達最早狀態");
return null;
}
currentIndex--;
return history.get(currentIndex);
}
public WordMemento redo() {
if (currentIndex >= history.size() - 1) {
System.out.println("無法重做:已到達最新狀態");
return null;
}
currentIndex++;
return history.get(currentIndex);
}
public boolean canUndo() { return currentIndex > 0; }
public boolean canRedo() { return currentIndex < history.size() - 1; }
}
public class MementoDemo {
public static void main(String[] args) {
Word doc = new Word("初始內容", "logo.png", "table1");
Caretaker caretaker = new Caretaker();
// 保存初始狀態
caretaker.save(doc.createMemento());
System.out.println("初始: " + doc);
// 修改 1
doc.setContent("修改後內容 v1");
doc.setImage("banner.png");
caretaker.save(doc.createMemento());
System.out.println("修改1: " + doc);
// 修改 2
doc.setContent("修改後內容 v2");
doc.setTable("table2");
caretaker.save(doc.createMemento());
System.out.println("修改2: " + doc);
// 撤銷
WordMemento m = caretaker.undo();
if (m != null) doc.restore(m);
System.out.println("撤銷1: " + doc);
// 撤銷
m = caretaker.undo();
if (m != null) doc.restore(m);
System.out.println("撤銷2: " + doc);
// 重做
m = caretaker.redo();
if (m != null) doc.restore(m);
System.out.println("重做1: " + doc);
}
}
初始: Word{content='初始內容', image='logo.png', table='table1'}
修改1: Word{content='修改後內容 v1', image='banner.png', table='table1'}
修改2: Word{content='修改後內容 v2', image='banner.png', table='table2'}
撤銷1: Word{content='修改後內容 v1', image='banner.png', table='table1'}
撤銷2: Word{content='初始內容', image='logo.png', table='table1'}
重做1: Word{content='修改後內容 v1', image='banner.png', table='table1'}
// 備忘錄實現 Serializable
final class WordMemento implements Serializable {
private static final long serialVersionUID = 1L;
// ... 同上
}
// 管理者持久化
class PersistentCaretaker extends Caretaker {
private final String storagePath;
public void saveToDisk() {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(storagePath))) {
oos.writeObject(history);
oos.writeInt(currentIndex);
} catch (IOException e) { log.error("持久化失敗", e); }
}
@SuppressWarnings("unchecked")
public void loadFromDisk() {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(storagePath))) {
history.clear();
history.addAll((List) ois.readObject());
currentIndex = ois.readInt();
} catch (IOException | ClassNotFoundException e) { log.error("加載失敗", e); }
}
}
class MultiCaretaker {
private final Map caretakers = new HashMap<>();
public Caretaker getCaretaker(String originatorId) {
return caretakers.computeIfAbsent(originatorId, k -> new Caretaker());
}
}
| 優點 | 缺點 |
|---|---|
| 不破壞封裝性 (發起人控制狀態暴露) | 大狀態對象頻繁備份佔用大量記憶體 |
| 簡化發起人代碼 (無需管理歷史) | 管理者需知道何時保存/恢復 |
| 支援任意級別撤銷/重做 | 若發起人內部結構頻繁變更,備忘錄類需同步修改 |
| 易於實現持久化、棧點、分支 | 語言無 GC 時需手動管理備忘錄生命週期 |
備忘錄模式以「不可變快照 + 外部管理者」實現狀態回溯,核心在於: