ข้ามไปยังเนื้อหา

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
originator สร้าง memento ส่วน caretaker เก็บไว้โดยไม่ตรวจสอบข้างใน
  • 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()); // hello
  • ข้อดี: เปิดทางให้ทำ undo, rollback, และ checkpoint ได้โดยไม่เปิดเผยฟิลด์ภายในของ originator
  • ข้อดี: originator ยังคงควบคุมได้เต็มที่ว่าจะบันทึกอะไร encapsulation จึงยังคงอยู่ครบถ้วน
  • ข้อเสีย: การเก็บภาพถ่ายเต็มจำนวนมากอาจสิ้นเปลืองหน่วยความจำ สถานะขนาดใหญ่อาจต้องใช้การ diff หรือกำหนดขีดจำกัด
  • ข้อเสีย: caretaker ต้องจัดการอายุของ memento เอง ถ้าไม่เคยลบทิ้ง ภาพถ่ายเก่าจะพอกพูนขึ้นเรื่อย ๆ
  • Command กับ Memento เป็นคู่หูธรรมชาติของงาน undo โดย command เก็บ memento ของสถานะที่ตัวเองไปเปลี่ยนเอาไว้ แล้วกู้คืนตอนย้อนกลับ
  • Iterator ใช้ memento จับเก็บตำแหน่งปัจจุบันได้ เมื่อสถานะการเดินผ่านต้องอยู่นอกตัว iterator
MementoCommandPrototype
จุดประสงค์บันทึกและกู้คืน stateห่อ action เพื่อ undo/queuecopy object ที่มีอยู่
เก็บอะไรsnapshot ของ stateaction + ข้อมูลที่ต้องการทั้ง object
undo mechanismกู้ snapshot เก่าเรียก undo() ของ commandไม่ใช่ design สำหรับ undo
ตัวอย่างeditor history, game save, form drafteditor action, HTTP retryObject.assign(), clone()
Memento pattern ปกป้องอะไรระหว่างที่บันทึกสถานะ?
บทบาทของ caretaker คืออะไร?
pattern ใดที่จับคู่กับ Memento บ่อยที่สุดเพื่อทำ undo?