Bu yazı Design Patterns/Tasarım Desenleri nedir? başlıklı yazı dizisinin bir parçasıdır.

Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.

Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.

Memento Tasarım Deseninin Amacı

Memento adını duyunca sizin de aklınıza Christopher Nolan’ın yönetmenliğini yaptığı o meşhur film mi geliyor bilmiyorum ama bu yazda tasarım deseni “design pattern” olan Memento‘yu inceleyeceğiz. Memento, bir nesnenin önceki durumunu, uygulama detaylarını açığa çıkartmadan kaydetmeniz ve geri getirmenizi sağlayan bir tasarım desenidir.

Sorun

Bir metin editörü uygulaması yazdığınızı düşünün. Uygulamanız sadece metni düzenleme işlemleri değil, metin biçimlendirme, metin içine görseller ekleme gibi özelliklere de sahip olsun.

Diyelim ki kullanıcıların yaptıkları değişiklikleri geri alabilmelerini sağlamaya karar verdiniz. Bu özellik yıllardır çoğu uygulamada varsayılan olarak bulunduğu için kullanıcıların uygulamanızdan böyle bir beklentisi var. Bunu şu şekilde doğrudan uygulamaya karar verdiniz; her işlemden önce uygulama tüm nesnelerin mevcut durumlarını bir depolama alanına kaydedecek, kullanıcı geri dönmeye karar verirse uygulamanız en saklama alanından en son anı alıp bunu bütün nesneleri o hale döndürmek için kullanacak.

Memento deseni nedir
Nesnenin özel (private) alanlarını nasıl kopyalacaksınız?

Dahası da var. Editörün anlık görüntülerini ele alalım. Bu görüntülerin içinde ne olur? En azından mevcut metin, imleçin o andaki konumu, kaydırma çubuğunun konumu vb. bilgiler olmalı değil mi? Bir anlık görüntü oluşturmak için bütün bu değerleri toplamalı ve bir çeşit konteyner içine yerleştirmelisiniz.

Bu konteyner listelerini de büyük ihtimalle bir geçmişi gösteren bir liste içinde tutacaksınız. Sonuç olarak bu konteynerler bir sınıfa ait nesneler haline gelecekler. Bu sınıfın neredeyse hiç bir metodu yokken, editörün durumunu yansıtan bir sürü alanı olacak. Diğer nesnelerin bu görüntüye yazmaları veya okuyabilmeleri için bu alanları açık (public) yapmanız gerekecek. Bu da özel (private) olsun ya da olmasın editörün durumundaki tüm bilgiyi açık hale getirecek. Diğer sınıflar, anlık görüntü sınıfındaki en ufak değişiklikten bile etkilenebilecek ve bağımlı hale gelecekler. (normalde özel (private) alanlar ve metotların kendi iç dinamikleri ile hallolacak konularda bile)

Görünüşe göre bir çıkmaza girdik, ya ilgili sınıfların tüm detaylarını dışa açık (public) hale getirecek ve çok kırılgan bir yapıya dönüştüreceksiniz, ya da durumlarına erişimi kısıtlayacak ama o zamanda anlık görüntü oluşturamayacaksınız. “Geri al” özelliğini uygulamak için başka bir yol yok mu?

Çözüm

Az önce yaşadığımız tüm sorunlar kapsamı sınırlandırmanın bozulmasından kaynaklanıyor. Bazı nesneler yapmaları gerekenden daha fazlasını yapıyorlar. Bir işlemi ilgili nesneye yaptırmak yerine, o işlemi yapmak için gereken veriyi nesnenin özel (private) alanlarına müdahale ederek yapmaya çalışıyorlar.

Memento deseni anlık görüntü oluşturma işini, asıl nesnenin kendisine delege ediyor. Başka nesnelerin dışarıdan editörün durumunu kopyalamaya çalışması yerine editör sınıfının kendi anlık görüntüsünü oluşturmasını sağlıyor.

Bu desen nesnenin durum kopyasını memento adı verilen özel bir nesnede saklamayı öneriyor. Memento’nun içeriği kendisini oluşturandan başka hiç bir nesne tarafından ulaşılabilir değil. Diğer nesneler, Memento ile sınırlı bir arayüz üzerinden iletişim kurarak sadece anlık görüntü meta bilgilerine (oluşturma zamanı, yapılan işlemin adı vs. gibi bilgiler) ulaşabilirler. Nesnenin orijinal durumu ise görüntünün kendisinde yer alır.

Memento deseni örneği
Orijinal nesne mementoya tam erişime sahipken, bakıcılar sadece sınırlı bilgilere ulaşabilirler.

Bu sınırlayıcı politika mementoları, bakıcı olarak adlandırılan başka nesnelerin içinde depolamaya olanak verir. Bakıcı memento ile çok sınırlı bir arayüz üzerinden iletişim kurduğu için memento içinde saklanan görüntüye erişemez. Aynı zamanda asıl nesnenin memento içerisindeki tüm nesnelere erişebilmesi ve gerektiğinde önceki duruma getirebilmesi gerekir.

Uygulanabilirlik

Bir nesneyi önceki duruma getirmeniz için anlık görüntüler oluşturmanız gerekiyorsa Memento desenini kullanabilirsiniz

Memento deseni bir nesnenin özel alanlar da dahil olmak tam bir kopyasını oluşturmanıza ve nesneden ayrı olarak saklamanıza olanak veren bir desendir. Çoğu insan bu deseni “geri al” kullanımı nedeniyle biliyor olsa da işleme (transaction) dayalı işlemlerle uğraşırken de vazgeçilmez olanaklar sağlar. ( hata durumunda işlemi geri almak için )

Bir nesnenin alanları, getirici (getter) ve ayarlayıcılarına ( setter ) ulaşmak nesnenin kapsam sınırlarını ihlal ediyorsa bu deseni kullanabilirsiniz.

Memento, bir nesnenin durumunu saklamak için sorumluluğu nesnenin kendisine verir. Başka hiç bir nesne durumun bu anlık görüntüsüne ulaşamaz, böylece nesnenin verileri güvenli ve emniyetli kalır.

Diğer tasarım desenleri/kalıpları ile ilişkisi

  • Geri Al ( Undo ) işlevi için Command ve Memento‘yu birlikte uygulayabilirsiniz. Bu durumda Command’ler hedef nesneye bazı işlemler yapmaktan sorumlu iken, Memento’lar Command çalıştırılmadan önce nesnenin durumunu kaydetmek için kullanılır.
  • Mevcut dolaşım durumunu yakalamak ve gerekirse geri döndürmek için Iterator ve Memento‘yu birlikte kullanabilirsiniz.
  • Prototype bazen Memento’ya basit bir alternatif olabilir. Bu durumunu (state) saklamak istediğiniz nesne basit ve dış kaynaklarla bağlantıyısı olmayan veya bu kaynaklara yeniden bağlanması kolay bir nesne ise geçerlidir.

Memento Tasarım Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\Memento\Conceptual;

/**
 * The Originator holds some important state that may change over time. It also
 * defines a method for saving the state inside a memento and another method for
 * restoring the state from it.
 */
class Originator
{
    /**
     * @var string For the sake of simplicity, the originator's state is stored
     * inside a single variable.
     */
    private $state;

    public function __construct(string $state)
    {
        $this->state = $state;
        echo "Originator: My initial state is: {$this->state}\n";
    }

    /**
     * The Originator's business logic may affect its internal state. Therefore,
     * the client should backup the state before launching methods of the
     * business logic via the save() method.
     */
    public function doSomething(): void
    {
        echo "Originator: I'm doing something important.\n";
        $this->state = $this->generateRandomString(30);
        echo "Originator: and my state has changed to: {$this->state}\n";
    }

    private function generateRandomString(int $length = 10): string
    {
        return substr(
            str_shuffle(
                str_repeat(
                    $x = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
                    ceil($length / strlen($x))
                )
            ),
            1,
            $length,
        );
    }

    /**
     * Saves the current state inside a memento.
     */
    public function save(): Memento
    {
        return new ConcreteMemento($this->state);
    }

    /**
     * Restores the Originator's state from a memento object.
     */
    public function restore(Memento $memento): void
    {
        $this->state = $memento->getState();
        echo "Originator: My state has changed to: {$this->state}\n";
    }
}

/**
 * The Memento interface provides a way to retrieve the memento's metadata, such
 * as creation date or name. However, it doesn't expose the Originator's state.
 */
interface Memento
{
    public function getName(): string;

    public function getDate(): string;
}

/**
 * The Concrete Memento contains the infrastructure for storing the Originator's
 * state.
 */
class ConcreteMemento implements Memento
{
    private $state;

    private $date;

    public function __construct(string $state)
    {
        $this->state = $state;
        $this->date = date('Y-m-d H:i:s');
    }

    /**
     * The Originator uses this method when restoring its state.
     */
    public function getState(): string
    {
        return $this->state;
    }

    /**
     * The rest of the methods are used by the Caretaker to display metadata.
     */
    public function getName(): string
    {
        return $this->date . " / (" . substr($this->state, 0, 9) . "...)";
    }

    public function getDate(): string
    {
        return $this->date;
    }
}

/**
 * The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
 * doesn't have access to the originator's state, stored inside the memento. It
 * works with all mementos via the base Memento interface.
 */
class Caretaker
{
    /**
     * @var Memento[]
     */
    private $mementos = [];

    /**
     * @var Originator
     */
    private $originator;

    public function __construct(Originator $originator)
    {
        $this->originator = $originator;
    }

    public function backup(): void
    {
        echo "\nCaretaker: Saving Originator's state...\n";
        $this->mementos[] = $this->originator->save();
    }

    public function undo(): void
    {
        if (!count($this->mementos)) {
            return;
        }
        $memento = array_pop($this->mementos);

        echo "Caretaker: Restoring state to: " . $memento->getName() . "\n";
        try {
            $this->originator->restore($memento);
        } catch (\Exception $e) {
            $this->undo();
        }
    }

    public function showHistory(): void
    {
        echo "Caretaker: Here's the list of mementos:\n";
        foreach ($this->mementos as $memento) {
            echo $memento->getName() . "\n";
        }
    }
}

/**
 * Client code.
 */
$originator = new Originator("Super-duper-super-puper-super.");
$caretaker = new Caretaker($originator);

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

echo "\n";
$caretaker->showHistory();

echo "\nClient: Now, let's rollback!\n\n";
$caretaker->undo();

echo "\nClient: Once more!\n\n";
$caretaker->undo();

Örnek Python Kodu

from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime
from random import sample
from string import ascii_letters, digits


class Originator():
    """
    The Originator holds some important state that may change over time. It also
    defines a method for saving the state inside a memento and another method
    for restoring the state from it.
    """

    _state = None
    """
    For the sake of simplicity, the originator's state is stored inside a single
    variable.
    """

    def __init__(self, state: str) -> None:
        self._state = state
        print(f"Originator: My initial state is: {self._state}")

    def do_something(self) -> None:
        """
        The Originator's business logic may affect its internal state.
        Therefore, the client should backup the state before launching methods
        of the business logic via the save() method.
        """

        print("Originator: I'm doing something important.")
        self._state = self._generate_random_string(30)
        print(f"Originator: and my state has changed to: {self._state}")

    def _generate_random_string(self, length: int = 10) -> None:
        return "".join(sample(ascii_letters, length))

    def save(self) -> Memento:
        """
        Saves the current state inside a memento.
        """

        return ConcreteMemento(self._state)

    def restore(self, memento: Memento) -> None:
        """
        Restores the Originator's state from a memento object.
        """

        self._state = memento.get_state()
        print(f"Originator: My state has changed to: {self._state}")


class Memento(ABC):
    """
    The Memento interface provides a way to retrieve the memento's metadata,
    such as creation date or name. However, it doesn't expose the Originator's
    state.
    """

    @abstractmethod
    def get_name(self) -> str:
        pass

    @abstractmethod
    def get_date(self) -> str:
        pass


class ConcreteMemento(Memento):
    def __init__(self, state: str) -> None:
        self._state = state
        self._date = str(datetime.now())[:19]

    def get_state(self) -> str:
        """
        The Originator uses this method when restoring its state.
        """
        return self._state

    def get_name(self) -> str:
        """
        The rest of the methods are used by the Caretaker to display metadata.
        """

        return f"{self._date} / ({self._state[0:9]}...)"

    def get_date(self) -> str:
        return self._date


class Caretaker():
    """
    The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
    doesn't have access to the originator's state, stored inside the memento. It
    works with all mementos via the base Memento interface.
    """

    def __init__(self, originator: Originator) -> None:
        self._mementos = []
        self._originator = originator

    def backup(self) -> None:
        print("\nCaretaker: Saving Originator's state...")
        self._mementos.append(self._originator.save())

    def undo(self) -> None:
        if not len(self._mementos):
            return

        memento = self._mementos.pop()
        print(f"Caretaker: Restoring state to: {memento.get_name()}")
        try:
            self._originator.restore(memento)
        except Exception:
            self.undo()

    def show_history(self) -> None:
        print("Caretaker: Here's the list of mementos:")
        for memento in self._mementos:
            print(memento.get_name())


if __name__ == "__main__":
    originator = Originator("Super-duper-super-puper-super.")
    caretaker = Caretaker(originator)

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    print()
    caretaker.show_history()

    print("\nClient: Now, let's rollback!\n")
    caretaker.undo()

    print("\nClient: Once more!\n")
    caretaker.undo()

Diğer Tasarım Kalıpları/Design Patterns

Yaratımsal Kalıplar (Creational Patterns)

Yapısal Kalıplar (Structural Patterns)

Davranışsal Kalıplar (Behavioral Patterns)