Memento
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Memento จับเก็บสถานะภายในของ object ลงใน token แยกต่างหาก เพื่อให้กู้คืนสถานะนั้นภายหลังได้ โดยไม่เปิดเผยว่าเก็บสถานะไว้อย่างไร นี่คือ pattern ที่อยู่เบื้องหลัง undo
text editor ต้องมีคำสั่ง undo เพื่อกู้คืนสถานะก่อนหน้า แปลว่าต้องมีอะไรสักอย่างจำได้ว่าเอกสารหน้าตาเป็นอย่างไรก่อนการแก้ไขแต่ละครั้ง วิธีที่เห็นชัดที่สุดคือปล่อยให้กลไก undo อ่านและเขียน private field ของ editor ตรง ๆ แต่วิธีนี้ทำลาย encapsulation เพราะ code ฝั่งประวัติจะไปผูกกับ layout ภายในของ editor พอ layout เปลี่ยนเมื่อไหร่ ผลกระทบก็กระเพื่อมออกมาข้างนอกทันที
Memento เก็บภาพถ่าย (snapshot) แบบทึบแสง object ที่คุณต้องการบันทึกสถานะ (originator) เป็นคนสร้าง memento ที่จับเก็บสถานะของตัวเอง ส่วน caretaker แค่ถือ memento ไว้เหมือนกล่องที่ปิดผนึก จะส่งกล่องคืนให้ originator เพื่อกู้คืนก็ได้ แต่แอบมองข้างในไม่ได้ ผลคือ originator ยังคุมได้เต็มที่ว่าจะบันทึกอะไรและบันทึกอย่างไร ส่วนภายในยังคงเป็น private อยู่ แม้จะมีประวัติเก็บไว้ครบทุกขั้นก็ตาม
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Editor {
-content: string
+type(text)
+save() Memento
+restore(m Memento)
}
class Memento {
-state: string
+getState() string
}
class History {
-snapshots: List~Memento~
+push(m Memento)
+pop() Memento
}
Editor ..> Memento : creates
History o-- Memento : stores
Editor ..> History : uses - Editor (Originator) — object เจ้าของสถานะที่ต้องรักษาไว้ สร้าง memento เพื่อจับเก็บสถานะปัจจุบัน และกู้คืนตัวเองจาก memento ได้
- Memento — snapshot ทึบแสง มีแต่ originator ที่เข้าใจเนื้อหาข้างใน คนอื่นเห็นเป็นแค่กล่องดำ
- History (Caretaker) — เก็บ stack ของ memento และตัดสินใจว่าจะบันทึกเมื่อใดและจะย้อนกลับเมื่อใด โดยไม่อ่านภาพถ่ายเหล่านั้น
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”editor จิ๋วที่ทำ undo ได้ การบันทึกแต่ละครั้งจะ push ภาพถ่ายขึ้น stack ประวัติ ส่วน undo จะ pop ภาพถ่ายล่าสุดออกมาแล้วกู้คืนสถานะนั้น
class Memento { constructor(readonly state: string) {}}
class Editor { private content = '';
type(text: string): void { this.content += text; }
read(): string { return this.content; }
save(): Memento { return new Memento(this.content); }
restore(m: Memento): void { this.content = m.state; }}
class History { private snapshots: Memento[] = []; push(m: Memento): void { this.snapshots.push(m); } pop(): Memento | undefined { return this.snapshots.pop(); }}
const editor = new Editor();const history = new History();
editor.type('hello ');history.push(editor.save());editor.type('world');console.log(editor.read()); // hello world
const snapshot = history.pop();if (snapshot) editor.restore(snapshot);console.log(editor.read()); // hellofrom __future__ import annotationsfrom dataclasses import dataclass
@dataclass(frozen=True)class Memento: state: str
class Editor: def __init__(self) -> None: self._content = ""
def type(self, text: str) -> None: self._content += text
def read(self) -> str: return self._content
def save(self) -> Memento: return Memento(self._content)
def restore(self, m: Memento) -> None: self._content = m.state
class History: def __init__(self) -> None: self._snapshots: list[Memento] = []
def push(self, m: Memento) -> None: self._snapshots.append(m)
def pop(self) -> Memento | None: return self._snapshots.pop() if self._snapshots else None
editor = Editor()history = History()
editor.type("hello ")history.push(editor.save())editor.type("world")print(editor.read()) # hello world
snapshot = history.pop()if snapshot: editor.restore(snapshot)print(editor.read()) # hellopackage main
import "fmt"
// Memento is an opaque snapshot of the editor's state.type Memento struct { state string}
type Editor struct { content string}
func (e *Editor) Type(text string) { e.content += text}
func (e *Editor) Read() string { return e.content}
func (e *Editor) Save() Memento { return Memento{state: e.content}}
func (e *Editor) Restore(m Memento) { e.content = m.state}
type History struct { snapshots []Memento}
func (h *History) Push(m Memento) { h.snapshots = append(h.snapshots, m)}
func (h *History) Pop() (Memento, bool) { if len(h.snapshots) == 0 { return Memento{}, false } last := h.snapshots[len(h.snapshots)-1] h.snapshots = h.snapshots[:len(h.snapshots)-1] return last, true}
func main() { editor := &Editor{} history := &History{}
editor.Type("hello ") history.Push(editor.Save()) editor.Type("world") fmt.Println(editor.Read()) // hello world
if snapshot, ok := history.Pop(); ok { editor.Restore(snapshot) } fmt.Println(editor.Read()) // hello}// The memento is a plain value; only the editor knows what it means.#[derive(Clone)]struct Memento { state: String,}
struct Editor { content: String,}
impl Editor { fn new() -> Self { Editor { content: String::new() } }
fn type_text(&mut self, text: &str) { self.content.push_str(text); }
fn save(&self) -> Memento { Memento { state: self.content.clone() } }
fn restore(&mut self, m: Memento) { self.content = m.state; }}
fn main() { let mut editor = Editor::new(); let mut history: Vec<Memento> = Vec::new();
editor.type_text("hello "); history.push(editor.save()); editor.type_text("world"); println!("{}", editor.content); // hello world
if let Some(snapshot) = history.pop() { editor.restore(snapshot); } println!("{}", editor.content); // hello}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: เปิดทางให้ทำ undo, rollback, และ checkpoint ได้โดยไม่เปิดเผยฟิลด์ภายในของ originator
- ข้อดี: originator ยังคงควบคุมได้เต็มที่ว่าจะบันทึกอะไร encapsulation จึงยังคงอยู่ครบถ้วน
- ข้อเสีย: การเก็บภาพถ่ายเต็มจำนวนมากอาจสิ้นเปลืองหน่วยความจำ สถานะขนาดใหญ่อาจต้องใช้การ diff หรือกำหนดขีดจำกัด
- ข้อเสีย: caretaker ต้องจัดการอายุของ memento เอง ถ้าไม่เคยลบทิ้ง ภาพถ่ายเก่าจะพอกพูนขึ้นเรื่อย ๆ
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Command กับ Memento เป็นคู่หูธรรมชาติของงาน undo โดย command เก็บ memento ของสถานะที่ตัวเองไปเปลี่ยนเอาไว้ แล้วกู้คืนตอนย้อนกลับ
- Iterator ใช้ memento จับเก็บตำแหน่งปัจจุบันได้ เมื่อสถานะการเดินผ่านต้องอยู่นอกตัว iterator
| Memento | Command | Prototype | |
|---|---|---|---|
| จุดประสงค์ | บันทึกและกู้คืน state | ห่อ action เพื่อ undo/queue | copy object ที่มีอยู่ |
| เก็บอะไร | snapshot ของ state | action + ข้อมูลที่ต้องการ | ทั้ง object |
| undo mechanism | กู้ snapshot เก่า | เรียก undo() ของ command | ไม่ใช่ design สำหรับ undo |
| ตัวอย่าง | editor history, game save, form draft | editor action, HTTP retry | Object.assign(), clone() |