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

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 = ...
  1. ก่อน​จะ​เปลี่ยนแปลง​สถานะ​ที่​เสี่ยง (หรือ​ทุก​ครั้ง​ที่​แก้ไข​สำเร็จ) Caretaker เรียก Save() บน Originator
  2. Originator สร้าง Memento ใหม่​ที่​ห่อ​หุ้ม​สถานะ​ปัจจุบัน​ของ​ตัวเอง (มัก​เป็น deep copy หรือ value ที่ immutable อยู่​แล้ว) แล้ว​คืน​กลับ​ให้ Caretaker
  3. Caretaker เก็บ Memento นั้น​ไว้​ใน collection ของ​ตัวเอง (เช่น undo stack) โดย​ไม่​แกะ​ดู​เนื้อหา
  4. เมื่อ​จำเป็น​ต้อง undo Caretaker ดึง Memento ล่าสุด​ออก​มา แล้ว​ส่ง​กลับ​ให้ Originator ผ่าน Restore(m)
  5. Originator อ่าน​สถานะ​จาก Memento (เพราะ​มัน​เป็น​ผู้​สร้าง จึง​มี​สิทธิ์​เข้าถึง​เนื้อหา​ข้าง​ใน) แล้ว​นำ​มา​ตั้ง​ค่าตัวเอง​ใหม่

จุด​ที่​มัก​ถูก​มอง​ข้าม​คือ Memento ควร​เป็น immutable และ Originator ควร​ทำ deep copy ของ​สถานะ mutable (เช่น List<T>, object graph) ก่อน​ห่อ​ใน Memento — ไม่​เช่น​นั้น Caretaker จะ​ถือ reference ไป​ยัง​สถานะ​ที่​ยัง “มี​ชีวิต” อยู่ ทำให้ snapshot เปลี่ยน​ตาม​การ​แก้ไข​ครั้ง​ถัด​ไป​โดย​ไม่​ตั้งใจ

ตัวอย่าง 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