Memento Pattern (Undo/Redo) — Design Patterns

Memento menyimpan snapshot state object, lalu mengembalikannya nanti — basis dari undo/redo. Tiga peran: Originator — object yang state-nya disimpan (editor…

Memento Pattern menyimpan snapshot (potret) keadaan sebuah object sebelum object itu berubah, supaya keadaan itu bisa dikembalikan nanti. Itu dasar tombol undo dan redo. Bayangin kamu mau membongkar rem sepeda. Sebelum mulai, kamu memotretnya dulu. Kalau rakitannya kacau, foto itu yang menunjukkan cara mengembalikannya. Fotonya gak kamu coret-coret, kamu simpan di album, dan kalau memori HP penuh, foto paling lama yang dihapus.

🧪 Tebakan cepat: draft kiriman Kurir Kancil punya alamat: { jalan: "Jl. Kenari 5", kota: "Bandung" }. Snapshot-nya dibuat dengan { ...this.draft }. Admin mengubah kota jadi Bogor (d.alamat.kota = "Bogor"), lalu menekan undo. Kota apa yang tampil? Tebak dulu, jawabannya ada di bagian "Salinan dangkal: snapshot yang ikut berubah".

Masalahnya: formulir tanpa jalan kembali

Formulir draft kiriman berisi penerima, layanan, berat, dan alamat. Admin mengubah layanan ke kilat, lalu kota ke Bogor, lalu sadar keduanya salah. Kalau keadaan lama gak disimpan di mana pun, gak ada yang bisa dikembalikan.

Di lesson Command, tiap aksi menyimpan cara membatalkan dirinya. Untuk formulir dengan banyak isian, itu berarti menulis kebalikan untuk tiap isian. Memento memakai cara yang lebih sederhana: sebelum tiap perubahan, simpan seluruh draft.

Tiga peran: originator, memento, caretaker

// Originator: formulir yang membuat dan memulihkan snapshot
const formulir = {
  draft: {
    penerima: "Bu Sari",
    service: "reguler",
    kg: 2,
    alamat: { jalan: "Jl. Kenari 5", kota: "Bandung" },
  },
  save() {
    return structuredClone(this.draft) // memento: salinan penuh, alamat ikut disalin
  },
  restore(memento) {
    this.draft = structuredClone(memento)
  },
}

// Caretaker: menyimpan snapshot, gak pernah mengubah isinya
const MAX_HISTORY = 5
const riwayat = { undo: [], redo: [] }

function ubah(nama, change) {
  riwayat.undo.push(formulir.save()) // simpan SEBELUM berubah
  if (riwayat.undo.length > MAX_HISTORY) riwayat.undo.shift() // buang yang paling tua
  riwayat.redo.length = 0 // edit baru membatalkan redo
  change(formulir.draft)
}

function undo() {
  if (riwayat.undo.length === 0) return false
  riwayat.redo.push(formulir.save())
  formulir.restore(riwayat.undo.pop())
  return true
}

function redo() {
  if (riwayat.redo.length === 0) return false
  riwayat.undo.push(formulir.save())
  formulir.restore(riwayat.redo.pop())
  return true
}

function ringkas(d) {
  return d.service + ", " + d.kg + " kg, " + d.alamat.kota
}

Undo dan redo, langkah demi langkah

ubah("layanan", (d) => { d.service = "kilat" })
ubah("kota", (d) => { d.alamat.kota = "Bogor" })
undo()
undo()
redo()
ubah("berat", (d) => { d.kg += 1 })
console.log(redo()) // false
langkah draft sesudahnya isi stack undo isi stack redo
awal reguler, 2 kg, Bandung 0 0
ubah("layanan", ...) kilat, 2 kg, Bandung 1 0
ubah("kota", ...) kilat, 2 kg, Bogor 2 0
undo() kilat, 2 kg, Bandung 1 1
undo() reguler, 2 kg, Bandung 0 2
redo() kilat, 2 kg, Bandung 1 1
ubah("berat", ...) kilat, 3 kg, Bandung 2 0
redo() false, draft gak berubah 2 0

Undo menyimpan draft sekarang ke stack redo, lalu memulihkan snapshot teratas dari stack undo. Redo melakukan kebalikannya. Saat ada edit baru sesudah undo (ubah("berat")), stack redo dikosongkan. Edit itu membuat cabang baru, jadi snapshot "kilat, 2 kg, Bogor" di redo gak cocok lagi: me-redo ke sana akan membuang berat yang baru diubah. Begitu juga perilaku undo di editor teks dan aplikasi desain.

Salinan dangkal: snapshot yang ikut berubah

Sekarang save() ditulis dengan spread:

save() {
  return { ...this.draft } // salinan dangkal
},
restore(memento) {
  this.draft = { ...memento }
},

Jalankan ubah("kota", ...) lalu undo(), dan kotanya tetap Bogor. Itu jawaban tebakannya. { ...draft } cuma menyalin isian teratas. service dan kg berisi string dan angka, jadi nilainya ikut tersalin. Tapi alamat berisi object, dan yang tersalin cuma referensinya: snapshot dan draft memegang object alamat yang sama. d.alamat.kota = "Bogor" mengubah object itu, dan snapshot-nya ikut berubah. Semua snapshot yang dibuat dengan spread memakai alamat yang sama, jadi undo berapa kali pun gak mengembalikan Bandung.

Tiga cara membetulkannya:

Memori: tiap snapshot ada harganya

Satu snapshot draft di atas cuma sekitar 100 byte, jadi 5 snapshot gak terasa. Lain cerita kalau draft-nya membawa foto paket sebagai data URL sekitar 300 KB. structuredClone menyalin fotonya juga di tiap snapshot: 5 snapshot berarti sekitar 1.500 KB, dan riwayat tanpa batas dengan 200 edit berarti sekitar 60 MB.

Latihan: editor dengan undo dan redo

Latihan nanti memakai pola yang sama untuk teks. createEditor(maxHistory) mengembalikan object dengan lima method. Keadaannya cuma satu string, text, dan string di JavaScript gak bisa diubah isinya, jadi string itu sendiri sudah snapshot yang aman. Gak perlu menyalin apa pun.

Dengan createEditor(3), starter mencetak 10 baris: string kosong, Hello, World, 3, true, Hello, (dengan spasi di akhir), 1, true, Hello, World, lalu sesudah undo dan type('!!'): Hello, !! dan 0. Yang juga dinilai: createEditor(2) yang diketik a, b, c cuma menyimpan 2 snapshot.

⚠️ Jebakan yang sering ketemu

TL;DR: Memento menyimpan snapshot keadaan sebelum tiap perubahan supaya bisa dikembalikan. Tiga perannya: originator (formulir, dengan save dan restore), memento (snapshot-nya, gak diubah siapa pun), dan caretaker (riwayat, yang menyimpan stack undo dan redo tanpa membaca isinya). Undo memindahkan keadaan sekarang ke redo lalu memulihkan snapshot teratas; edit baru mengosongkan redo. { ...draft } cuma menyalin isian teratas, jadi alamat dipakai bersama dan undo kota tetap Bogor; structuredClone menyalin penuh, JSON merusak Date. Tiap snapshot memakan memori: batasi dengan MAX_HISTORY, buang yang tua dengan shift(), dan untuk state besar simpan perubahannya saja (Command). Di latihan: snapshot text sebelum type, undo dan redo mengembalikan true atau false, dan historySize() mengembalikan { undo, redo }.

Yang akan kamu pelajari