Memento Pattern: বস ফাইটের আগে গেম সেভ করো
ভিডিও গেমের save point-এর গল্প দিয়ে Memento pattern বোঝো — TypeScript আর Python-এ undo-র উদাহরণ, diagram, table আর সহজ practice task সহ।
বস ফাইটের আগের সেই রাত 🐉
ধরো রবিবার বিকেল। রুবেল, চৌদ্দ বছর বয়সী, দুপুর থেকে তার প্রিয় adventure game খেলছে। তার ছোট বোন সুমাইয়া খাটের কিনারায় বসে দেখছে আর ছোট বোনের মতো "উপকারী পরামর্শ" দিয়ে যাচ্ছে।
রুবেলের character-এর দিনটা দারুণ গেছে। সে ৪,৫০০ সোনার মুদ্রা জমা করেছে। একটা লুকানো গুহায় বিরল উত্তরের তলোয়ার খুঁজে পেয়েছে। Health bar পূর্ণ আর সবুজ। আর এখন স্ক্রিনে দাঁড়িয়ে আছে একটা বিশাল পাথরের দরজা — ফাঁক দিয়ে আগুনের আলো দেখা যাচ্ছে। সেই দরজার পেছনে অপেক্ষা করছে চূড়ান্ত boss — বিশাল আগুনের dragon।
সুমাইয়া বলল, "ঢুকে পড়ো! আক্রমণ করো!"
কিন্তু রুবেল চালাক। সে আগেও boss-এর কাছে হেরেছে। সে প্রথমে কী করে? তার character-কে জ্বলজ্বলে save point-এ নিয়ে যায় আর Save Game চাপে।
কেন? কারণ সে জানে কী হতে পারে। Dragon হয়তো ত্রিশ সেকেন্ডে তাকে শেষ করে দেবে। তার মুদ্রা, তার তলোয়ার, তার পুরো রবিবার — সব মুছে যেতে পারে। কিন্তু একটা save file থাকলে কিছুই আর সত্যিকার অর্থে হারায় না। হেরে গেলে Load Game চাপলেই হয় — আর সে আবার দরজার সামনে, মুদ্রা আর তলোয়ার অক্ষত, দ্বিতীয় রাউন্ডের জন্য প্রস্তুত।
রুবেল দরজা খুলল। Dragon গর্জন করল। দুই মিনিট পর স্ক্রিনে বড় বড় লাল অক্ষরে লেখা YOU DIED। সুমাইয়া হাসল। রুবেল শান্তভাবে Load Game চাপল — আর সে আবার দরজার সামনে, ৪,৫০০ মুদ্রা, তলোয়ার ঝকঝকে। দ্বিতীয় রাউন্ড শুরু হলো।
এবার save file-এর একটা মজার বিষয় লক্ষ্য করো। রুবেল সেটার মালিক। সে এটা রাখতে পারে, পাঁচটা রাখতে পারে, পুরনোটা মুছতে পারে। কিন্তু সে কি এটা খুলে এডিট করতে পারে? Notepad-এ coins = 4500 বদলে coins = 99999 লিখতে পারে? না। Save file হলো bytes-এর একটা sealed packet। শুধু গেম ইঞ্জিন জানে ভেতরে কী আছে আর কীভাবে পড়তে হয়।
এটাই হলো Memento pattern, পুরোপুরি:
- গেম ইঞ্জিন নিজের state-এর snapshot বানায় আর সেখান থেকে restore করতে পারে। আমরা একে Originator বলি।
- Save file হলো snapshot — বাইরের কারো কাছে sealed, অপঠনযোগ্য। আমরা একে Memento বলি।
- রুবেল save file রাখে কিন্তু কখনো ভেতরে তাকায় না। আমরা তাকে Caretaker বলি।
তুমি এই pattern শত শত বার ব্যবহার করেছ, নামটা না জেনে। হোমওয়ার্কের document-এ প্রতিটা Ctrl+Z, গেমের প্রতিটা checkpoint, প্রতিটা "আগের version পুনরুদ্ধার করো" — সবই Memento। এই পোস্টে আমরা রুবেলের save system তিনভাবে বানাব, সব দিক থেকে আঁকব, আর শিখব কেন সিলমোহর করাটাই আসল কৌশল — শুধু সংরক্ষণ করা না।
Memento Pattern আসলে কী?
সহজ কথায় বলি।
Memento একটা behavioral design pattern যেটা তোমাকে একটা object-এর internal state একটা snapshot object-এ ধরে রাখতে দেয়, সেই snapshot নিরাপদে কোথাও রাখতে দেয়, আর পরে object-কে সেই exact state-এ ফিরিয়ে দেয় — কখনো object-এর private বিবরণ বাইরের জগতে ফাঁস না করে।
শেষের অংশটাই এই pattern-এর প্রাণ। যে কেউ public data কপি করতে পারে। Memento-র চালাক কৌশল হলো private state সংরক্ষণ আর পুনরুদ্ধার করা — সেটাকে private রেখেই। Object নিজেই নিজের ছবি তোলে, একটা খামে সিল করে, আর খামটা বাইরে দেয়। খামের রক্ষক সেগুলো রাখতে পারে, ফেরত দিতে পারে — কিন্তু কখনো খুলতে পারে না।
এই pattern-কে কিছু বইয়ে Snapshot বা Token-ও বলা হয়। তিনটা role একসাথে কাজ করে:
| Role | রুবেলের গল্পে | কাজ | Snapshot পড়তে পারে? |
|---|---|---|---|
| Originator | গেম ইঞ্জিন | save() দিয়ে memento বানায়, restore(m) দিয়ে পুনরুদ্ধার করে | ✅ হ্যাঁ — পূর্ণ access |
| Memento | Save file | State-এর একটা frozen কপি রাখে, চিরকাল immutable | এটা নিজেই snapshot |
| Caretaker | Player রুবেল | কখন save করবে আর কখন restore করবে ঠিক করে; stack রাখে | ❌ কখনো না |
কলেজের viva-র জন্য দুটো দরকারি কথা মনে রাখো: originator memento-তে wide interface পায় (ভেতরের সবকিছু পড়তে পারে), আর caretaker পায় narrow interface (শুধু memento ধরতে আর pass করতে পারে)। একই object, দুটো দৃষ্টিভঙ্গি।
মনে রাখার কৌশল: Originator = গেম ইঞ্জিন, Memento = save file, Caretaker = রুবেল। রুবেল save file রাখে কিন্তু পড়তে পারে না। ইঞ্জিন পড়তে পারে কিন্তু shelf পরিচালনা করে না। প্রতিটা role ঠিক একটাই কাজ করে — এই বিভাজনটাই এই pattern।
সমস্যাটা কোথায়: খোলা লকার
আমাদের কেন একটা বিশেষ pattern দরকার? আমরা কি নিজেরাই fields কপি করতে পারি না? চলো naive উপায়টা চেষ্টা করি আর দেখি রুবেলের নিজের গেমেই কীভাবে এটা ভেঙে পড়ে।
ধরো "সহজ" পদ্ধতি: কোনো History class গেমের ভেতরে ঢুকে field-গুলো এক এক করে কপি করছে:
// ❌ BAD: the history list reads the game's internals directly
class Game {
// To let History copy these, we are FORCED to make them public!
public level = 1;
public coins = 0;
public health = 100;
public inventory: string[] = [];
}
class History {
snapshots: { level: number; coins: number; health: number; inventory: string[] }[] = [];
backup(game: Game) {
this.snapshots.push({
level: game.level,
coins: game.coins,
health: game.health,
inventory: game.inventory, // ⚠️ a nasty bug is hiding here too!
});
}
}এটা "কাজ করে", কিন্তু ভেতরে চারটা গুরুতর সমস্যা লুকিয়ে আছে:
- Encapsulation ভেঙে গেছে। Fields কপি করতে
History-কে প্রতিটা field-এ public access দরকার। যে মুহূর্তে সেই fields public হয়, যেকোনো code যেকোনো জায়গা থেকেgame.coins = -5লিখে গেমটাকে corrupt করতে পারে। সুমাইয়া এক লাইনে একটা cheat script লিখতে পারবে। গেম আর তার নিজের নিয়ম রক্ষা করতে পারবে না। - এটা ভঙ্গুর। পরের মাসে একটা নতুন field যোগ হবে — ধরো
questsCompleted।History.backup()-ও আপডেট করতে হবে। ভুলে গেলে "load game" নীরবে একটা আধা-সঠিক state restore করবে। এই ধরনের bug মাসের পর মাস লুকিয়ে থাকে। - History class অনেক বেশি জানে। একটা history keeper-এর বোকা shelf হওয়া উচিত। Shelf-কে কেন বুঝতে হবে "health" আর "inventory" মানে কী? এই coupling-এর কারণে আমাদের
Historyকখনো text editor বা drawing app-এ reuse করা যাবে না। - ভূতুড়ে save file।
inventory: game.inventoryদেখো। এটা array-টা নয়, reference কপি করে। রুবেল পরে নতুন কোনো item তুললে, "saved" snapshot-টাও বদলে যায়! Save file নীরবে সেভ করার পরেও edit হচ্ছে। Restore করলে সে সময়ে পিছিয়ে যায় না — সে corrupt present-এ থেকে যায়। হরর মুভির উপাদান।
দুটো approach-এর পার্থক্য পাশাপাশি দেখো:
Memento pattern দায়িত্ব উল্টে দেয়। বাইরের কেউ ভেতরে ঢোকার বদলে, গেম নিজেই একটা sealed snapshot বানিয়ে বাইরে দেয়। গেম তার নিজের private fields পড়তে পারে — কারণ সেগুলো তার নিজের। History keeper sealed খাম রাখে। সবাই শুধু তার নিজের কাজ করে।
কলেজ কর্নার: এটা হলো encapsulation of snapshots, আর এটাই পরীক্ষায় আসার মতো এই pattern-এর মূল কথা। Encapsulation সাধারণত মানে "private state plus public behaviour"। Memento এটাকে সময়ের মধ্য দিয়ে বিস্তার করে: externalized state (snapshot) অবশ্যই live state-এর মতোই private থাকতে হবে। Gang of Four এটা language trick দিয়ে অর্জন করে — C++-এ friend দিয়ে, Java আর C#-এ nested private classes দিয়ে যেখানে memento class originator-এর ভেতরে থাকে। Caretaker একটা narrow marker interface-এর বিরুদ্ধে compile করে (কখনো কখনো আক্ষরিক অর্থেই একটা খালি interface IMemento)। যদি তোমার design-এ caretaker memento.getCoins() call করতে পারে, তুমি Memento implement করনি — তুমি extra step সহ একটা public struct বানিয়েছ।
কীভাবে কাজ করে, ধাপে ধাপে
যেকোনো language-এ follow করার recipe এখানে:
- Originator খোঁজো। কোন object-এর state rewind-এ টিকে থাকতে হবে? গেম ইঞ্জিন, editor document, drawing canvas।
- Memento class বানাও। যে state capture করতে চাও ঠিক সেই fields দাও। Constructor-এ set করো আর পরে কখনো পরিবর্তনের সুযোগ দিও না — snapshot drift করবে না।
- Memento সিল করো। তোমার language-এর tools ব্যবহার করো: Java/C#-এ nested class, শুধু originator পড়তে পারে এমন
privatefields, বা serialized string। Caretaker memento ধরতে পারবে কিন্তু পড়তে পারবে না। - Originator-এ
save()যোগ করো। এটা current state একটা নতুন memento-তে কপি করে। State-এ arrays বা objects থাকলে deeply কপি করো। - Originator-এ
restore(memento)যোগ করো। এটা memento থেকে current state overwrite করে। - Caretaker বানাও। সাধারণত একটা সহজ stack:
backup()originator.save()push করে;undo()একটা memento pop করে আরoriginator.restore()call করে।
রুবেলের রবিবার থেকে একটা পুরো save-and-undo round trip দেখো:
Flow সবসময় একই triangle। Caretaker জিজ্ঞেস করে; originator snapshot নেয়; caretaker রাখে। পরে: caretaker খাম ফেরত দেয়; originator নিজেকে rewind করে। Memento শুধু ভ্রমণ করে, নীরব আর sealed।
আর এখানে রুবেলের পুরো সন্ধ্যা, অনুভূতিসহ:
সেই ১ স্কোরটা লক্ষ্য করো। সবকিছু হারিয়ে ফেলা সাধারণত সন্ধ্যার শেষ হয়ে যেত। একটা snapshot থাকায় রাতের সবচেয়ে নিচু মুহূর্তটা ঠিক একটা loading screen-এর সমান সময় স্থায়ী হলো।
Blueprint টা দেখো 📐
এখানে class structure দেওয়া হলো। পরীক্ষায় এটাই আঁকতে হবে। সাবধানে লক্ষ্য করো কোন arrows আছে আর কোনগুলো নেই:
যে arrow নেই সেটাই গল্প বলে: SaveSlotManager থেকে SaveFile-এর fields-এ কোনো line নেই। Manager বাক্স রাখে; শুধু Game সেগুলো খোলে।
Originator একটা ছোট, অনুমানযোগ্য states-এর মধ্য দিয়ে যায়। এটাই এখন পর্যন্ত বানানো প্রতিটা undo system-এর ছন্দ:
লক্ষ্য করো Defeated-এর একটা exit আছে। Memento pattern ছাড়া Defeated একটা dead end হতো আর রুবেলের রবিবার চিরতরে চলে যেত।
আসল Code: TypeScript-এ রুবেলের গেম
চলো adventure game-টা সঠিকভাবে বানাই। Boss-এর আগে সেভ করো, fight করো, হারো, restore করো। Comments পড়ো — ওগুলো গল্প বহন করে।
// --- The Memento: a sealed save file ---
// All fields are private and readonly. Nothing can change after creation.
class SaveFile {
constructor(
private readonly level: number,
private readonly coins: number,
private readonly health: number,
private readonly inventory: string[],
public readonly savedAt: Date // tiny bit of metadata is fine to expose
) {}
// These getters exist ONLY for the Game to use during restore.
// In Java/C# we would seal this with a nested class. In TypeScript,
// we keep the rule by discipline: only Game calls these.
getState() {
return {
level: this.level,
coins: this.coins,
health: this.health,
inventory: [...this.inventory], // give out a copy, never the original
};
}
}
// --- The Originator: the game engine that owns the real state ---
class Game {
private level = 1;
private coins = 0;
private health = 100;
private inventory: string[] = [];
// Normal game play methods
collectCoins(amount: number) {
this.coins += amount;
console.log(`Collected ${amount} coins. Total: ${this.coins}`);
}
pickUpItem(item: string) {
this.inventory.push(item);
console.log(`Picked up: ${item}`);
}
fightDragon() {
// Spoiler: the dragon wins this time.
this.health = 0;
this.coins = 0;
this.inventory = [];
console.log("The dragon defeated you! Health 0, everything lost...");
}
// POWER 1: take a snapshot of MY OWN private state.
// No outsider needed. Deep copy the array so the snapshot never drifts.
save(): SaveFile {
console.log("💾 Game saved at the save point.");
return new SaveFile(this.level, this.coins, this.health, [...this.inventory], new Date());
}
// POWER 2: rewind myself from a snapshot.
restore(file: SaveFile) {
const s = file.getState();
this.level = s.level;
this.coins = s.coins;
this.health = s.health;
this.inventory = s.inventory;
console.log(`⏪ Game loaded! Level ${this.level}, ${this.coins} coins, health ${this.health}.`);
}
status() {
console.log(
`Status -> level: ${this.level}, coins: ${this.coins}, ` +
`health: ${this.health}, bag: [${this.inventory.join(", ")}]`
);
}
}
// --- The Caretaker: Rohan, who keeps save files but never opens them ---
class SaveSlotManager {
private slots: SaveFile[] = [];
constructor(private game: Game) {}
backup() {
this.slots.push(this.game.save()); // just stores the sealed file
}
undo() {
const file = this.slots.pop();
if (!file) {
console.log("No save file found!");
return;
}
this.game.restore(file); // hands it back, asks the game to rewind
}
}
// --- Rohan's Sunday evening ---
const game = new Game();
const rohan = new SaveSlotManager(game);
game.collectCoins(4500);
game.pickUpItem("Rare Sword");
game.status();
rohan.backup(); // 💾 save before the boss door
game.fightDragon(); // disaster!
game.status();
rohan.undo(); // ⏪ load the save — back at the door
game.status();Output পুরো নাটকটা বলে:
Collected 4500 coins. Total: 4500
Picked up: Rare Sword
Status -> level: 1, coins: 4500, health: 100, bag: [Rare Sword]
💾 Game saved at the save point.
The dragon defeated you! Health 0, everything lost...
Status -> level: 1, coins: 0, health: 0, bag: []
⏪ Game loaded! Level 1, 4500 coins, health 100.
Status -> level: 1, coins: 4500, health: 100, bag: [Rare Sword]তিনটা জিনিস একটু ভাবো:
Game-এর প্রতিটা field private থাকল। আমরা কখনো গেমের internals কারো কাছে খুলিনি — না manager-এর কাছে, না সুমাইয়ার cheat script-এর কাছে।SaveSlotManagerসম্পূর্ণ বোকা। এটা coins বা health সম্পর্কে কিছুই জানে না। কাল এটা সম্পূর্ণ আলাদা একটা গেমের বা text editor-এর save file পরিচালনা করতে পারবে — একটা line না বদলে।- Arrays deep copy হয়েছে — একবার
save()-এ আর একবারgetState()-এ। Snapshot নেওয়ার পরে সেটা কখনো ভুলবশত edit হবে না। এখানে কোনো ভূতুড়ে save file নেই।
Python-এ একই ধারণা: হোমওয়ার্কের Ctrl+Z
এবার দ্বিতীয় দৃশ্যকল্প যেটা তুমি খুব ভালো চেনো: হোমওয়ার্ক টাইপ করার সময় Ctrl+Z চাপা। ধরো ফাতেমা Physics assignment লিখছে আর একটা ভুল sentence টাইপ করে ফেলেছে। Python-এ undo সহ একটা ছোট text editor দেখো:
# --- Memento: one sealed snapshot of the document ---
class Snapshot:
def __init__(self, text: str, cursor: int):
# Single underscore = "private by convention" in Python
self._text = text
self._cursor = cursor
# --- Originator: the editor that owns the text ---
class Editor:
def __init__(self):
self._text = ""
self._cursor = 0
def type(self, words: str):
self._text += words
self._cursor = len(self._text)
print(f'Typed. Document is now: "{self._text}"')
def save(self) -> Snapshot:
return Snapshot(self._text, self._cursor)
def restore(self, snap: Snapshot):
# Only the editor reads the snapshot's insides.
self._text = snap._text
self._cursor = snap._cursor
print(f'Ctrl+Z! Document is back to: "{self._text}"')
# --- Caretaker: the undo stack ---
class UndoStack:
def __init__(self, editor: Editor):
self._editor = editor
self._stack = []
def backup(self):
self._stack.append(self._editor.save())
def undo(self):
if self._stack:
self._editor.restore(self._stack.pop())
# --- Homework time ---
editor = Editor()
undo = UndoStack(editor)
editor.type("The Taj Mahal is in Agra.")
undo.backup() # checkpoint before the next sentence
editor.type(" It was built in Mumbai.") # oops, wrong fact!
undo.undo() # Ctrl+Z saves the dayOutput:
Typed. Document is now: "The Taj Mahal is in Agra."
Typed. Document is now: "The Taj Mahal is in Agra. It was built in Mumbai."
Ctrl+Z! Document is back to: "The Taj Mahal is in Agra."Python সম্পর্কে একটা সৎ কথা: এটা caretaker-কে snapshot-এর বাইরে থাকতে force করতে পারে না — underscore একটা ভদ্র অনুরোধ, তালাবন্ধ দরজা নয়। Java আর C#-এ আমরা আসল enforcement পাই। চলো সেই আসল তালা দেখি।
C#-এ Sealed Version: তালাবন্ধ Save File 🔒
এই snippet সেই কৌশলটা দেখায় যেটা কলেজ পরীক্ষা পছন্দ করে: একটা private nested class। Memento originator-এর ভেতরে থাকে, তাই শুধু originator তার fields পড়তে পারে। বাইরের জগৎ শুধু একটা খালি marker interface দেখে:
// The narrow interface — this is ALL the caretaker ever sees.
public interface ISaveFile { DateTime SavedAt { get; } }
public class Game
{
private int _coins;
private int _health = 100;
public void CollectCoins(int amount) => _coins += amount;
public void FightDragon() { _coins = 0; _health = 0; }
// The wide interface exists only in here.
// Nobody outside Game can even NAME this class, let alone read it.
private sealed class SaveFile : ISaveFile
{
public int Coins { get; }
public int Health { get; }
public DateTime SavedAt { get; } = DateTime.Now;
public SaveFile(int coins, int health) { Coins = coins; Health = health; }
}
public ISaveFile Save() => new SaveFile(_coins, _health);
public void Restore(ISaveFile file)
{
// Only Game can unwrap the sealed envelope.
if (file is SaveFile s) { _coins = s.Coins; _health = s.Health; }
}
}
// The caretaker compiles against ISaveFile only.
// rohanSlots.Peek().Coins --> COMPILE ERROR. The seal is real.
var game = new Game();
var rohanSlots = new Stack<ISaveFile>();
game.CollectCoins(4500);
rohanSlots.Push(game.Save()); // save before the boss
game.FightDragon(); // disaster
game.Restore(rohanSlots.Pop()); // rewind — coins are backএটা TypeScript আর Python-এর মতো একই triangle, কিন্তু এখন compiler নিজেই খাম পাহারা দেয়। কোনো teammate যদি বাইরে থেকে file.Coins চেষ্টা করে, code-টাই build হবে না। সময় জুড়ে encapsulation, type system দ্বারা enforce হচ্ছে।
Save File-এর ভেতরে কী থাকে? 📊
ছাত্ররা কখনো কখনো save file-কে "কয়েকটা সংখ্যা" ভাবে। আসল গেমে এটা অনেক ভারী, আর সেই ভার Memento-র চারপাশের প্রতিটা design সিদ্ধান্তকে চালিত করে:
Snapshot ভারী হওয়ায় ঘন ঘন সেভ করা ব্যয়বহুল হয়ে পড়ে। দেখো রুবেলের গেম যদি প্রতিটা checkpoint-এ ৫ MB-এর পুরো snapshot সেভ করত, বনাম একটা স্মার্ট ইঞ্জিন যেটা checkpoints-এর মধ্যে শুধু পার্থক্য (diff) রাখে — memory-তে কী ঘটে:
কলেজ কর্নার: উপরের দুটো line এই pattern-এর classic space trade-off। k snapshot-এর জন্য state size S-এর full snapshot O(S × k) memory খায়, কিন্তু O(S) computation-এ restore করে। Diff-ভিত্তিক (incremental) snapshot অনেক কম space নেয় কিন্তু restore O(S + d × k) হয় — ঠিক video keyframe আর Git packfile যেভাবে কাজ করে। তৃতীয় বিকল্প হলো snapshot সম্পূর্ণ বাদ দিয়ে reverse operation রাখা (Command pattern), যেটা space-এ প্রায় কিছুই নেয় না কিন্তু প্রতিটা operation নির্ভরযোগ্যভাবে invertible হতে হয়। আসল system তিনটাই মেশায়: প্রতি N ধাপে একটা full memento, মাঝে diffs, সস্তা action-গুলোর জন্য commands।
এখানে এটা ব্যবহার করা উচিত?
দশ সেকেন্ডে scan করার টেবিল:
| পরিস্থিতি | Memento ব্যবহার করবে? | কেন |
|---|---|---|
| Object-এর state-এর undo/redo দরকার | ✅ হ্যাঁ | Classic, নিখুঁত fit |
| Operation fail হলে rollback দরকার (transactions) | ✅ হ্যাঁ | আগে সেভ করো, fail হলে restore করো |
| দীর্ঘ-চলমান process-এ checkpoints দরকার | ✅ হ্যাঁ | শেষ ভালো snapshot থেকে resume করো |
| বাইরে থেকে state পড়লে fields public করতে বাধ্য হবে | ✅ হ্যাঁ | Originator নিজেই snapshot নেয়; privacy টিকে থাকে |
| State ছোট আর ইতিমধ্যে public | ❌ না | সাধারণ copy বা Prototype clone সহজ |
| State বিশাল আর প্রতি সেকেন্ডে snapshot নেবে | ❌ সতর্ক | Memory বেলুনের মতো ফোলে; diff বা Command ব্যবহার করো |
| বাইরের effect সহ action undo করতে হবে (পাঠানো email!) | ❌ না | কোনো snapshot email unsend করতে পারে না; design নতুন করে ভাবো |
আসল Software-এ যেখানে দেখা যায়
Memento তোমার চারপাশে চুপচাপ কাজ করছে:
- Editor-এ Undo/redo। Text editor, Photoshop-এর মতো drawing app, আর IDE-গুলো state snapshot (বা diff)-এর stack রাখে যাতে Ctrl+Z rewind করতে পারে। এটাই Refactoring Guru আর GoF বই নিজেই বর্ণিত textbook ব্যবহার।
- Game save system আর checkpoint। ঠিক রুবেলের গল্প — console আর PC গেম world state একটা opaque save file-এ serialize করে যেটা শুধু engine interpret করতে পারে। Boss arena-র আগে auto-save মানে caretaker তোমার প্রতি সদয় হচ্ছে।
- Database transaction আর savepoint। Records পরিবর্তনের আগে databases rollback করার জন্য যথেষ্ট state রেকর্ড করে। একটা SQL
SAVEPOINTtransaction-এর একটা অংশের জন্য memento-র মতো কাজ করে: কিছু fail হলেROLLBACK TO SAVEPOINTআগের state restore করে। - Version control। একটা Git commit তোমার পুরো repository-র memento-র মতো আচরণ করে: একটা sealed snapshot যেখানে তুমি এক command-এ ফিরে যেতে পারো। (Git-এর internals আরো fancy — diff আর packfile, চিত্র ৭-এর মতো — কিন্তু mental model মিলে যায়।)
- Virtual machine আর disk snapshot। VirtualBox বা cloud provider-এর মতো tools তোমাকে একটা পুরো machine snapshot করতে আর ঝুঁকিপূর্ণ পরীক্ষার পরে restore করতে দেয়। Windows-এ System Restore হলো তোমার operating system-এর একটা memento।
- Open-source উদাহরণ। java-design-patterns repository-তে একটা পরিষ্কার runnable memento উদাহরণ আছে, আর GeeksforGeeks diagram সহ structure ব্যাখ্যা করে।
ছাত্ররা যে সাধারণ ভুলগুলো করে ⚠️
ভুল ১: Shallow copy — ভূতুড়ে save file। Originator-এর state-এ arrays, lists বা nested objects থাকলে আর তুমি শুধু reference কপি করলে, তোমার snapshot live object-এর সাথে সংযুক্ত থেকে যায়। রুবেল একটা নতুন item তোলে, আর পুরনো save file-টাও বদলে যায়। Restore করলে সে সময়ে পিছিয়ে যায় না — সে corrupt present-এ থেকে যায়। সবসময় memento-তে mutable state deep-copy করো: [...inventory], list(items), clone method। এই একটা bug ছাত্রদের project-এ আর production code-এ বেশিরভাগ broken undo feature-এর কারণ।
ভুল ২: প্রতিটা snapshot চিরতরে রাখা। প্রতিটা memento হলো state-এর একটা পুরো কপি। একটা বড় document-এ প্রতিটা keystroke-এ সেভ করলে memory buffet-এর মতো খেয়ে ফেলবে। আসল caretaker-দের একটা policy থাকে: শেষ N snapshot রাখো, পুরনোগুলো merge করো, বা diff রাখো (আবার চিত্র ৭)। Cleanup policy ছাড়া একটা caretaker হলো ধীর গতির memory leak।
আরো তিনটা দ্রুত ভুল:
- Caretaker-কে ভেতরে উঁকি মারতে দেওয়া। History class যদি কিছু display করতে
memento.getCoins()call করা শুরু করে, seal ভেঙে গেছে আর তুমি naive design-এ ফিরে গেছ। UI-কে যদি "Save 3 — রবিবার ৭:৪২ PM"-এর মতো label দরকার হয়, memento-কে শুধু safe metadata দাও — একটা নাম আর timestamp, যেমন আমাদেরsavedAtfield। - Memento mutable করা। Setter সহ snapshot আসলে snapshot নয়, এটা একটা time bomb। TypeScript-এ প্রতিটা field
readonly, Java-তেfinal, C#-এ get-only করো। - ভুল মুহূর্তে snapshot নেওয়া। পরিবর্তনের আগে সেভ করো, পরে নয়। ভুলের পরে সেভ করলে শুধু ভুলটাই সংরক্ষণ হয়। রুবেল দরজার আগে সেভ করে, dragon-এর প্রথম আগুনের গোলার পরে নয়।
Cousins-দের সাথে তুলনা
Memento-র দুটো cousin আছে যেটা ছাত্ররা গুলিয়ে ফেলে: Command (undo-র জন্য) আর Prototype (copy করার জন্য)।
| প্রশ্ন | Memento | Command | Prototype |
|---|---|---|---|
| মূল ধারণা | State-এর sealed snapshot সেভ করো | Action-কে object হিসেবে wrap করো | পুরো object clone করো |
| Undo style | সরাসরি পুরনো state restore করো | Reverse action চালাও | Object-কে আগের clone দিয়ে replace করো |
| Memory cost | প্রতিবার পুরো snapshot | ক্ষুদ্র (শুধু action info) | প্রতিবার পুরো clone |
| Internals জানে? | শুধু originator memento পড়ে | Command কী পরিবর্তন হয়েছিল জানে | Clone একটা সাধারণ open object |
| সেরা | Undo, rollback, checkpoint | Action history, queue, redo log | Self-contained state-এর দ্রুত copy |
| Game analogy | Save file | তোমার moves-এর replay, উল্টো করে | তোমার character-এর যমজ |
বড় editor-গুলোতে Command আর Memento প্রায়ই দল বাঁধে: প্রতিটা command (যেমন "paste paragraph") যে area পরিবর্তন করবে সেটার একটা memento নেয়। Command undo করলে সেই memento থেকে restore করে। Command কী হয়েছিল জানে; Memento কেমন ছিল মনে রাখে। একসাথে তারা bulletproof undo বানায়।
এক ছবিতে পুরো pattern 🗺️
দ্রুত Revision Box
+--------------------------------------------------------------+
| MEMENTO — QUICK REVISION |
+--------------------------------------------------------------+
| What : Save an object's state in a SEALED snapshot; |
| restore it later. Privacy stays intact. |
| Picture : Save Game before the boss fight. |
| Roles : Originator = game engine (save/restore) |
| Memento = save file (sealed, immutable) |
| Caretaker = player (stores, never opens) |
| Key calls: m = originator.save() |
| originator.restore(m) |
| Wins : Real undo, rollback, checkpoints; encapsulation |
| never breaks; caretaker stays dumb and reusable. |
| Dangers : Shallow copies (haunted saves!), memory bloat, |
| mutable mementos. |
| Cousins : Command = undo by reverse ACTION; |
| Prototype = clone when state is simple. |
| Used in : Editor Ctrl+Z, game saves, DB savepoints, |
| VM snapshots, Git-style commits. |
+--------------------------------------------------------------+Practice Exercise ✏️
নিজের হাতে এগুলো চেষ্টা করো। উপরের TypeScript game code থেকে শুরু করো।
-
Redo সাপোর্ট। এখন
SaveSlotManager-এ শুধু undo আছে।redoStackনামে একটা দ্বিতীয় stack যোগ করো। রুবেল undo করলে, restore করার আগে current state redo stack-এ push করো। তারপরredo()implement করো। এই sequence test করো: coins collect করো, backup করো, coins হারাও, undo করো, redo করো। State কি দুইদিকে সঠিকভাবে যায়? -
ফর্মের ফটোকপি। ধরো রুবেলের বাবা সরকারি অফিসে application form জমা দেওয়ার আগে একটা ফটোকপি রাখেন। একটা
ApplicationFormoriginator বানাও (fields: name, address, phone — সবই private), একটাPhotocopymemento, আর একটাFileCabinetcaretaker যেটা তারিখসহ ফটোকপি রাখে। Form জমা দাও, "ভুলবশত" address ভুল দিয়ে overwrite করো, আর ফটোকপি থেকে restore করো। -
History সীমা। Caretaker-কে সর্বোচ্চ ৫টা save slot দাও। ৬তম backup এলে পুরনোটা ছুঁড়ে ফেলতে হবে (নিচে queue-এর মতো, উপরে stack-এর মতো)। তারপর একটা বাক্য লেখো: একটা আসল text editor-এ এই সীমা কেন গুরুত্বপূর্ণ যেখানে user দিনে হাজার হাজার character টাইপ করে?
-
Seal ভাঙো, তারপর কষ্ট পাও (কলেজ)। ইচ্ছে করে TypeScript
SaveFileপরিবর্তন করোpublicfields ব্যবহার করতে, আরSaveSlotManager-কে একটা fancy UI label-এslots[0].coinsদেখাতে দাও। এখন game-এ একটা নতুন fieldmanaযোগ করো। প্রতিটা file গণনা করো যেটা তোমাকে edit করতে হবে, আর ভুলে গেলে যে bug দেখা যায় সেটা খোঁজো। Sealed version কী রোধ করত সে সম্পর্কে দুটো বাক্য লেখো। -
Diff snapshot (challenge)। চিত্র ৭-এর দ্বিতীয় line implement করো: প্রতিবার পুরো inventory সেভ করার বদলে,
save()শুধু শেষ সেভের পর থেকে যোগ হওয়া বা সরানো items রাখুক।restore()করো diffs উল্টো করে replay করে। ১,০০০-item inventory-র ১০০ save-এর জন্য memory পার্থক্য measure করো বা যুক্তি দিয়ে ব্যাখ্যা করো।
সচরাচর জিজ্ঞাসা
- এক লাইনে Memento pattern কী?
- এটা একটা behavioral design pattern যেটা একটা object-কে নিজের state-এর sealed snapshot বানাতে দেয় — যাতে পরে সেই exact state-এ ফিরে যাওয়া যায়। ঠিক গেমের save file-এর মতো।
- Memento pattern-এ তিনটা role কী কী?
- Originator (গেম ইঞ্জিন — snapshot বানায় আর পড়ে), Memento (save file — state-এর sealed কপি), আর Caretaker (player — save file রাখে কিন্তু কখনো খোলে না)।
- Caretaker কেন memento পড়তে পারে না?
- এটাই এই pattern-এর আসল কথা। Caretaker যদি snapshot পড়তে বা এডিট করতে পারত, তাহলে originator-এর private state বাইরে বেরিয়ে যেত আর encapsulation ভেঙে পড়ত। Memento তার creator ছাড়া সবার কাছে একটা black box।
- Ctrl+Z (undo) কি সত্যিই Memento দিয়ে বানানো?
- Editor-এ undo হলো এই pattern-এর classic ব্যবহার। প্রতিটা পরিবর্তনের আগে editor একটা memento সেভ করে। Ctrl+Z চাপলে সর্বশেষ memento-টা stack থেকে pop করে restore করা হয়।
- পুরো snapshot সেভ করলে কি memory নষ্ট হয় না?
- হতে পারে, যদি state অনেক বড় হয় বা ঘন ঘন সেভ করা হয়। সাধারণ সমাধান হলো history-র depth সীমিত রাখা, পুরো কপির বদলে diff সংরক্ষণ করা, অথবা Command pattern ব্যবহার করা।
আরো দেখো
সম্পর্কিত পাঠ
Command Pattern: প্রতিটি কাজকে একটি অর্ডার স্লিপে বদলে দাও
রেস্তোরাঁর অর্ডার স্লিপের গল্প দিয়ে Command pattern শেখো। TypeScript আর C#-এ undo ও redo সহ পুরো কোড, diagram, table, আর practice task।
Prototype Pattern: জিরো থেকে না বানিয়ে ফটোকপি করো
বিয়ের কার্ডের দোকানের গল্প দিয়ে Prototype design pattern শেখো — TypeScript আর Python-এর সহজ উদাহরণ, আর shallow vs deep copy-এর পরিষ্কার demo সহ।
Iterator Pattern: একে একে প্রতিটা element-এ যাও
Iterator pattern শেখো ট্রেনের টিকেট চেকারের গল্প দিয়ে। TypeScript-এ custom iterator বানাও, for...of আর generator ব্যবহার করো, আর C#-এ IEnumerable দেখো।
Mediator Pattern: Control Tower যেটা ক্যাওস থামায়
বিমানবন্দরের control tower-এর গল্প দিয়ে Mediator pattern শেখো — সহজ TypeScript আর C# কোড, MediatR উদাহরণ, diagram, table আর practice task সহ।