Langsung ke konten
KamusNgoding
Terapan Behavioral 5 menit baca

Implementasi Memento Pattern di Proyek Nyata: Membuat Sistem Save/Load

#design pattern #memento #aplikasi nyata #behavioral #save load

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:

KomponenPeran
OriginatorObjek yang state-nya ingin disimpan
Memento”Snapshot” state dari Originator
CaretakerPengelola 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!

Artikel Terkait