Implementasi Memento Pattern di Proyek Nyata: Membuat Sistem Save/Load
Pendahuluan
Pernahkah kamu bermain game dan menekan tombol save sebelum pertarungan bos, lalu load kembali saat kalah? Atau menggunakan Ctrl+Z di editor teks untuk membatalkan kesalahan? Di balik fitur-fitur itu, ada sebuah pola desain yang bekerja diam-diam: Memento Pattern.
Memento Pattern adalah salah satu behavioral design pattern yang memungkinkan kita menyimpan dan memulihkan keadaan (state) sebuah objek tanpa melanggar enkapsulasi. Artikel ini akan membawa kamu dari konsep dasar hingga implementasi nyata — membuat sistem Save/Load lengkap menggunakan Python.
Jika kamu sudah familiar dengan konsep OOP di Python, kamu bisa langsung ikuti implementasinya. Untuk memahami fondasi OOP lebih dalam, baca dulu OOP di Python: Class, Object, dan Inheritance.
Memahami Konsep Dasar Memento Pattern
Memento Pattern melibatkan tiga komponen utama:
| Komponen | Peran |
|---|---|
| Originator | Objek yang state-nya ingin disimpan |
| Memento | ”Snapshot” state dari Originator |
| Caretaker | Pengelola koleksi Memento (tidak membaca isinya) |
Analogi sederhananya: bayangkan kamu sedang mengisi formulir panjang di aplikasi seperti Tokopedia. Sistem secara otomatis menyimpan progresmu setiap beberapa menit. Kalau browser kamu tiba-tiba tutup, progresnya masih bisa dipulihkan. Di sini, formulir adalah Originator, setiap snapshot otomatis adalah Memento, dan sistem autosave adalah Caretaker.
Prinsip kunci Memento Pattern:
- Caretaker tidak boleh membaca isi Memento — hanya menyimpan dan mengembalikannya
- Originator yang tahu cara membuat dan memulihkan dari Memento
- State tersimpan terisolasi dari logika utama program
Merancang Komponen Sistem Save/Load
Sebelum menulis kode, kita rancang dulu sistemnya. Kita akan membuat sistem Save/Load untuk karakter RPG sederhana dengan atribut:
- Nama karakter
- Level
- HP (hit points)
- Inventory (daftar item)
- Posisi (x, y)
Diagram alur:
PlayerCharacter (Originator)
│── save() → membuat CharacterMemento
└── load() ← menerima CharacterMemento
CharacterMemento
└── Menyimpan snapshot state (immutable)
SaveManager (Caretaker)
├── save_game(slot, memento)
├── load_game(slot) → memento
└── list_saves()
Langkah-Langkah Implementasi dalam Kode
Langkah 1: Buat Kelas Memento
Memento harus immutable — isinya tidak boleh berubah setelah dibuat.
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Tuple
@dataclass(frozen=True) # frozen=True membuat objek immutable
class CharacterMemento:
"""Snapshot state karakter — hanya Originator yang boleh membacanya."""
name: str
level: int
hp: int
max_hp: int
inventory: tuple # tuple agar immutable (bukan list)
position: Tuple[int, int]
timestamp: str = field(
default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
def get_info(self) -> str:
return f"[{self.timestamp}] {self.name} — Level {self.level}, HP {self.hp}/{self.max_hp}"
Menggunakan frozen=True dari dataclass memastikan tidak ada kode luar yang bisa mengubah isi snapshot secara tidak sengaja.
Langkah 2: Buat Kelas Originator (PlayerCharacter)
class PlayerCharacter:
"""Originator: objek yang state-nya bisa disimpan dan dipulihkan."""
def __init__(self, name: str):
self.name = name
self.level = 1
self.hp = 100
self.max_hp = 100
self._inventory: List[str] = []
self.position: Tuple[int, int] = (0, 0)
# --- Aksi karakter ---
def move(self, x: int, y: int):
self.position = (x, y)
print(f"{self.name} bergerak ke posisi {self.position}")
def pick_up_item(self, item: str):
self._inventory.append(item)
print(f"{self.name} mengambil: {item}")
def take_damage(self, damage: int):
self.hp = max(0, self.hp - damage)
print(f"{self.name} terkena {damage} damage! HP: {self.hp}/{self.max_hp}")
def level_up(self):
self.level += 1
self.max_hp += 20
self.hp = self.max_hp
print(f"Level Up! {self.name} sekarang Level {self.level}. HP penuh: {self.max_hp}")
# --- Memento Pattern ---
def save(self) -> CharacterMemento:
"""Membuat snapshot state saat ini."""
return CharacterMemento(
name=self.name,
level=self.level,
hp=self.hp,
max_hp=self.max_hp,
inventory=tuple(self._inventory), # konversi ke tuple agar immutable
position=self.position,
)
def load(self, memento: CharacterMemento):
"""Memulihkan state dari snapshot."""
self.name = memento.name
self.level = memento.level
self.hp = memento.hp
self.max_hp = memento.max_hp
self._inventory = list(memento.inventory) # konversi kembali ke list
self.position = memento.position
print(f"Game dimuat: {memento.get_info()}")
def status(self):
print(f"\n{'='*40}")
print(f"Nama : {self.name}")
print(f"Level : {self.level}")
print(f"HP : {self.hp}/{self.max_hp}")
print(f"Posisi : {self.position}")
print(f"Item : {self._inventory}")
print(f"{'='*40}\n")
Langkah 3: Buat Kelas Caretaker (SaveManager)
class SaveManager:
"""Caretaker: mengelola koleksi Memento tanpa membaca isinya."""
def __init__(self):
self._slots: dict[str, CharacterMemento] = {}
def save_game(self, slot: str, memento: CharacterMemento):
self._slots[slot] = memento
print(f"Game tersimpan di slot '{slot}': {memento.get_info()}")
def load_game(self, slot: str) -> CharacterMemento:
if slot not in self._slots:
raise KeyError(f"Slot '{slot}' tidak ditemukan!")
return self._slots[slot]
def delete_save(self, slot: str):
if slot in self._slots:
del self._slots[slot]
print(f"Slot '{slot}' dihapus.")
def list_saves(self):
if not self._slots:
print("Belum ada save game.")
return
print("\n--- Daftar Save Game ---")
for slot, memento in self._slots.items():
print(f" [{slot}] {memento.get_info()}")
print()
Langkah 4: Uji Sistem Lengkap
if __name__ == "__main__":
# Inisialisasi
hero = PlayerCharacter("Arjuna")
save_mgr = SaveManager()
# === Sesi bermain pertama ===
hero.move(10, 5)
hero.pick_up_item("Pedang Besi")
hero.pick_up_item("Ramuan Merah")
hero.level_up()
# Simpan sebelum masuk dungeon
save_mgr.save_game("sebelum_dungeon", hero.save())
hero.status()
# === Masuk dungeon, terkena serangan bos ===
hero.move(50, 80)
hero.pick_up_item("Kunci Bos")
hero.take_damage(85) # hampir mati!
hero.take_damage(30) # mati!
hero.status()
# === Load save sebelum dungeon ===
print("\n>>> Memuat ulang save...")
hero.load(save_mgr.load_game("sebelum_dungeon"))
hero.status()
# === Multiple save slots ===
hero.move(20, 20)
hero.level_up()
save_mgr.save_game("slot_2", hero.save())
save_mgr.list_saves()
Output yang dihasilkan:
Arjuna bergerak ke posisi (10, 5)
Arjuna mengambil: Pedang Besi
Arjuna mengambil: Ramuan Merah
Level Up! Arjuna sekarang Level 2. HP penuh: 120
Game tersimpan di slot 'sebelum_dungeon': [2026-04-10 10:00:00] Arjuna — Level 2, HP 120/120
========================================
Nama : Arjuna
Level : 2
HP : 120/120
Posisi : (10, 5)
Item : ['Pedang Besi', 'Ramuan Merah']
========================================
Arjuna bergerak ke posisi (50, 80)
Arjuna mengambil: Kunci Bos
Arjuna terkena 85 damage! HP: 35/120
Arjuna terkena 30 damage! HP: 5/120
========================================
Nama : Arjuna
Level : 2
HP : 5/120
Posisi : (50, 80)
Item : ['Pedang Besi', 'Ramuan Merah', 'Kunci Bos']
========================================
>>> Memuat ulang save...
Game dimuat: [2026-04-10 10:00:00] Arjuna — Level 2, HP 120/120
========================================
Nama : Arjuna
Level : 2
HP : 120/120
Posisi : (10, 5)
Item : ['Pedang Besi', 'Ramuan Merah']
========================================
Game tersimpan di slot 'slot_2': [2026-04-10 10:01:00] Arjuna — Level 3, HP 140/140
--- Daftar Save Game ---
[sebelum_dungeon] [2026-04-10 10:00:00] Arjuna — Level 2, HP 120/120
[slot_2] [2026-04-10 10:01:00] Arjuna — Level 3, HP 140/140
Contoh Kasus Nyata
Undo/Redo di Editor Teks
Memento Pattern sangat populer untuk fitur undo/redo. Berikut implementasi ringkas yang langsung bisa dijalankan:
from typing import List
class TextEditor:
def __init__(self):
self.content = ""
self._history: List[str] = [] # stack undo
self._redo_stack: List[str] = []
def type(self, text: str):
self._history.append(self.content) # simpan state sebelumnya
self._redo_stack.clear()
self.content += text
def undo(self):
if self._history:
self._redo_stack.append(self.content)
self.content = self._history.pop()
print(f"Undo → '{self.content}'")
else:
print("Tidak ada yang bisa di-undo.")
def redo(self):
if self._redo_stack:
self._history.append(self.content)
self.content = self._redo_stack.pop()
print(f"Redo → '{self.content}'")
else:
print("Tidak ada yang bisa di-redo.")
if __name__ == "__main__":
editor = TextEditor()
editor.type("Halo ")
editor.type("dunia")
editor.type("!")
print(f"Konten: '{editor.content}'") # Konten: 'Halo dunia!'
editor.undo() # Undo → 'Halo dunia'
editor.undo() # Undo → 'Halo '
editor.redo() # Redo → 'Halo dunia'
print(f"Konten akhir: '{editor.content}'") # Konten akhir: 'Halo dunia'
Jika kamu tertarik membangun aplikasi seperti Notion atau editor kolaboratif real-time, Memento Pattern adalah fondasi yang bagus untuk fitur history-nya. Untuk memahami pola lain yang sering dikombinasikan, baca juga Implementasi Singleton Pattern: Menjaga Satu Instance Tunggal — Singleton sering dipakai untuk mengelola SaveManager secara global.
Serialisasi Save ke File (Persistent Storage)
Untuk game sungguhan, save harus tersimpan ke disk. Berikut contoh lengkapnya:
import json
import os
from dataclasses import dataclass, field
from datetime import datetime
from typing import Tuple
@dataclass(frozen=True)
class CharacterMemento:
name: str
level: int
hp: int
max_hp: int
inventory: tuple
position: Tuple[int, int]
timestamp: str = field(
default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
def get_info(self) -> str:
return f"[{self.timestamp}] {self.name} — Level {self.level}, HP {self.hp}/{self.max_hp}"
class PersistentSaveManager:
SAVE_DIR = "saves/"
def __init__(self):
os.makedirs(self.SAVE_DIR, exist_ok=True)
def save_to_file(self, slot: str, memento: CharacterMemento):
data = {
"name": memento.name,
"level": memento.level,
"hp": memento.hp,
"max_hp": memento.max_hp,
"inventory": list(memento.inventory),
"position": list(memento.position),
"timestamp": memento.timestamp,
}
path = f"{self.SAVE_DIR}{slot}.json"
with open(path, "w") as f:
json.dump(data, f, indent=2)
print(f"Tersimpan ke file: {path}")
def load_from_file(self, slot: str) -> CharacterMemento:
path = f"{self.SAVE_DIR}{slot}.json"
if not os.path.exists(path):
raise FileNotFoundError(f"File save '{path}' tidak ditemukan!")
with open(path, "r") as f:
data = json.load(f)
return CharacterMemento(
name=data["name"],
level=data["level"],
hp=data["hp"],
max_hp=data["max_hp"],
inventory=tuple(data["inventory"]),
position=tuple(data["position"]),
timestamp=data["timestamp"],
)
if __name__ == "__main__":
mgr = PersistentSaveManager()
# Buat memento untuk demo
memento = CharacterMemento(
name="Arjuna",
level=3,
hp=100,
max_hp=140,
inventory=("Pedang Besi", "Ramuan Merah"),
position=(20, 20),
)
mgr.save_to_file("slot_utama", memento)
loaded = mgr.load_from_file("slot_utama")
print(loaded.get_info())
# Output: [2026-04-10 10:00:00] Arjuna — Level 3, HP 100/140
Pola ini juga relevan saat membangun backend untuk game mobile — prinsipnya sama dengan menyimpan state sesi user di API. Kalau kamu ingin eksplorasi lebih lanjut tentang membangun API, artikel Membangun RESTful API Sederhana dengan Go: Tutorial Step-by-Step bisa jadi referensi yang bagus.
Pertanyaan yang Sering Diajukan
Apa perbedaan Memento Pattern dengan menyimpan state secara langsung di variabel biasa?
Perbedaan utamanya ada pada enkapsulasi. Jika kamu menyimpan state langsung ke variabel publik, kode luar bisa membaca dan memodifikasi isi snapshot tersebut, yang berpotensi menyebabkan bug. Memento Pattern memastikan hanya Originator yang tahu struktur internal state-nya — Caretaker hanya memegang referensi opaque tanpa bisa mengintip isinya.
Bagaimana cara menangani state yang sangat besar agar tidak memenuhi memori?
Ada beberapa strategi: pertama, gunakan incremental snapshot — hanya simpan delta (perubahan) bukan state penuh. Kedua, batasi jumlah slot undo (misalnya maksimal 20 langkah terakhir). Ketiga, kompres data sebelum disimpan menggunakan library seperti zlib di Python, atau serialisasi ke format biner yang ringkas.
Apakah Memento Pattern cocok untuk aplikasi web modern?
Ya, sangat cocok! Dalam aplikasi React misalnya, konsep Memento mirip dengan Redux time-travel debugging — setiap dispatch action menyimpan snapshot state. Pola ini juga digunakan dalam fitur undo di aplikasi SaaS berbasis browser seperti Figma atau Google Docs.
Mengapa Caretaker tidak boleh membaca isi Memento?
Ini adalah prinsip information hiding. Jika Caretaker bisa membaca dan memodifikasi Memento, ia menjadi bergantung pada struktur internal Originator. Ketika struktur Originator berubah (misalnya menambah atribut baru), Caretaker juga harus ikut diubah — ini melanggar prinsip OCP (Open/Closed Principle) dan menciptakan coupling yang ketat.
Apa yang membedakan Memento Pattern dari Command Pattern untuk fitur undo?
Command Pattern menyimpan aksi (perintah) dan membalikkannya satu per satu (dengan metode undo() di setiap command). Memento Pattern menyimpan state penuh dan memulihkan semuanya sekaligus. Memento lebih sederhana untuk state kompleks, sedangkan Command lebih efisien memori jika operasi undo-nya bisa di-reverse secara logis.
Kesimpulan
Memento Pattern adalah solusi elegan untuk masalah yang sangat umum: menyimpan dan memulihkan keadaan objek. Dengan memisahkan tanggung jawab antara Originator (yang tahu state-nya), Memento (snapshot immutable), dan Caretaker (pengelola koleksi), kode kamu menjadi lebih terorganisir, mudah diuji, dan tidak melanggar enkapsulasi.
Dari sistem Save/Load game RPG hingga fitur undo di editor teks, pola ini sangat fleksibel dan bisa disesuaikan dengan kebutuhan proyekmu. Kuncinya adalah menjaga Memento tetap sederhana dan immutable, serta memastikan hanya Originator yang mengakses isinya.
Selamat mencoba implementasinya di proyekmu sendiri! Jika ada pertanyaan atau kamu ingin eksplorasi pola desain lain, jangan ragu untuk menjelajahi artikel-artikel lainnya di KamusNgoding — masih banyak konsep seru yang menunggumu!