Behavioral — Davranışsal Desenler
Memento
Hatıra (Anlık Görüntü) Deseni
Amaç: Bir nesnenin iç durumunu, kapsüllemeyi bozmadan kaydetmek ve daha sonra nesneyi bu duruma geri döndürebilmek.
Basit Anlatım
Teknik terim yok — günlük hayattan bir benzetme
Bir video oyunu oynuyorsunuz ve zorlu bir bölüme girmeden önce oyunu kaydediyorsunuz. Bölümde kaybederseniz kayıt noktasına geri dönersiniz: canınız, envanteriniz, konumunuz kaydettiğiniz andaki gibi olur.
Kayıt dosyasının içinde ne olduğunu bilmezsiniz, açıp değiştiremezsiniz de. Sadece "şu kaydı yükle" dersiniz. İçeriği yalnızca oyun anlayabilir.
Telefon yedeği
Telefonunuzu güncellemeden önce yedek alırsınız. Bir sorun çıkarsa yedekten geri dönersiniz. Yedeği saklayan bulut servisi, içinde hangi fotoğrafların veya mesajların olduğuyla ilgilenmez; sadece saklar.
Teknik Anlamı
Bir yazılımcının bakış açısıyla
Memento, bir nesnenin durumunun anlık görüntüsünü (snapshot) ayrı bir nesnede saklar. Kritik nokta: durumu yalnızca onu üreten nesne okuyabilir; saklayan taraf içeriğe erişemez. Böylece encapsulation korunur.
Katılımcılar
- Originator: Durumu kaydedilen nesne. Memento üretir ve memento'dan geri yüklenir (
GameCharacter). - Memento: Durumun değiştirilemez (immutable) anlık görüntüsü.
- Caretaker: Memento'ları saklar ama içeriğine dokunmaz (
SaveManager).
Command deseniyle ilişkisi
Undo'nun iki yolu vardır: Command her işlemin tersini bilir; Memento ise önceki durumun tamamını saklar. Tersini hesaplamanın zor olduğu durumlarda Memento daha basittir, ama daha fazla bellek kullanır.
Kullanım alanları
- Undo/Redo, oyun kayıt sistemleri
- İşlem (transaction) geri alma, form taslakları
- Uzun süren bir işlemin "checkpoint"leri
Avantajlar
- Nesnenin iç yapısını dışarıya açmadan durum kaydedilir.
- Originator'ın kodu sadeleşir; geçmiş yönetimi Caretaker'a devredilir.
Dezavantajlar
- Sık ve büyük snapshot'lar ciddi bellek tüketebilir.
C# ile Uygulama
Adım adım, çalışan kod örneği
Senaryo: Bir oyun karakterinin durumunu kayıt noktalarında saklamak ve gerektiğinde geri yüklemek.
Adım 1 Memento: değiştirilemez anlık görüntü
C# record tipi immutable olduğu için memento için idealdir. Liste de kopyalanarak saklanır, böylece sonradan değişiklikler kaydı etkilemez.
public sealed record CharacterSnapshot(
int Level, int Health, string Location, IReadOnlyList<string> Inventory,
DateTime SavedAt);Adım 2 Originator: durumu kaydedilen nesne
Alanlar private. Dışarıdan doğrudan değiştirilemez; sadece kendi snapshot'ını üretip ondan geri yüklenebilir.
public class GameCharacter
{
private int _level = 1;
private int _health = 100;
private string _location = "Köy";
private List<string> _inventory = new() { "Tahta Kılıç" };
public void Play(string location, int damage, string? loot = null)
{
_location = location;
_health -= damage;
if (loot is not null) _inventory.Add(loot);
if (damage == 0) _level++;
}
public CharacterSnapshot Save() =>
new(_level, _health, _location, _inventory.ToList(), DateTime.Now);
public void Restore(CharacterSnapshot s)
{
_level = s.Level;
_health = s.Health;
_location = s.Location;
_inventory = s.Inventory.ToList();
}
public override string ToString() =>
$"Seviye {_level} | Can {_health,3} | {_location,-12} | {string.Join(", ", _inventory)}";
}Adım 3 Caretaker: kayıtları saklar ama içine bakmaz
public class SaveManager
{
private readonly Dictionary<string, CharacterSnapshot> _slots = new();
public void Save(string slot, GameCharacter character)
{
_slots[slot] = character.Save();
Console.WriteLine($" [Kaydedildi: {slot}]");
}
public void Load(string slot, GameCharacter character)
{
character.Restore(_slots[slot]);
Console.WriteLine($" [Yüklendi: {slot}]");
}
}Adım 4 Kullanım
Tüm parçaları bir araya getiren istemci (client) kodu:
var hero = new GameCharacter();
var saves = new SaveManager();
Console.WriteLine(hero);
hero.Play("Orman", damage: 0, loot: "Demir Kalkan");
Console.WriteLine(hero);
saves.Save("Kayıt-1", hero);
hero.Play("Ejderha İni", damage: 80, loot: "Ejderha Pulu");
Console.WriteLine(hero);
hero.Play("Ejderha İni", damage: 45);
Console.WriteLine(hero + " <- karakter öldü!");
saves.Load("Kayıt-1", hero);
Console.WriteLine(hero);Konsol çıktısı
Seviye 1 | Can 100 | Köy | Tahta Kılıç Seviye 2 | Can 100 | Orman | Tahta Kılıç, Demir Kalkan [Kaydedildi: Kayıt-1] Seviye 2 | Can 20 | Ejderha İni | Tahta Kılıç, Demir Kalkan, Ejderha Pulu Seviye 2 | Can -25 | Ejderha İni | Tahta Kılıç, Demir Kalkan, Ejderha Pulu <- karakter öldü! [Yüklendi: Kayıt-1] Seviye 2 | Can 100 | Orman | Tahta Kılıç, Demir Kalkan
Kodda dikkat edilmesi gerekenler
SaveManagersnapshot'ları saklıyor ama karakterin canını ya da envanterini değiştiremiyor; çünkürecordimmutable ve karakterin alanlarıprivate.Save()envanter listesini kopyalıyor (ToList()). Kopyalamasaydı, sonradan eklenen "Ejderha Pulu" eski kayda da sızacaktı.