Memento Pattern #

Seorang pengguna mengisi form pendaftaran multi-step yang panjang — data pribadi, data perusahaan, preferensi produk, upload dokumen. Di step keempat, ia menyadari ada yang perlu diubah di step pertama. Tanpa mekanisme snapshot, satu-satunya pilihan adalah kembali ke awal dan mengisi ulang semuanya. Dengan Memento Pattern, setiap perpindahan ke step berikutnya menyimpan snapshot state saat itu; kembali ke step manapun berarti memulihkan snapshot tersebut, bukan mengulang dari awal. Kode yang menyimpan dan memulihkan snapshot tidak perlu tahu apa isi state itu atau bagaimana form merepresentasikannya — enkapsulasi tetap terjaga karena objek Memento hanya bisa dimanfaatkan oleh objek yang membuatnya.

Apa itu Memento Pattern? #

Memento Pattern adalah behavioral design pattern yang memungkinkan kamu menyimpan dan memulihkan state sebelumnya dari sebuah objek tanpa mengekspos detail implementasi internalnya. Objek yang state-nya ingin disimpan membuat “snapshot” dalam bentuk objek Memento; snapshot ini bisa disimpan oleh Caretaker dan dikembalikan ke originator kapan saja untuk memulihkan state sebelumnya.

Tiga properti yang mendefinisikan Memento Pattern:

  • Enkapsulasi terjaga — Caretaker yang menyimpan Memento tidak bisa membaca atau memodifikasi isi state; hanya Originator yang bisa membuatnya dan memulihkannya
  • Snapshot yang lengkap — Memento menyimpan semua yang diperlukan untuk mengembalikan objek ke kondisi persis saat snapshot dibuat
  • Pemisahan tanggung jawab — Originator tahu apa yang perlu disimpan; Caretaker tahu kapan dan berapa snapshot yang disimpan
sequenceDiagram
    participant C as Caretaker
    participant O as Originator (Form)
    participant M as Memento (Snapshot)

    C->>O: Minta snapshot
    O->>M: CreateMemento() — buat snapshot state
    O-->>C: kembalikan Memento
    C->>C: simpan Memento di stack

    Note over O: User ubah state

    C->>O: Restore(memento)
    O->>M: Baca state dari Memento
    M-->>O: state sebelumnya
    Note over O: State kembali ke snapshot

Perbedaan dengan Command Pattern untuk Undo #

Memento dan Command Pattern sama-sama mendukung undo, tapi dengan cara yang berbeda. Memilih yang tepat tergantung karakteristik state yang perlu di-undo.

// Command Pattern untuk Undo:
// Simpan AKSI yang dilakukan + cara membaliknya
type InsertCommand struct {
    editor   *Editor
    char     rune
    position int
}
func (c *InsertCommand) Undo() error {
    // Balikkan aksi: hapus karakter yang baru saja disisipkan
    _, err := c.editor.Delete(c.position)
    return err
}
// Cocok untuk: aksi diskret yang mudah dibalik secara logis

// Memento Pattern untuk Undo:
// Simpan SNAPSHOT STATE sebelum aksi
type EditorMemento struct {
    content string
    cursor  int
}
// Undo = pulihkan snapshot
// Cocok untuk: state kompleks yang sulit dibalik aksinya satu per satu
Aspek Command Pattern Memento Pattern
Yang disimpan Aksi + cara membaliknya Snapshot state
Cocok untuk Aksi diskret, mudah di-invert State kompleks, susah di-invert
Memori Lebih hemat — hanya delta Lebih boros — seluruh state
Partial undo Bisa undo sebagian aksi Harus restore seluruh snapshot
Contoh Text insert/delete Form wizard, game save

Tiga Komponen Memento Pattern #

classDiagram
    class FormWizard {
        -currentStep int
        -personalData PersonalData
        -companyData CompanyData
        -preferences Preferences
        +CreateMemento() WizardMemento
        +RestoreFromMemento(m WizardMemento)
        +NextStep()
        +PrevStep()
    }

    class WizardMemento {
        -step int
        -personalData PersonalData
        -companyData CompanyData
        -preferences Preferences
        -createdAt time.Time
        +CreatedAt() time.Time
        +StepNumber() int
    }

    class WizardHistory {
        -snapshots []WizardMemento
        -maxSize int
        +Save(originator FormWizard)
        +Undo(originator FormWizard)
        +Count() int
    }

    FormWizard ..> WizardMemento : creates
    FormWizard <.. WizardMemento : restores state
    WizardHistory o-- WizardMemento : manages
    WizardHistory --> FormWizard : interacts with
Komponen Peran Yang Boleh Akses
Originator Membuat dan memulihkan Memento Semua field Memento (akses penuh)
Memento Menyimpan snapshot state Originator Originator saja — field unexported
Caretaker Menyimpan dan mengembalikan Memento Hanya interface Memento — tidak bisa baca state

Implementasi Lengkap: Form Wizard Multi-Step #

Originator: FormWizard #

package wizard

import (
    "fmt"
    "time"
)

// PersonalData menyimpan data pribadi pengguna.
type PersonalData struct {
    FullName    string
    Email       string
    PhoneNumber string
    BirthDate   string
}

// CompanyData menyimpan data perusahaan.
type CompanyData struct {
    CompanyName string
    Industry    string
    Size        string
    Website     string
}

// Preferences menyimpan preferensi produk pengguna.
type Preferences struct {
    ProductTier   string
    BillingCycle  string
    Addons        []string
    NewsletterOpt bool
}

// WizardMemento menyimpan snapshot state wizard pada satu titik waktu.
// Field-field ini unexported — hanya FormWizard yang bisa membaca dan menulisnya.
type WizardMemento struct {
    step         int
    personal     PersonalData
    company      CompanyData
    preferences  Preferences
    createdAt    time.Time
}

// CreatedAt mengembalikan waktu snapshot dibuat — satu-satunya method yang bisa diakses Caretaker.
func (m *WizardMemento) CreatedAt() time.Time { return m.createdAt }

// StepNumber mengembalikan step saat snapshot dibuat.
func (m *WizardMemento) StepNumber() int { return m.step }

// FormWizard adalah Originator — objek yang state-nya ingin disimpan.
type FormWizard struct {
    currentStep int
    personal    PersonalData
    company     CompanyData
    preferences Preferences
    maxSteps    int
}

func NewFormWizard() *FormWizard {
    return &FormWizard{
        currentStep: 1,
        maxSteps:    4,
    }
}

// CreateMemento membuat snapshot dari state wizard saat ini.
// Deep copy dilakukan untuk memastikan Memento independen dari state wizard.
func (w *FormWizard) CreateMemento() *WizardMemento {
    // Deep copy Preferences.Addons (slice — perlu disalin)
    addonsCopy := make([]string, len(w.preferences.Addons))
    copy(addonsCopy, w.preferences.Addons)

    return &WizardMemento{
        step: w.currentStep,
        personal: PersonalData{
            FullName:    w.personal.FullName,
            Email:       w.personal.Email,
            PhoneNumber: w.personal.PhoneNumber,
            BirthDate:   w.personal.BirthDate,
        },
        company: CompanyData{
            CompanyName: w.company.CompanyName,
            Industry:    w.company.Industry,
            Size:        w.company.Size,
            Website:     w.company.Website,
        },
        preferences: Preferences{
            ProductTier:   w.preferences.ProductTier,
            BillingCycle:  w.preferences.BillingCycle,
            Addons:        addonsCopy,
            NewsletterOpt: w.preferences.NewsletterOpt,
        },
        createdAt: time.Now(),
    }
}

// RestoreFromMemento memulihkan state wizard dari Memento.
func (w *FormWizard) RestoreFromMemento(m *WizardMemento) {
    // Deep copy kembali untuk menghindari sharing slice
    addonsCopy := make([]string, len(m.preferences.Addons))
    copy(addonsCopy, m.preferences.Addons)

    w.currentStep    = m.step
    w.personal       = m.personal
    w.company        = m.company
    w.preferences    = m.preferences
    w.preferences.Addons = addonsCopy
}

// Step 1: Data Pribadi
func (w *FormWizard) SetPersonalData(data PersonalData) error {
    if data.FullName == "" || data.Email == "" {
        return fmt.Errorf("full name and email are required")
    }
    w.personal = data
    return nil
}

// Step 2: Data Perusahaan
func (w *FormWizard) SetCompanyData(data CompanyData) error {
    if data.CompanyName == "" {
        return fmt.Errorf("company name is required")
    }
    w.company = data
    return nil
}

// Step 3: Preferensi
func (w *FormWizard) SetPreferences(prefs Preferences) error {
    if prefs.ProductTier == "" {
        return fmt.Errorf("product tier must be selected")
    }
    w.preferences = prefs
    return nil
}

// NextStep maju ke step berikutnya.
func (w *FormWizard) NextStep() error {
    if w.currentStep >= w.maxSteps {
        return fmt.Errorf("already at final step")
    }
    w.currentStep++
    return nil
}

// PrevStep mundur ke step sebelumnya.
func (w *FormWizard) PrevStep() error {
    if w.currentStep <= 1 {
        return fmt.Errorf("already at first step")
    }
    w.currentStep--
    return nil
}

// CurrentStep mengembalikan step saat ini.
func (w *FormWizard) CurrentStep() int { return w.currentStep }

// Summary mengembalikan ringkasan data wizard saat ini.
func (w *FormWizard) Summary() string {
    return fmt.Sprintf("Step %d/%d | %s (%s) | %s | %s",
        w.currentStep, w.maxSteps,
        w.personal.FullName, w.personal.Email,
        w.company.CompanyName,
        w.preferences.ProductTier,
    )
}

Caretaker: WizardHistory #

package wizard

import (
    "fmt"
    "time"
)

// SnapshotInfo menyimpan metadata snapshot untuk ditampilkan ke user.
type SnapshotInfo struct {
    Index     int
    Step      int
    CreatedAt time.Time
}

// WizardHistory adalah Caretaker — menyimpan dan mengelola Memento.
// Tidak bisa membaca state di dalam Memento — hanya menyimpan dan mengembalikannya.
type WizardHistory struct {
    snapshots []*WizardMemento
    maxSize   int
}

func NewWizardHistory(maxSize int) *WizardHistory {
    return &WizardHistory{
        snapshots: make([]*WizardMemento, 0, maxSize),
        maxSize:   maxSize,
    }
}

// Save menyimpan snapshot state wizard saat ini.
func (h *WizardHistory) Save(wizard *FormWizard) {
    if len(h.snapshots) >= h.maxSize {
        // Hapus snapshot terlama untuk memberi ruang
        h.snapshots = h.snapshots[1:]
    }
    h.snapshots = append(h.snapshots, wizard.CreateMemento())
    fmt.Printf("[History] Snapshot disimpan untuk step %d (total: %d)\n",
        wizard.CurrentStep(), len(h.snapshots))
}

// Undo memulihkan snapshot terakhir ke wizard.
func (h *WizardHistory) Undo(wizard *FormWizard) error {
    if len(h.snapshots) == 0 {
        return fmt.Errorf("tidak ada snapshot yang tersedia untuk di-restore")
    }
    // Pop snapshot terakhir
    last := h.snapshots[len(h.snapshots)-1]
    h.snapshots = h.snapshots[:len(h.snapshots)-1]

    wizard.RestoreFromMemento(last)
    fmt.Printf("[History] State dipulihkan ke step %d (sisa snapshot: %d)\n",
        last.StepNumber(), len(h.snapshots))
    return nil
}

// GoToStep memulihkan snapshot untuk step tertentu.
func (h *WizardHistory) GoToStep(wizard *FormWizard, targetStep int) error {
    for i := len(h.snapshots) - 1; i >= 0; i-- {
        if h.snapshots[i].StepNumber() == targetStep {
            wizard.RestoreFromMemento(h.snapshots[i])
            // Hapus semua snapshot setelah yang ini
            h.snapshots = h.snapshots[:i+1]
            return nil
        }
    }
    return fmt.Errorf("tidak ada snapshot untuk step %d", targetStep)
}

// ListSnapshots mengembalikan daftar snapshot yang tersimpan untuk ditampilkan ke user.
// Caretaker tidak bisa mengakses state di dalam Memento — hanya metadata.
func (h *WizardHistory) ListSnapshots() []SnapshotInfo {
    info := make([]SnapshotInfo, len(h.snapshots))
    for i, snap := range h.snapshots {
        info[i] = SnapshotInfo{
            Index:     i,
            Step:      snap.StepNumber(),
            CreatedAt: snap.CreatedAt(),
        }
    }
    return info
}

// Count mengembalikan jumlah snapshot yang tersimpan.
func (h *WizardHistory) Count() int { return len(h.snapshots) }

Demonstrasi #

func main() {
    wizard := wizard.NewFormWizard()
    history := wizard.NewWizardHistory(10)

    // Step 1: Isi data pribadi dan simpan snapshot
    _ = wizard.SetPersonalData(wizard.PersonalData{
        FullName:    "Budi Santoso",
        Email:       "[email protected]",
        PhoneNumber: "+6281234567890",
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("Setelah step 1: %s\n", wizard.Summary())

    // Step 2: Isi data perusahaan dan simpan snapshot
    _ = wizard.SetCompanyData(wizard.CompanyData{
        CompanyName: "PT Maju Bersama",
        Industry:    "Technology",
        Size:        "51-200",
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("Setelah step 2: %s\n", wizard.Summary())

    // Step 3: Isi preferensi
    _ = wizard.SetPreferences(wizard.Preferences{
        ProductTier:  "Enterprise",
        BillingCycle: "annual",
        Addons:       []string{"sso", "audit-log"},
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("Setelah step 3: %s\n", wizard.Summary())

    // User ingin kembali ke step 1 untuk koreksi email
    fmt.Println("\n--- User ingin kembali ke step 1 ---")
    _ = history.GoToStep(wizard, 1)
    fmt.Printf("Setelah kembali ke step 1: %s\n", wizard.Summary())

    // Koreksi email
    _ = wizard.SetPersonalData(wizard.PersonalData{
        FullName:    "Budi Santoso",
        Email:       "[email protected]", // email baru
        PhoneNumber: "+6281234567890",
    })
    fmt.Printf("Setelah koreksi email: %s\n", wizard.Summary())

    // Undo satu langkah
    fmt.Println("\n--- Undo ---")
    _ = history.Undo(wizard)
    fmt.Printf("Setelah undo: %s\n", wizard.Summary())
}

Studi Kasus Kedua: Game Checkpoint System #

Game adalah use case Memento yang paling klasik — setiap checkpoint menyimpan seluruh state game (posisi player, health, inventory, progress level) agar player bisa kembali ke checkpoint ketika mati.

package game

import (
    "fmt"
    "time"
)

// PlayerState menyimpan state lengkap player untuk snapshot.
type PlayerState struct {
    Level     int
    Health    int
    MaxHealth int
    Position  Position
    Inventory []Item
    Gold      int
    Skills    map[string]int
    QuestLog  []Quest
}

// Position merepresentasikan posisi player di dunia game.
type Position struct {
    X, Y, Z float64
    Map      string
}

// Item merepresentasikan satu item dalam inventory.
type Item struct {
    ID       string
    Name     string
    Quantity int
    Equipped bool
}

// Quest merepresentasikan quest yang sedang/sudah dijalankan.
type Quest struct {
    ID       string
    Name     string
    Progress int
    Complete bool
}

// GameMemento menyimpan snapshot state game.
type GameMemento struct {
    state       PlayerState
    savedAt     time.Time
    checkpointName string
}

func (m *GameMemento) SavedAt() time.Time       { return m.savedAt }
func (m *GameMemento) CheckpointName() string   { return m.checkpointName }

// Player adalah Originator dalam game context.
type Player struct {
    state PlayerState
}

func NewPlayer() *Player {
    return &Player{
        state: PlayerState{
            Level:     1,
            Health:    100,
            MaxHealth: 100,
            Position:  Position{X: 0, Y: 0, Z: 0, Map: "starting_village"},
            Inventory: make([]Item, 0),
            Gold:      50,
            Skills:    map[string]int{"sword": 1, "shield": 1},
            QuestLog:  make([]Quest, 0),
        },
    }
}

// Save membuat snapshot dari state player saat ini.
func (p *Player) Save(checkpointName string) *GameMemento {
    // Deep copy semua reference types
    inventoryCopy := make([]Item, len(p.state.Inventory))
    copy(inventoryCopy, p.state.Inventory)

    skillsCopy := make(map[string]int, len(p.state.Skills))
    for k, v := range p.state.Skills {
        skillsCopy[k] = v
    }

    questsCopy := make([]Quest, len(p.state.QuestLog))
    copy(questsCopy, p.state.QuestLog)

    return &GameMemento{
        state: PlayerState{
            Level:     p.state.Level,
            Health:    p.state.Health,
            MaxHealth: p.state.MaxHealth,
            Position:  p.state.Position, // struct — value copy aman
            Inventory: inventoryCopy,
            Gold:      p.state.Gold,
            Skills:    skillsCopy,
            QuestLog:  questsCopy,
        },
        savedAt:        time.Now(),
        checkpointName: checkpointName,
    }
}

// Restore memulihkan state player dari Memento.
func (p *Player) Restore(m *GameMemento) {
    inventoryCopy := make([]Item, len(m.state.Inventory))
    copy(inventoryCopy, m.state.Inventory)

    skillsCopy := make(map[string]int, len(m.state.Skills))
    for k, v := range m.state.Skills {
        skillsCopy[k] = v
    }

    questsCopy := make([]Quest, len(m.state.QuestLog))
    copy(questsCopy, m.state.QuestLog)

    p.state = PlayerState{
        Level:     m.state.Level,
        Health:    m.state.Health,
        MaxHealth: m.state.MaxHealth,
        Position:  m.state.Position,
        Inventory: inventoryCopy,
        Gold:      m.state.Gold,
        Skills:    skillsCopy,
        QuestLog:  questsCopy,
    }
    fmt.Printf("[Game] Restored to checkpoint '%s' at %s\n",
        m.checkpointName, m.savedAt.Format("15:04:05"))
}

// Aksi-aksi player
func (p *Player) TakeDamage(amount int) {
    p.state.Health -= amount
    if p.state.Health < 0 {
        p.state.Health = 0
    }
}

func (p *Player) AddItem(item Item) {
    p.state.Inventory = append(p.state.Inventory, item)
}

func (p *Player) SpendGold(amount int) error {
    if p.state.Gold < amount {
        return fmt.Errorf("not enough gold")
    }
    p.state.Gold -= amount
    return nil
}

func (p *Player) MoveTo(pos Position) {
    p.state.Position = pos
}

func (p *Player) LevelUp() {
    p.state.Level++
    p.state.MaxHealth += 20
    p.state.Health = p.state.MaxHealth
}

func (p *Player) StatusReport() string {
    return fmt.Sprintf("Level %d | HP %d/%d | Gold %d | Map: %s (%.0f,%.0f)",
        p.state.Level, p.state.Health, p.state.MaxHealth,
        p.state.Gold, p.state.Position.Map,
        p.state.Position.X, p.state.Position.Y,
    )
}

func (p *Player) IsDead() bool { return p.state.Health <= 0 }


// SaveGameManager adalah Caretaker untuk game — mengelola save slots.
type SaveGameManager struct {
    slots   map[string]*GameMemento // slot name → memento
    maxSlots int
}

func NewSaveGameManager(maxSlots int) *SaveGameManager {
    return &SaveGameManager{
        slots:    make(map[string]*GameMemento),
        maxSlots: maxSlots,
    }
}

// SaveToSlot menyimpan snapshot ke slot tertentu.
func (m *SaveGameManager) SaveToSlot(player *Player, slotName string) {
    memento := player.Save(slotName)
    m.slots[slotName] = memento
    fmt.Printf("[SaveManager] Game disimpan ke slot '%s'\n", slotName)
}

// LoadFromSlot memulihkan state dari slot tertentu.
func (m *SaveGameManager) LoadFromSlot(player *Player, slotName string) error {
    memento, ok := m.slots[slotName]
    if !ok {
        return fmt.Errorf("save slot '%s' tidak ditemukan", slotName)
    }
    player.Restore(memento)
    return nil
}

// ListSlots mengembalikan daftar save slot yang tersedia.
func (m *SaveGameManager) ListSlots() []string {
    slots := make([]string, 0, len(m.slots))
    for name := range m.slots {
        slots = append(slots, name)
    }
    return slots
}


// Demonstrasi
func playGame() {
    player := NewPlayer()
    saveManager := NewSaveGameManager(5)

    fmt.Printf("Start: %s\n", player.StatusReport())

    // Checkpoint 1: Di desa awal
    saveManager.SaveToSlot(player, "checkpoint_village")

    // Player berkembang
    player.LevelUp()
    player.AddItem(Item{ID: "iron_sword", Name: "Iron Sword", Quantity: 1, Equipped: true})
    player.MoveTo(Position{X: 150, Y: 0, Z: 0, Map: "dark_forest"})
    fmt.Printf("After exploring: %s\n", player.StatusReport())

    // Checkpoint 2: Sebelum boss fight
    saveManager.SaveToSlot(player, "checkpoint_before_boss")

    // Player kalah di boss fight
    player.TakeDamage(200)
    fmt.Printf("After boss fight: %s\n", player.StatusReport())

    if player.IsDead() {
        fmt.Println("\n--- Player mati! Load checkpoint... ---")
        _ = saveManager.LoadFromSlot(player, "checkpoint_before_boss")
        fmt.Printf("Setelah load: %s\n", player.StatusReport())
    }
}

Serialisasi Memento ke Persistent Storage #

Untuk game save yang bertahan setelah aplikasi ditutup, Memento perlu di-serialisasi ke disk atau database.

package game

import (
    "encoding/json"
    "os"
    "path/filepath"
    "fmt"
)

// PersistentSaveManager menyimpan Memento ke file system.
type PersistentSaveManager struct {
    saveDir string
}

func NewPersistentSaveManager(saveDir string) (*PersistentSaveManager, error) {
    if err := os.MkdirAll(saveDir, 0755); err != nil {
        return nil, fmt.Errorf("cannot create save directory: %w", err)
    }
    return &PersistentSaveManager{saveDir: saveDir}, nil
}

// serializedMemento adalah versi yang bisa di-JSON-kan dari GameMemento.
// Ini diperlukan karena field GameMemento unexported.
type serializedMemento struct {
    State          PlayerState `json:"state"`
    SavedAt        string      `json:"saved_at"`
    CheckpointName string      `json:"checkpoint_name"`
}

// SaveToDisk menyimpan Memento ke file.
func (m *PersistentSaveManager) SaveToDisk(player *Player, slotName string) error {
    memento := player.Save(slotName)

    // Serialisasi — kita perlu akses ke field internal
    // Dalam implementasi nyata, Originator menyediakan method ExportForSave()
    serialized := serializedMemento{
        State:          memento.state,
        SavedAt:        memento.savedAt.Format(time.RFC3339),
        CheckpointName: memento.checkpointName,
    }

    data, err := json.MarshalIndent(serialized, "", "  ")
    if err != nil {
        return fmt.Errorf("cannot serialize save: %w", err)
    }

    filename := filepath.Join(m.saveDir, slotName+".json")
    if err := os.WriteFile(filename, data, 0644); err != nil {
        return fmt.Errorf("cannot write save file: %w", err)
    }

    fmt.Printf("[PersistentSave] Saved to %s\n", filename)
    return nil
}

// LoadFromDisk memuat Memento dari file dan memulihkan ke player.
func (m *PersistentSaveManager) LoadFromDisk(player *Player, slotName string) error {
    filename := filepath.Join(m.saveDir, slotName+".json")
    data, err := os.ReadFile(filename)
    if err != nil {
        return fmt.Errorf("save file not found: %w", err)
    }

    var serialized serializedMemento
    if err := json.Unmarshal(data, &serialized); err != nil {
        return fmt.Errorf("cannot deserialize save: %w", err)
    }

    // Rekonstruksi Memento dari data yang di-deserialisasi
    savedAt, _ := time.Parse(time.RFC3339, serialized.SavedAt)
    memento := &GameMemento{
        state:          serialized.State,
        savedAt:        savedAt,
        checkpointName: serialized.CheckpointName,
    }

    player.Restore(memento)
    fmt.Printf("[PersistentSave] Loaded from %s\n", filename)
    return nil
}

Manajemen Memori: Batas Jumlah Snapshot #

Menyimpan terlalu banyak snapshot bisa menghabiskan memori, terutama untuk state yang besar. Ada beberapa strategi untuk mengatasinya.

// Strategi 1: Fixed-size ring buffer — hapus yang terlama saat penuh
type BoundedHistory struct {
    snapshots []*WizardMemento
    maxSize   int
    writeIdx  int
    count     int
}

func NewBoundedHistory(maxSize int) *BoundedHistory {
    return &BoundedHistory{
        snapshots: make([]*WizardMemento, maxSize),
        maxSize:   maxSize,
    }
}

func (h *BoundedHistory) Save(wizard *FormWizard) {
    h.snapshots[h.writeIdx] = wizard.CreateMemento()
    h.writeIdx = (h.writeIdx + 1) % h.maxSize
    if h.count < h.maxSize {
        h.count++
    }
}


// Strategi 2: Snapshot dengan kompresi delta
// Simpan state penuh hanya setiap N langkah; di antaranya simpan delta saja
type DeltaHistory struct {
    baseSnapshot   *WizardMemento
    deltas         []WizardDelta
    fullSnapshotN  int // simpan full snapshot setiap N perubahan
}

type WizardDelta struct {
    changedFields map[string]interface{}
    timestamp     time.Time
}


// Strategi 3: Expired snapshot — hapus snapshot yang sudah terlalu lama
type TimedHistory struct {
    snapshots []*timedMemento
    maxAge    time.Duration
}

type timedMemento struct {
    memento   *WizardMemento
    expiresAt time.Time
}

func (h *TimedHistory) PurgeExpired() {
    now := time.Now()
    var active []*timedMemento
    for _, tm := range h.snapshots {
        if now.Before(tm.expiresAt) {
            active = append(active, tm)
        }
    }
    h.snapshots = active
}

State Besar = Snapshot Mahal

Jika objek yang di-snapshot memiliki field berupa slice besar, map dengan banyak entry, atau nested struct yang dalam, setiap snapshot bisa memakan memori yang signifikan. Untuk kasus seperti ini, pertimbangkan: (1) hanya snapshot field yang berubah (delta snapshot), (2) batasi jumlah snapshot maksimum dengan ring buffer, (3) kompresi snapshot sebelum disimpan, atau (4) serialisasi langsung ke disk untuk state yang sangat besar.


Testing Memento Pattern #

func TestFormWizard_CreateAndRestoreMemento(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(5)

    // Set state awal
    _ = wizard.SetPersonalData(PersonalData{FullName: "Alice", Email: "[email protected]"})
    history.Save(wizard)
    _ = wizard.NextStep()

    // Ubah state
    _ = wizard.SetCompanyData(CompanyData{CompanyName: "Test Corp"})
    _ = wizard.NextStep()

    // Verifikasi state berubah
    if wizard.CurrentStep() != 3 {
        t.Errorf("expected step 3, got %d", wizard.CurrentStep())
    }

    // Undo ke step 1
    _ = history.Undo(wizard)
    if wizard.CurrentStep() != 1 {
        t.Errorf("after undo: expected step 1, got %d", wizard.CurrentStep())
    }
    if wizard.personal.FullName != "Alice" {
        t.Errorf("personal data should be restored, got %q", wizard.personal.FullName)
    }
}

func TestWizardHistory_BoundedByMaxSize(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(3) // maksimum 3 snapshot

    for i := 0; i < 5; i++ {
        _ = wizard.SetPersonalData(PersonalData{
            FullName: fmt.Sprintf("User %d", i),
            Email:    fmt.Sprintf("user%[email protected]", i),
        })
        history.Save(wizard)
    }

    if history.Count() > 3 {
        t.Errorf("expected max 3 snapshots, got %d", history.Count())
    }
}

func TestWizardHistory_GoToStep(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(10)

    for step := 1; step <= 3; step++ {
        history.Save(wizard)
        _ = wizard.NextStep()
    }

    err := history.GoToStep(wizard, 2)
    if err != nil {
        t.Fatalf("GoToStep failed: %v", err)
    }
    if wizard.CurrentStep() != 2 {
        t.Errorf("expected step 2 after GoToStep, got %d", wizard.CurrentStep())
    }
}

func TestGameMemento_DeepCopyInventory(t *testing.T) {
    player := NewPlayer()
    player.AddItem(Item{ID: "sword", Name: "Iron Sword", Quantity: 1})

    // Buat snapshot
    memento := player.Save("test")

    // Modifikasi inventory setelah snapshot
    player.state.Inventory[0].Quantity = 99

    // Restore — inventory harus kembali ke quantity 1, bukan 99
    player.Restore(memento)

    if player.state.Inventory[0].Quantity != 1 {
        t.Errorf("expected quantity 1 after restore, got %d (shallow copy bug!)",
            player.state.Inventory[0].Quantity)
    }
}

func TestGameMemento_UndoAfterDeath(t *testing.T) {
    player := NewPlayer()
    saveManager := NewSaveGameManager(3)

    initialHealth := player.state.Health
    saveManager.SaveToSlot(player, "before_boss")

    player.TakeDamage(200)
    if !player.IsDead() {
        t.Error("player should be dead after 200 damage")
    }

    err := saveManager.LoadFromSlot(player, "before_boss")
    if err != nil {
        t.Fatalf("LoadFromSlot failed: %v", err)
    }
    if player.state.Health != initialHealth {
        t.Errorf("health should be restored to %d, got %d", initialHealth, player.state.Health)
    }
    if player.IsDead() {
        t.Error("player should not be dead after restore")
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Memento jika:
  ✓ Perlu menyimpan snapshot state kompleks untuk undo/redo atau rollback
  ✓ State objek terlalu kompleks untuk dibalik secara logis (Command Pattern tidak cukup)
  ✓ Enkapsulasi harus tetap terjaga — Caretaker tidak boleh tahu isi state
  ✓ Perlu multiple save points yang bisa di-navigate (form wizard, game checkpoint)
  ✓ State perlu disimpan ke persistent storage untuk restore nanti

HINDARI Memento jika:
  ✗ State objek sangat besar — setiap snapshot memakan memori signifikan
  ✗ State berubah sangat sering — terlalu banyak snapshot yang perlu dibuat
  ✗ Aksi yang perlu di-undo bersifat diskret dan mudah dibalik — gunakan Command
  ✗ Hanya perlu undo satu langkah — cukup simpan satu copy state sebelumnya

Checklist Review Memento #

DESAIN:
  □ Field Memento unexported — hanya Originator yang bisa membaca/menulis state
  □ Caretaker hanya mengakses metadata Memento (CreatedAt, Name), bukan state
  □ Memento immutable setelah dibuat — tidak ada setter
  □ Originator melakukan deep copy saat CreateMemento dan RestoreFromMemento

DEEP COPY:
  □ Semua slice di-copy (bukan shared reference)
  □ Semua map di-copy (bukan shared reference)
  □ Semua nested pointer struct di-copy secara eksplisit
  □ Tidak ada shared mutable state antara Originator dan Memento

MEMORI:
  □ Ada batas maksimum jumlah snapshot (ring buffer atau eviction policy)
  □ Snapshot yang tidak lagi dibutuhkan dihapus dari Caretaker
  □ State besar dipertimbangkan untuk delta snapshot atau kompresi

TESTING:
  □ State setelah Restore sama persis dengan state saat CreateMemento
  □ Modifikasi state setelah CreateMemento tidak mempengaruhi Memento (deep copy)
  □ Modifikasi Memento tidak mempengaruhi state Originator (deep copy di Restore)
  □ Batas maksimum snapshot diverifikasi

Ringkasan #

  • Memento menyimpan snapshot state tanpa melanggar enkapsulasi — Caretaker menyimpan Memento tapi tidak bisa membaca isinya; hanya Originator yang bisa membuat dan memulihkan Memento.
  • Tiga komponen: Originator (membuat dan memulihkan Memento), Memento (menyimpan state dengan field unexported), dan Caretaker (menyimpan dan mengembalikan Memento tanpa tahu isinya).
  • Deep copy adalah kewajiban — setiap slice, map, dan pointer di state harus disalin secara mendalam saat CreateMemento dan RestoreFromMemento; shallow copy menyebabkan shared mutable state yang menjadi sumber bug.
  • Berbeda dari Command: Command menyimpan aksi dan cara membaliknya; Memento menyimpan snapshot state lengkap — pilih berdasarkan kompleksitas state yang perlu di-undo.
  • Batasi jumlah snapshot — gunakan ring buffer atau eviction policy untuk mencegah memory leak dari akumulasi snapshot yang tidak terbatas.
  • Serialisasi ke disk memungkinkan Memento bertahan setelah aplikasi ditutup — berguna untuk game save, form draft, dan session recovery.
  • Form wizard dan game checkpoint adalah dua use case paling natural — keduanya membutuhkan kemampuan kembali ke titik tertentu tanpa mengulang dari awal.
  • Untuk state yang sangat besar, pertimbangkan delta snapshot (simpan hanya yang berubah) atau kompresi sebelum menyimpan.

← Sebelumnya: Mediator   Berikutnya: Iterator →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact