3.5. Memento

3.5.1. Rôle

It provides the ability to restore an object to it’s previous state (undo via rollback) or to gain access to state of the object, without revealing it’s implementation (i.e., the object is not required to have a function to return the current state).

Le pattern Memento est mis en œuvre avec trois objets : Originator, Caretaker et un Memento.

Memento - Objet contenant un instantané concret et unique de l’état de tout objet ou ressource : chaîne de caractères, nombre, tableau, instance de classe, etc. L’unicité dans ce cas n’implique pas l’interdiction de l’existence d’états similaires dans différents instantanés. Cela signifie que l’état peut être extrait en tant que clone indépendant. Tout objet stocké dans le Memento doit être une copie complète de l’objet original plutôt qu’une référence à l’objet original. L’objet Mémento est un « objet opaque » (l’objet que personne ne peut ou ne doit modifier).

Originator - Objet contenant l’état réel d’un objet externe de type strictement spécifié. L’Originator est capable de créer une copie unique de cet état et de la renvoyer enveloppée dans un Memento. L’Originator ne connaît pas l’historique des changements. Vous pouvez définir un état concret pour l’Originator depuis l’extérieur, qui sera considéré comme réel. L’Originator doit s’assurer que l’état donné correspond au type d’objet autorisé. L’Originator peut (mais ne doit pas) avoir des méthodes, mais elles ne peuvent pas modifier l’état de l’objet sauvegardé.

Caretaker - Objet contrôlant l’historique des états. Il peut apporter des modifications à un objet, prendre la décision de sauvegarder l’état d’un objet externe dans l’Originator, demander à l’Originator un instantané de l’état actuel, ou mettre l’état de l’Originator en équivalence avec un instantané de l’histoire.

3.5.2. Exemples

  • La source d’un générateur de nombres pseudo-aléatoires

  • L’état dans une machine à états finis

  • Contrôle des états intermédiaires du modèle ORM <http://en.wikipedia.org/wiki/Object-relational_mapping> avant la sauvegarde

3.5.3. Diagramme UML

Alt Momento UML Diagram

3.5.4. Code

Vous pouvez également trouver ce code sur GitHub

Memento.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

class Memento
{
    private State $state;

    public function __construct(State $stateToSave)
    {
        $this->state = $stateToSave;
    }

    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
<?php declare(strict_types=1);

namespace DesignPatterns\Behavioral\Memento;

use InvalidArgumentException;

class State
{
    const STATE_CREATED = 'created';
    const STATE_OPENED = 'opened';
    const STATE_ASSIGNED = 'assigned';
    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
<?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.5.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
<?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());
    }
}