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.