Memento
เก็บสถานะภายในของ object ไว้กู้คืนภายหลัง โดยไม่ละเมิด encapsulation
Memento (หรือ Token) ใช้ ดึงสถานะภายในของ object ออกไปเก็บไว้เพื่อกู้คืนภายหลัง โดยไม่ละเมิด encapsulation เป็น pattern เชิงพฤติกรรมที่เพิ่มความสามารถ undo และ replay
เปรียบเหมือน save point ในเกม, stack ติดตามการเปลี่ยนแปลง หรือ commit ใน source control จุดสำคัญคือมันจับสถานะภายในได้โดยไม่เปิดเผยไส้ในให้ภายนอก มีสามบทบาท: Originator, Memento และ Caretaker
โปรแกรมจำนวนมากต้องการ “ย้อนเวลา” กลับไปยังสถานะก่อนหน้า เช่น ปุ่ม undo ใน text editor, การ rollback transaction, หรือการ replay เหตุการณ์เพื่อ debug ปัญหาคือ ถ้าจะให้ object ภายนอก (เช่น undo manager) จับ snapshot ของสถานะ object เป้าหมายเอง มันมักต้องเข้าถึง field ภายในโดยตรง — ซึ่งละเมิด encapsulation และทำให้ทุกครั้งที่เปลี่ยนโครงสร้างภายในของ object เป้าหมาย code ที่ทำ undo ก็พังตามไปด้วย
Memento แก้ปัญหานี้ด้วยการให้ ตัว object เอง (Originator) เป็นผู้สร้าง snapshot ของสถานะตัวมันเอง แล้วส่งมอบ snapshot นั้น (Memento) ให้ผู้อื่น (Caretaker) เก็บรักษาไว้แทน Caretaker เห็นแค่ “ก้อนข้อมูลปิดผนึก” ที่ส่งกลับมาให้ Originator restore เท่านั้น ไม่มีสิทธิ์อ่านหรือแก้ไขเนื้อหาข้างในเลย — encapsulation จึงยังคงอยู่ครบถ้วนตลอดกระบวนการ
แนวคิดหลักคือแยกความรับผิดชอบออกเป็นสามส่วน: ใครสร้างสถานะ (Originator), สถานะนั้นหน้าตาเป็นอย่างไร (Memento) และใครตัดสินใจว่าจะเก็บ/คืนสถานะเมื่อไหร่ (Caretaker)
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Originator {
-string state
+Save() Memento
+Restore(Memento m)
}
class Memento {
-string state
+GetState() string
}
class Caretaker {
-List~Memento~ history
+Backup()
+Undo()
}
Originator ..> Memento : creates
Caretaker o-- Memento : stores
Caretaker --> Originator : requests
- Originator — object ที่มีสถานะภายในต้องการเก็บและกู้คืน มี method
Save()เพื่อสร้าง Memento และRestore(m)เพื่อนำสถานะจาก Memento กลับมาใช้ - Memento — วัตถุ immutable ที่เก็บ snapshot ของสถานะ ณ ขณะหนึ่ง โดยปกติเปิดเผย interface แคบมาก (หรือไม่มีเลย) ให้ภายนอก มีแค่ Originator เท่านั้นที่รู้วิธีอ่านเนื้อหาข้างใน
- Caretaker — ผู้ถือครอง Memento (มักเป็น stack หรือ list เพื่อทำ undo/redo) ทำหน้าที่ตัดสินใจว่าจะขอ snapshot เมื่อไหร่ และจะ restore อันไหน แต่ไม่แตะเนื้อหาข้างในมันเลย
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานทั่วไปเมื่อผู้ใช้แก้ไขแล้วกด undo:
sequenceDiagram
participant Caretaker
participant Originator
participant Memento
Caretaker->>Originator: Save()
Originator->>Memento: new Memento(state)
Originator-->>Caretaker: memento
Caretaker->>Caretaker: history.Push(memento)
Note over Originator: ผู้ใช้แก้ไขสถานะต่อ
Caretaker->>Caretaker: history.Pop()
Caretaker->>Originator: Restore(memento)
Originator->>Memento: GetState()
Originator->>Originator: state = ...
- ก่อนจะเปลี่ยนแปลงสถานะที่เสี่ยง (หรือทุกครั้งที่แก้ไขสำเร็จ) Caretaker เรียก
Save()บน Originator - Originator สร้าง Memento ใหม่ที่ห่อหุ้มสถานะปัจจุบันของตัวเอง (มักเป็น deep copy หรือ value ที่ immutable อยู่แล้ว) แล้วคืนกลับให้ Caretaker
- Caretaker เก็บ Memento นั้นไว้ใน collection ของตัวเอง (เช่น undo stack) โดยไม่แกะดูเนื้อหา
- เมื่อจำเป็นต้อง undo Caretaker ดึง Memento ล่าสุดออกมา แล้วส่งกลับให้ Originator ผ่าน
Restore(m) - Originator อ่านสถานะจาก Memento (เพราะมันเป็นผู้สร้าง จึงมีสิทธิ์เข้าถึงเนื้อหาข้างใน) แล้วนำมาตั้งค่าตัวเองใหม่
จุดที่มักถูกมองข้ามคือ Memento ควรเป็น immutable และ Originator ควรทำ deep copy ของสถานะ mutable (เช่น List<T>, object graph) ก่อนห่อใน Memento — ไม่เช่นนั้น Caretaker จะถือ reference ไปยังสถานะที่ยัง “มีชีวิต” อยู่ ทำให้ snapshot เปลี่ยนตามการแก้ไขครั้งถัดไปโดยไม่ตั้งใจ
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่าง text editor ง่าย ๆ ที่รองรับ undo หลายขั้นด้วย Memento + Caretaker:
// Memento: snapshot ที่ปิดผนึก อ่านค่าได้จาก Originator เท่านั้นในทางปฏิบัติpublic sealed record EditorMemento(string Text, int CursorPosition);
// Originator: object ที่มีสถานะภายในต้องเก็บ/กู้คืนpublic class TextEditor{ public string Text { get; private set; } = string.Empty; public int CursorPosition { get; private set; }
public void Type(string content) { Text = Text.Insert(CursorPosition, content); CursorPosition += content.Length; }
// สร้าง snapshot ของสถานะปัจจุบัน public EditorMemento Save() => new(Text, CursorPosition);
// กู้คืนสถานะจาก snapshot ที่ได้รับมา public void Restore(EditorMemento memento) { Text = memento.Text; CursorPosition = memento.CursorPosition; }}
// Caretaker: เก็บประวัติ memento ไว้ทำ undo โดยไม่แตะเนื้อหาข้างในpublic class UndoManager{ private readonly Stack<EditorMemento> _history = new(); private readonly TextEditor _editor;
public UndoManager(TextEditor editor) => _editor = editor;
// เรียกก่อนทำการแก้ไขทุกครั้งที่ต้องการให้ undo ได้ public void Checkpoint() => _history.Push(_editor.Save());
public void Undo() { if (_history.Count == 0) return; _editor.Restore(_history.Pop()); }}
// การใช้งานvar editor = new TextEditor();var undo = new UndoManager(editor);
undo.Checkpoint();editor.Type("Hello");
undo.Checkpoint();editor.Type(", world!");
Console.WriteLine(editor.Text); // "Hello, world!"
undo.Undo();Console.WriteLine(editor.Text); // "Hello"
undo.Undo();Console.WriteLine(editor.Text); // ""ในตัวอย่างนี้ EditorMemento เป็น record ที่ immutable โดยธรรมชาติ (ค่า string และ int เป็น value ที่ copy อยู่แล้ว) จึงไม่ต้องกังวลเรื่อง deep copy แต่ถ้าสถานะภายในเป็น collection หรือ object ที่ mutable ต้อง clone ก่อนใส่ใน memento เสมอ
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการ feature undo/redo หรือ checkpoint ที่ให้ย้อนกลับไปยังสถานะก่อนหน้าได้
- ต้องการ snapshot สถานะก่อนทำ operation ที่เสี่ยง (เช่น transaction) เพื่อ rollback หากล้มเหลว
- การเข้าถึง field ของ object โดยตรงเพื่อ backup จะละเมิด encapsulation หรือทำให้ code ภายนอกผูกติดกับโครงสร้างภายในมากเกินไป
- ต้องการเก็บประวัติสถานะเพื่อ debug, replay หรือ audit (เช่น event log แบบง่าย)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- สถานะของ object มีขนาดใหญ่มากและเปลี่ยนบ่อย — การสร้าง snapshot ทุกครั้งจะกิน memory มหาศาล ควรพิจารณาเก็บเฉพาะ diff/delta แทนทั้งก้อน
- Originator มีความสัมพันธ์กับ object หรือ resource ภายนอก (file, connection, ทรัพยากรภายนอกระบบ) — Memento จับ snapshot ได้เฉพาะสถานะภายในของ object เดียว ไม่ครอบคลุม side effect ภายนอก
- ในภาษาที่ไม่มีกลไกบังคับ immutability หรือ access control ที่เข้มงวด (เช่น dynamic language) การรับประกันว่า Caretaker จะไม่แก้ไข Memento ทำได้ยากกว่า ต้องอาศัยวินัยของทีม
- ระบบมีความต้องการเพียงเก็บ log คำสั่งที่ทำไปแล้วเพื่อ replay/undo ทีละขั้น — กรณีนี้ Command ที่เก็บ inverse operation อาจเหมาะกว่าและประหยัด memory กว่า
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | รักษา encapsulation ได้เต็มที่ เพราะเฉพาะ Originator เท่านั้นที่รู้จักโครงสร้างสถานะของตัวเอง |
| ข้อดี | ทำให้ code ของ Originator เรียบง่ายขึ้น เพราะย้าย logic การจัดเก็บประวัติ/undo ไปไว้ที่ Caretaker แยกต่างหาก |
| ข้อดี | เพิ่มความสามารถ undo/redo, checkpoint หรือ rollback ได้โดยไม่ต้องแก้โครงสร้างภายในของ object เดิม |
| ข้อเสีย | ใช้หน่วยความจำสูงหากสร้าง snapshot บ่อยหรือสถานะมีขนาดใหญ่ |
| ข้อเสีย | Caretaker ต้องดูแลวงจรชีวิตของ memento เอง (เช่น ลบ memento เก่าที่ไม่ใช้แล้ว) ไม่เช่นนั้นจะรั่วไหลของ memory |
| ข้อเสีย | ไม่ครอบคลุมสถานะหรือ side effect ที่อยู่นอก object เดียว (เช่น ทรัพยากรภายนอก, object อื่นที่เกี่ยวข้อง) |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Encapsulation — หลักการที่ Memento ปกป้องไว้ตลอดกระบวนการ snapshot/restore
- Command — มักใช้คู่กันเพื่อทำ undo/redo โดย Command เก็บ action ส่วน Memento เก็บ state
- Prototype — ทั้งสอง pattern เกี่ยวข้องกับการ clone สถานะ แต่ Prototype สร้าง object ใหม่ทั้งตัว ส่วน Memento เก็บ snapshot เพื่อกู้คืน object เดิม
- State — จัดการพฤติกรรมที่เปลี่ยนตามสถานะปัจจุบัน ต่างจาก Memento ที่เก็บสถานะไว้ย้อนกลับ
- Iterator — อีกหนึ่ง pattern เชิงพฤติกรรมที่เข้าถึงข้อมูลภายในของ object โดยไม่ละเมิด encapsulation