1. Memento Pattern là gì?

Memento Pattern là một mẫu thiết kế hành vi cho phép lưu trữ và khôi phục trạng thái trước đó của một đối tượng mà không tiết lộ chi tiết triển khai của nó. Pattern này còn được gọi là “Token Pattern” hoặc “Snapshot Pattern”.

1.1. Đặc điểm chính

  • Cho phép tạo snapshot của trạng thái đối tượng
  • Không vi phạm nguyên tắc đóng gói
  • Dễ dàng thêm chức năng undo/redo
  • Quản lý lịch sử trạng thái một cách hiệu quả

1.2. Structure

Memento Pattern có cấu trúc như sau:


  classDiagram
    class Originator {
        -state
        +getState()
        +setState(state)
        +save(): Memento
        +restore(memento: Memento)
    }

    class Memento {
        <<interface>>
        +getState()
        +getDate()
    }

    class ConcreteMemento {
        -state
        -date
        +getState()
        +getDate()
    }

    class Caretaker {
        -mementos: Memento[]
        -originator: Originator
        +backup()
        +undo()
        +showHistory()
    }

    Originator ..> ConcreteMemento : creates >
    Memento <|.. ConcreteMemento
    Caretaker o--> Memento : stores >
    Caretaker o--> Originator : uses >

    note for Originator "Đối tượng có trạng thái<br>cần lưu trữ và khôi phục"
    note for Memento "Interface định nghĩa<br>phương thức truy cập\ntrạng thái đã lưu"
    note for ConcreteMemento "Lưu trữ trạng thái<br>của Originator"
    note for Caretaker "Quản lý danh sách<br>các memento"

Các thành phần chính của Memento Pattern:

  1. Originator: Đối tượng có trạng thái cần lưu trữ và khôi phục
  2. Memento: Đối tượng lưu trữ trạng thái của Originator
  3. ConcreteMemento: Triển khai cụ thể của Memento
  4. Caretaker: Quản lý danh sách các memento, nhưng không thay đổi nội dung của chúng

1.3. Mã ví dụ

// Originator - đối tượng có trạng thái cần lưu trữ
class Originator {
  private state: string;

  constructor(state: string) {
    this.state = state;
  }

  public getState(): string {
    return this.state;
  }

  public setState(state: string): void {
    this.state = state;
  }

  // Tạo memento
  public save(): Memento {
    return new ConcreteMemento(this.state);
  }

  // Khôi phục từ memento
  public restore(memento: Memento): void {
    this.state = memento.getState();
  }
}

// Memento interface
interface Memento {
  getState(): string;
  getDate(): string;
}

// Concrete Memento
class ConcreteMemento implements Memento {
  private state: string;
  private date: string;

  constructor(state: string) {
    this.state = state;
    this.date = new Date().toISOString();
  }

  public getState(): string {
    return this.state;
  }

  public getDate(): string {
    return this.date;
  }
}

// Caretaker - quản lý lịch sử các memento
class Caretaker {
  private mementos: Memento[] = [];
  private originator: Originator;

  constructor(originator: Originator) {
    this.originator = originator;
  }

  public backup(): void {
    this.mementos.push(this.originator.save());
  }

  public undo(): void {
    if (this.mementos.length === 0) {
      return;
    }

    const memento = this.mementos.pop();
    if (memento) {
      this.originator.restore(memento);
    }
  }
}

2. Khi nào nên sử dụng Memento Pattern?

2.1. Các trường hợp nên sử dụng

  • Khi cần tạo snapshot của trạng thái đối tượng
  • Khi cần triển khai chức năng undo/redo
  • Khi cần lưu trữ checkpoint trong game hoặc ứng dụng
  • Khi muốn tránh vi phạm nguyên tắc đóng gói

2.2. Ví dụ thực tế: Text Editor

// Text Editor với chức năng undo/redo
class TextEditor {
  private content: string = "";

  getContent(): string {
    return this.content;
  }

  type(text: string): void {
    this.content += text;
  }

  save(): EditorMemento {
    return new EditorMemento(this.content);
  }

  restore(memento: EditorMemento): void {
    this.content = memento.getContent();
  }
}

class EditorMemento {
  private readonly content: string;
  private readonly timestamp: string;

  constructor(content: string) {
    this.content = content;
    this.timestamp = new Date().toISOString();
  }

  getContent(): string {
    return this.content;
  }

  getTimestamp(): string {
    return this.timestamp;
  }
}

class EditorHistory {
  private mementos: EditorMemento[] = [];
  private editor: TextEditor;

  constructor(editor: TextEditor) {
    this.editor = editor;
  }

  save(): void {
    this.mementos.push(this.editor.save());
  }

  undo(): void {
    if (this.mementos.length === 0) {
      return;
    }

    const memento = this.mementos.pop();
    if (memento) {
      this.editor.restore(memento);
    }
  }
}

// Usage
const editor = new TextEditor();
const history = new EditorHistory(editor);

editor.type("Hello");
history.save();

editor.type(" World");
history.save();

editor.type("!");
console.log(editor.getContent()); // "Hello World!"

history.undo();
console.log(editor.getContent()); // "Hello World"

history.undo();
console.log(editor.getContent()); // "Hello"

3. Triển khai Memento Pattern trong JavaScript / TypeScript

3.1. Ví dụ về Game State

interface GameState {
  level: number;
  score: number;
  position: { x: number; y: number };
  inventory: string[];
}

class Game {
  private state: GameState;

  constructor() {
    this.state = {
      level: 1,
      score: 0,
      position: { x: 0, y: 0 },
      inventory: [],
    };
  }

  public movePlayer(x: number, y: number): void {
    this.state.position = { x, y };
  }

  public collectItem(item: string): void {
    this.state.inventory.push(item);
    this.state.score += 10;
  }

  public levelUp(): void {
    this.state.level++;
  }

  public save(): GameMemento {
    // structuredClone (global, Node 17+, Chrome 98+, Firefox 94+, Safari 15.4+)
    // xử lý đúng Date, Map, Set, RegExp, TypedArray, và cycles — khác JSON.stringify
    return new GameMemento(structuredClone(this.state));
  }

  public restore(memento: GameMemento): void {
    this.state = structuredClone(memento.getState());
  }

  public getState(): GameState {
    return this.state;
  }
}

class GameMemento {
  private state: GameState;
  private timestamp: string;

  constructor(state: GameState) {
    this.state = state;
    this.timestamp = new Date().toISOString();
  }

  getState(): GameState {
    return this.state;
  }

  getTimestamp(): string {
    return this.timestamp;
  }
}

class GameHistory {
  private mementos: GameMemento[] = [];
  private game: Game;

  constructor(game: Game) {
    this.game = game;
  }

  createCheckpoint(): void {
    this.mementos.push(this.game.save());
  }

  restoreCheckpoint(): void {
    const memento = this.mementos.pop();
    if (memento) {
      this.game.restore(memento);
    }
  }

  getCheckpoints(): GameMemento[] {
    return this.mementos;
  }
}

// Usage
const game = new Game();
const history = new GameHistory(game);

// Playing the game
game.movePlayer(10, 20);
game.collectItem("Sword");
history.createCheckpoint();

game.movePlayer(30, 40);
game.collectItem("Shield");
game.levelUp();
console.log(game.getState());
// { level: 2, score: 20, position: { x: 30, y: 40 }, inventory: ['Sword', 'Shield'] }

// Restoring to previous checkpoint
history.restoreCheckpoint();
console.log(game.getState());
// { level: 1, score: 10, position: { x: 10, y: 20 }, inventory: ['Sword'] }

3.2. Ví dụ về Form State Management

interface FormState {
  values: Record<string, string>;
  errors: Record<string, string>;
  isDirty: boolean;
}

class FormManager {
  private state: FormState;

  constructor() {
    this.state = {
      values: {},
      errors: {},
      isDirty: false,
    };
  }

  updateField(field: string, value: string): void {
    this.state.values[field] = value;
    this.state.isDirty = true;
  }

  setError(field: string, error: string): void {
    this.state.errors[field] = error;
  }

  save(): FormMemento {
    return new FormMemento({ ...this.state });
  }

  restore(memento: FormMemento): void {
    this.state = { ...memento.getState() };
  }

  getState(): FormState {
    return this.state;
  }
}

class FormMemento {
  private state: FormState;
  private timestamp: string;

  constructor(state: FormState) {
    this.state = state;
    this.timestamp = new Date().toISOString();
  }

  getState(): FormState {
    return this.state;
  }

  getTimestamp(): string {
    return this.timestamp;
  }
}

// Usage
const form = new FormManager();
const snapshots: FormMemento[] = [];

form.updateField("name", "John");
snapshots.push(form.save());

form.updateField("email", "[email protected]");
form.setError("email", "Invalid email");
snapshots.push(form.save());

console.log(form.getState());
// { values: { name: 'John', email: '[email protected]' },
//   errors: { email: 'Invalid email' },
//   isDirty: true }

// Restore to first snapshot
form.restore(snapshots[0]);
console.log(form.getState());
// { values: { name: 'John' }, errors: {}, isDirty: true }

4. Ưu điểm và nhược điểm

4.1. Ưu điểm

  1. Đóng gói: Không vi phạm nguyên tắc đóng gói của đối tượng
  2. Đơn giản: Dễ dàng triển khai và sử dụng
  3. Linh hoạt: Có thể lưu trữ nhiều trạng thái khác nhau
  4. Bảo mật: Trạng thái được lưu trữ an toàn bên ngoài đối tượng gốc

4.2. Nhược điểm

  1. Bộ nhớ: Có thể tiêu tốn nhiều bộ nhớ khi lưu trữ nhiều trạng thái
  2. Hiệu suất: Việc tạo và khôi phục trạng thái có thể tốn thời gian
  3. Quản lý: Cần quản lý cẩn thận các memento để tránh rò rỉ bộ nhớ

5. Best Practices và Lưu ý

5.1. Khi nào nên sử dụng

  • Khi cần lưu trữ trạng thái tạm thời
  • Khi triển khai chức năng undo/redo
  • Khi cần tạo snapshot của hệ thống
  • Khi muốn tránh vi phạm đóng gói

5.2. Tips và Tricks

  1. Quản lý bộ nhớ: Giới hạn số lượng memento được lưu trữ
  2. Deep Copy: Sử dụng deep copy để tránh tham chiếu chung
  3. Immutable State: Nên sử dụng immutable state khi có thể
  4. Serialization: Cân nhắc serialization để lưu trữ lâu dài

5.3. Cách deep copy state — so sánh

CáchƯu điểmNhược điểm
structuredClone(obj)Native, nhanh, xử lý Date/Map/Set/RegExp/TypedArray, giữ cyclesKhông copy function, DOM node, class prototype; chỉ cloneable type
JSON.parse(JSON.stringify(obj))Đơn giản, serialize được để lưu diskMất Date (thành string), mất Map/Set/undefined, lỗi với cycle; chậm hơn structuredClone
Immutable library (Immer, ImmutableJS)Structural sharing → tiết kiệm RAM khi nhiều snapshotCần học API; overhead nhỏ
Copy thủ công từng fieldKiểm soát hoàn toàn, không copy thừaDễ quên field khi schema thay đổi

Khuyến nghị 2025:

  • Mặc định: structuredClone — chạy trên mọi modern runtime (browser + Node 17+) và là native global.
  • Snapshot để lưu disk/DB: JSON.stringify, nhưng phải định nghĩa rõ schema (Date ISO, Map → entries, v.v.).
  • State app lớn với nhiều undo/redo (editor, game): Immer cho cảm giác mutable nhưng thực tế immutable + structural sharing → memory scale tốt hơn structuredClone mỗi lần save.
// Ví dụ ba cách trên cùng 1 state
const state = { createdAt: new Date(), tags: new Set(["a", "b"]) };

// 1. structuredClone — giữ đúng type
const snap1 = structuredClone(state);
console.log(snap1.createdAt instanceof Date); // true
console.log(snap1.tags instanceof Set); // true

// 2. JSON round-trip — mất type
const snap2 = JSON.parse(JSON.stringify(state));
console.log(snap2.createdAt); // string "2025-..."
console.log(snap2.tags); // {} (Set serialize thành object rỗng)

// 3. Immer — immutable với mutable-style API
// npm i immer
// import { produce } from "immer";
// const next = produce(state, (draft) => { draft.tags.add("c"); });

6. Kết luận

Memento Pattern là một công cụ mạnh mẽ cho việc quản lý trạng thái trong ứng dụng JavaScript / TypeScript. Pattern này đặc biệt hữu ích khi cần lưu trữ và khôi phục trạng thái của đối tượng một cách an toàn và hiệu quả.

Tuy nhiên, cần cân nhắc kỹ về việc quản lý bộ nhớ và hiệu suất khi sử dụng pattern này, đặc biệt là trong các ứng dụng có nhiều trạng thái cần lưu trữ.