3.6. Memento

3.6.1. Amaç

Bir nesnenin, önceki durumuna (state) geri döndürülmesine (geri alarak başa döndürme (undo via rollback)) veya o anki durumuna erişilmesine olanak sağlamak. Ancak bunu gerçekleştirirken, o nesnenin uygulanımını (implementation) göstermez (yani nesne, güncel durumu geri döndürecek bir yöntem (method) içermek zorunda değildir).

Anımsatıcı deseninin uygulanımı üç nesne ile yapılır: Anımsatıcı (Memento), Oluşturucu (Originator), Yönetici (Caretaker).

Anımsatıcı - herhangi nesne veya bir kaynağa ait (string, number, array veya instance (bir nesne örneği) gibi) durumun somut ve özgün (unique) bir kopyasını içeren nesnedir. Buradaki özgünlük, benzer durumların diğer kopyalarda olmasının yasaklanması gerektiğini göstermez. Yani bu, durumun bağımsız bir klon olarak çıkarılabileceği anlamına gelir. Anımsatıcıda tutulan herhangi bir nesne, orijinal nesnenin bir referansı değil, onun tam bir kopyası olmalıdır. Anımsatıcı nesnesi “geçirimsiz (opaque)” bir nesnedir (nesne kimse tarafından değiştirilemez olmalı, değiştirilmemelidir).

Oluşturucu - türü kesin olarak belirlenmiş harici bir nesnenin gerçek durumunu içeren nesnedir. Oluşturucu, bu durumun özgün bir kopyasını oluşturabilir ve sarmalanmış (wrapped) bir biçimde anımsatıcıya döndürebilir. Oluşturucu değişikliklerin geçmişini bilmez. Güncel olarak alınacak soyut bir durum dışarıdan yani oluşturucu içerisinde belirlenebilir. Oluşturucu, verilen durumun izin verilen nesne türüyle uyumlu olduğundan emin olmalıdır. Oluşturucu, herhangi bir yönteme (method) sahip olabilir (olmayabilir de), ancak bu yöntemler nesnenin kayıtlı durumunda değişiklikler yapamaz.

Yönetici - değişikliklerin geçmişini kontrol eder. Bir nesnede değişiklik yapabilir, harici bir nesnenin durumunu oluşturucuya kaydetmek için karar alabilir, güncel durumun bir kopyasını oluşturucudan isteyebilir, veya geçmişten bir kopyanın durumu ile eşdeğer olarak oluşturucunun durumunu belirleyebilir.

3.6.2. Örnekler

  • The seed of a pseudorandom number generator
  • The state in a finite state machine
  • Control for intermediate states of ORM Model before saving

3.6.3. UML Diyagramı

Alt Momento UML Diagram

3.6.4. Kod

Bu kodu Github üzerinde de bulabilirsiniz.

Memento.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

class Memento
{
    public function __construct(private State $state)
    {
    }

    public function getState(): State
    {
        return $this->state;
    }
}

State.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

use InvalidArgumentException;

class State implements \Stringable
{
    public const STATE_CREATED = 'created';
    public const STATE_OPENED = 'opened';
    public const STATE_ASSIGNED = 'assigned';
    public const STATE_CLOSED = 'closed';

    private string $state;

    /**
     * @var string[]
     */
    private static array $validStates = [
        self::STATE_CREATED,
        self::STATE_OPENED,
        self::STATE_ASSIGNED,
        self::STATE_CLOSED,
    ];

    public function __construct(string $state)
    {
        self::ensureIsValidState($state);

        $this->state = $state;
    }

    private static function ensureIsValidState(string $state)
    {
        if (!in_array($state, self::$validStates)) {
            throw new InvalidArgumentException('Invalid state given');
        }
    }

    public function __toString(): string
    {
        return $this->state;
    }
}

Ticket.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

/**
 * Ticket is the "Originator" in this implementation
 */
class Ticket
{
    private State $currentState;

    public function __construct()
    {
        $this->currentState = new State(State::STATE_CREATED);
    }

    public function open()
    {
        $this->currentState = new State(State::STATE_OPENED);
    }

    public function assign()
    {
        $this->currentState = new State(State::STATE_ASSIGNED);
    }

    public function close()
    {
        $this->currentState = new State(State::STATE_CLOSED);
    }

    public function saveToMemento(): Memento
    {
        return new Memento(clone $this->currentState);
    }

    public function restoreFromMemento(Memento $memento)
    {
        $this->currentState = $memento->getState();
    }

    public function getState(): State
    {
        return $this->currentState;
    }
}

3.6.5. Test

Tests/MementoTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento\Tests;

use DesignPatterns\Behavioral\Memento\State;
use DesignPatterns\Behavioral\Memento\Ticket;
use PHPUnit\Framework\TestCase;

class MementoTest extends TestCase
{
    public function testOpenTicketAssignAndSetBackToOpen()
    {
        $ticket = new Ticket();

        // open the ticket
        $ticket->open();
        $openedState = $ticket->getState();
        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());

        $memento = $ticket->saveToMemento();

        // assign the ticket
        $ticket->assign();
        $this->assertSame(State::STATE_ASSIGNED, (string) $ticket->getState());

        // now restore to the opened state, but verify that the state object has been cloned for the memento
        $ticket->restoreFromMemento($memento);

        $this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
        $this->assertNotSame($openedState, $ticket->getState());
    }
}