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
}
- Originator adalah object yang keadaannya disimpan:
formulir. Cuma dia yang tahu cara membuat snapshot (save) dan memulihkannya (restore). - Memento adalah snapshot itu sendiri: object yang dikembalikan
save(). Gak ada yang boleh mengubahnya setelah dibuat. - Caretaker adalah penyimpan riwayat:
riwayatdengan dua tumpukan (stack). Dia menyimpan dan mengembalikan memento, tapi gak pernah membaca atau mengubah isinya. structuredClone(x)adalah function bawaan (browser modern dan Node 17 ke atas) yang membuat salinan penuh, termasuk object di dalam object sepertialamat.ubahmenyimpan snapshot sebelum draft berubah. Itu yang nanti dikembalikan undo.riwayat.redo.length = 0mengosongkan stack redo tiap ada edit baru. Alasannya di bagian berikut.
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:
structuredClone: salinan penuh, termasukDate,Map, danSet. Function gak bisa disalin (melemparDataCloneError), tapi draft formulir memang cuma data.JSON.parse(JSON.stringify(draft)): juga salinan penuh, tapi JSON gak punya tipeDate. Tanggal berubah jadi teks, jadi sesudah restore,draft.tanggalKirim.getDate()melemparTypeError.- Jangan ubah object di dalamnya: buat object baru,
formulir.draft = { ...d, alamat: { ...d.alamat, kota: "Bogor" } }. Snapshot dangkal jadi aman, dan bagian yang gak berubah dipakai bersama tanpa disalin. Ini cara Redux dan Immer bekerja.
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.
- Batasi riwayatnya.
MAX_HISTORYdenganshift()membuang snapshot paling tua saat stack penuh. - Simpan yang perlu di-undo saja. Foto yang gak diedit di formulir gak perlu ikut di snapshot.
- Simpan perubahannya, bukan seluruh keadaan. Untuk state besar dengan perubahan kecil, simpan
{ isian: "kota", lama: "Bandung", baru: "Bogor" }, dan undo menulis nilai lamanya kembali. Ini disebut incremental memento, dan bentuknya sama dengan Command dari lesson sebelumnya. - Jangan snapshot tiap ketikan. Simpan per aksi yang berarti, misalnya saat satu isian selesai diubah.
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.
type(str): pushtextke stack undo sebelum menambahkanstr. Kalau stack undo lebih darimaxHistory, buang yang paling tua denganshift(). Kosongkan stack redo. Lalutext += str.getText(): kembalikantext.undo(): kalau stack undo kosong,return false. Kalau gak, pushtextke stack redo, ambil snapshot teratas denganpop(), lalureturn true.redo(): kebalikannya: pushtextke stack undo, ambil dari stack redo,return true, ataufalsekalau kosong.historySize(): kembalikan{ undo: ..., redo: ... }, panjang kedua stack.
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
- Salinan dangkal untuk object bersarang.
{ ...draft }berbagialamatdengan draft, jadi undo gak mengembalikan Bandung. PakaistructuredClone, atau jangan ubah object di dalamnya. - Snapshot diambil sesudah perubahan. Undo pertama lalu memulihkan keadaan yang sama dengan sekarang, dan kelihatannya gak terjadi apa-apa.
- Lupa mengosongkan redo. Redo sesudah edit baru melompat ke cabang lama dan membuang edit yang baru.
- Riwayat tanpa batas, atau snapshot tiap ketikan. Dengan foto 300 KB, 200 snapshot sekitar 60 MB.
- Di latihan: undo yang gak menyimpan
textke stack redo.redo()lalu mengembalikanfalse, danhistorySize().redotetap0.
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 }.