Хранитель (Memento) — шаблон проектирования

2 февраля 2020
Шаблон предлагает схему для сохранения свойств объекта в виде снимка, а также восстановление значений свойств из ранее сохранённого снимка. 

ПРИМЕР ЗАДАЧИ: для процесса правки геометрической фигуры «Прямоугольник» реализовать сохранение и загрузку свойств: X, Y, ширина (width), высота (height) и цвет (color) в виде контрольных точек.

Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)

Задачу сохранения и загрузки свойств объекта в PHP можно решить сериализацией https://www.php.net/manual/ru/language.oop5.serialization.php, при которой с помощью магических методов __serialize() и __unserialize() (данные методы появились в PHP 7.4) можно указывать конкретные свойства для сохранения и загрузки. Но минус этого способа в том, что во время дисериализации затрутся значения свойств, которые не попали в сериализацию.

Покажу на примере. Допустим, у нас есть объект класса A с шестью заполненными свойствами: публичные prop1, prop2, prop3 и защищённые (protected) prop4, prop5, prop6. Если сериализировать объект, то в строку сериализации попадут все шесть свойств. Теперь берём объект класса B, который наследуется от класса A и также заполняем все шесть свойств. В магическом методе __serialize() класса B мы выбрали для сохранения три свойства prop1, prop5 и prop6, поэтому в сериализованную строку попали только эти свойства. Затем восстанавливаем объект класса B из сериализованной строки и видим, что у объекта заполнены только свойства prop1, prop5 и prop6, а другие свойства prop2, prop3 и prop4 пустые.

Пример скрипта находится в файле test_serialize.php, код: 
<?

/**
 * Класс A
 */
class A
{
    /**
     * Свойство 1
     * @var mixed
     */
    public $prop1;

    /**
     * Свойство 2
     * @var mixed
     */
    public $prop2;

    /**
     * Свойство 3
     * @var mixed
     */
    public $prop3;

    /**
     * Свойство 4
     * @var mixed
     */
    protected $prop4;

    /**
     * Свойство 5
     * @var mixed
     */
    protected $prop5;

    /**
     * Свойство 6
     * @var mixed
     */
    protected $prop6;

    /**
     * Установка значения свойства 4
     * @param mixed $value
     */
    public function setProp4($value)
    {
        $this->prop4 = $value;
    }

    /**
     * Установка значения свойства 5
     * @param mixed $value
     */
    public function setProp5($value)
    {
        $this->prop5 = $value;
    }

    /**
     * Установка значения свойства 6
     * @param mixed $value
     */
    public function setProp6($value)
    {
        $this->prop6 = $value;
    }

    /**
     * Выводитна экран список свойств объекта и их значений
     */
    public function echo()
    {
        $ar = array();
        foreach ($this as $key => $value) {
            $ar[$key] = $value;
        }

        echo 'Объект класса '.get_class($this).'<pre>'.print_r($ar, true).'</pre>';
    }
}

//Создаём объект класса А и наполняем его свойства значениями
$a = new A();
$a->prop1 = '1';
$a->prop2 = '2';
$a->prop3 = '3';
$a->setProp4('4');
$a->setProp5('5');
$a->setProp6('6');

//Выводим значения свойств объекта
$a->echo();

//Сериализуем объект и выводим строку сериализации
echo serialize($a);
echo '<br><br>';

/**
 * Класс B
 */
class B extends A
{
    /**
     * Магический метод, вызываемый перед сериализацией и возвращаемый массив свойств (в котором ключ элемента - имя свойства, 
     * а элемент - значение свойства), которые должны быть сохранены. Те свойства, которые не попадут в массив, не попадут и в
     * сериализованную строку, т.е не будут сохранены.
     * 
     * @return array
     */
    public function __serialize()
    {
        return array(
            'prop1' => $this->prop1,
            'prop5' => $this->prop5,
            'prop6' => $this->prop6
        );
    }
}

//Создаём объект класса B и наполняем его свойства значениями
$b = new B();
$b->prop1 = '1';
$b->prop2 = '2';
$b->prop3 = '3';
$b->setProp4('4');
$b->setProp5('5');
$b->setProp6('6');

//Выводим значения свойств объекта
$b->echo();

//Сериализуем объект и выводим строку сериализации
$savedB = serialize($b);
echo $savedB;
echo '<br><br>';

//Восстанавливаем объект $b из сериализованной строки и выводим на экран список свойств восстановленного объекта
echo 'Восстанавливаем объект $b из сохраннной строки';
$b = unserialize($savedB);
$b->echo();

?>

Результат работы скрипта test_serialize.php:
Объект класса A
Array
(
    [prop1] => 1
    [prop2] => 2
    [prop3] => 3
    [prop4] => 4
    [prop5] => 5
    [prop6] => 6
)

O:1:"A":6:{s:5:"prop1";s:1:"1";s:5:"prop2";s:1:"2";s:5:"prop3";s:1:"3";s:8:"*prop4";s:1:"4";s:8:"*prop5";s:1:"5";s:8:"*prop6";s:1:"6";}

Объект класса B
Array
(
    [prop1] => 1
    [prop2] => 2
    [prop3] => 3
    [prop4] => 4
    [prop5] => 5
    [prop6] => 6
)

O:1:"B":3:{s:5:"prop1";s:1:"1";s:5:"prop5";s:1:"5";s:5:"prop6";s:1:"6";}

Восстанавливаем объект $b из сохраннной строкиОбъект класса B
Array
(
    [prop1] => 1
    [prop2] => 
    [prop3] => 
    [prop4] => 
    [prop5] => 5
    [prop6] => 6
)

Если мы хотим делать восстановление только определённых свойств и чтобы другие свойства объекта не затирались, то есть два варианта решения:
  1. Заводить новый метод, использующий сериализацию и внедрять в него обработки для исключения затирки значений свойств, которые не попали в сериализацию;
  2. Использовать схему, предложенную шаблоном «Хранитель». Этим мы и займёмся.

Шаблон «Хранитель» состоит из следующих классов:
  • Хозяин (Originator) — исходный класс, свойства которого мы будем сохранять в виде снимка состояния. Класс содержит метод для получения и возвращения Снимка, а так же метод для восстановления значений свойств объекта из Снимка;
  • Снимок / Хранитель (Snapshot / Memento) — представляет собой набор свойств и значений исходного объекта, записанные в момент создания Снимка. В литературе этот класс часто называют Хранителем (Memento), но данное обозначение далеко по смыслу от своей функциональной роли, более близкий и корректный вариант обозначения — Снимок (Snapshot);
  • Управляющий снимками (Caretaker) — хранит и возвращает Снимок.

rectangle.classes.php

Файл содержит классы:
  • Rectangle – прямоугольник; 
  • Snapshot – снимок состояния объекта (прямоугольника);
  • HistorySnapshots –  история снимков.

Рассмотрим классы подробнее.

Rectangle

Представляет геометрическую фигуру — прямоугольник. Класс содержит следующие свойства и методы:
методы:
  • $name – имя прямоугольника;
  • $x – расположение на оси X;
  • $y – расположение на оси Y;
  • $width – ширина прямоугольника;
  • $height – высота;
  • $color – цвет. Указывается в форме HTML-записи, например, #333333 или red;
  • Конструктор __construct($x, $y, $width, $height) – создаёт объект прямоугольника;
  • printProps() – выводит на экран список свойств прямоугольника и их значений;
  • saveSnapshot() – возвращает снимок (объект класса Snapshot) с текущим состоянием прямоугольника. Под состоянием понимается набор определённых свойств прямоугольника и их значения. Тут важно отметить, что в снимок могут попадать не все свойства, а только те, которые действительно характеризуют состояние объекта. Например, не всегда есть смысл записывать в снимок промежуточный кэш объект. В нашем демонстративном примере из снимка исключено свойство name (имя прямоугольника);
  • loadFromSnapshot($snapshot) – загружает состояние прямоугольника из снимка $snapshot.
/**
 * Прямоугольник
 */
class Rectangle
{
    /**
     * Название фигуры
     * @var string
     */
    public $name;

    /**
     * Расположение на оси X
     * @var int
     */
    public $x;

    /**
     * Расположение на оси Y
     * @var int
     */
    public $y;

    /**
     * Ширина
     * @var int
     */
    public $width;

    /**
     * Высота
     * @var int
     */
    public $height;

    /**
     * Цвет. Указывается в форме HTML-записи. Например, '#333333' или 'red'
     * @var string
     */
    public $color;

    /**
     * Конструктор. Создаёт объект прямоугольника
     * @param int $x - расположение на оси X
     * @param int $y - расположение на оси Y
     * @param int $width - ширина прямоугольника
     * @param int $height - высота прямоугольника
     */
    public function __construct($x, $y, $width, $height)
    {
        $this->x = $x;
        $this->y = $y;
        $this->width = $width;
        $this->height = $height;
    }

    /**
     * Выводит на экран список свойств объекта и их значений
     */
    public function printProps()
    {
        echo '<pre>'.print_r(get_object_vars($this), true).'</pre>';
    }

    /**
     * Возвращает снимок с текущим состоянием объекта (прямоугольника)
     * @return Snapshot
     */
    public function saveSnapshot()
    {
        $props = array(
            'x' => $this->x,
            'y' => $this->y,
            'width' => $this->width,
            'height' => $this->height,
            'color' => $this->color
        );

        return new Snapshot($props);
    }

    /**
     * Загружает состояние объекта (прямоугольника) из снимка
     * @param Snapshot $snapshot - снимок состояния
     * @return bool - вернёт 'true' в случае успешного выполнения и 'false' в случае ошибки
     */
    public function loadFromSnapshot($snapshot)
    {
        $props = $snapshot->getProps();

        if (is_array($props)) {
            $this->x = $props['x'];
            $this->y = $props['y'];
            $this->width = $props['width'];
            $this->height = $props['height'];
            $this->color = $props['color'];

            return true;
        } else {
            return false;
        }
    }
}

Snapshot

Реализует снимок состояния объекта прямоугольника. Состоит из следующих свойств и методов:
  • $props – массив, состоящий из свойств и значений сохранённого состояния объекта.
    В ключе элемента массива находится имя свойства, а в значении – значение;
  • Конструктор __construct($props) – создаёт объект снимка. В параметре $props передаётся массив со свойствами сохраняемого состояния объекта;
  • getProps() – возвращает массив со свойствами и значениями сохранённого состояния объекта.
/**
 * Снимок состояния объекта
 */
class Snapshot
{
    /**
     * Содержит свойства и значения сохранённого состояния объекта. 
     * В ключе элемента массива находится имя свойства, а в значении - значение
     * @var array
     */
    private $props = array();

    /**
     * Конструктор. Создаёт снимок
     * @param array $props - массив, содержащий свойства и значения сохраняемого состояния объекта. 
     *                       В ключе элемента массива находится имя свойства, а в значении - значение
     */
    public function __construct($props)
    {
        foreach ($props as $name => $value) {
            $this->props[$name] = $value;
        }
    }

    /**
     * Возвращает массив со свойствами и значениями сохранённого состояния объекта
     * @return array
     */
    public function getProps()
    {
        return $this->props;
    }
}

HistorySnapshots

Представляет историю снимков. Состоит из следующих свойств и методов:
  • $history – массив со снимками;
  • add($snapshot)  – добавляет снимок $snapshot в историю;
  • getLast() – возвращает из истории последний снимок;
  • deleteLast() – удаляет из истории последний снимок.
/**
 * История снимков состояний объектов
 */
class HistorySnapshots
{
    /**
     * Содержит массив со снимками
     * @var Snapshot[]
     */
    private $history = array();

    /**
     * Добавляет снимок в историю
     * @param Snapshot $snapshot - снимок состояния объекта
     * @return bool - вернёт 'true' в случае успешного выполнения и 'false' в случае ошибки
     */
    public function add($snapshot)
    {
        $this->history[] = $snapshot;
        return true;
    }

    /**
     * Возвращает из истории последний снимок
     * @return Snapshot|bool - в случае успеха вернёт объект класса Snapshot, а в случае неудачи вернёт 'false'
     */
    public function getLast()
    {
        if (count($this->history) > 0) {
            return $this->history[array_key_last($this->history)];
        } else {
            return false;
        }
    }

    /**
     * Удаляет из истории последний снимок
     * @return bool - вернёт 'true' в случае успешного выполнения и 'false' в случае ошибки
     */
    public function deleteLast()
    {
        if (count($this->history) > 0) {
            unset($this->history[array_key_last($this->history)]);
            return true;
        } else {
            return false;
        }
    }
}

test.php

Содержит тестовый скрипт, в котором создаётся прямоугольник $rectangle, указывается его имя и цвет, а затем выводятся на экран все свойства и значения этого объекта:
//Создаём прямоугольник и выводим его свойства на экран
$rectangle = new Rectangle(0, 0, 500, 200);
$rectangle->name = 'Фигура';
$rectangle->color = '#FF4500';
echo 'СОСТОЯНИЕ 1:<br>';
echo '-------------------<br>';
$rectangle->printProps();

Далее создаём снимок состояния прямоугольника. Это будет первая контрольная точка сохранения
//Создаём снимок состояния объекта прямоугольника
$snapshot = $rectangle->saveSnapshot();

Затем заводим историю снимков и помещаем в неё созданный снимок 
//Создаём историю снимков и помещаем в неё наш снимок 
$historySnapshots = new HistorySnapshots();
$historySnapshots->add($snapshot);

Меняем свойства прямоугольника, вновь делаем снимок состояния и сохраняем его в историю (вторая контрольная точка сохранения), выводим свойства фигуры на экран
//Изменяем свойства прямоугольника, делаем и сохраняем снимок состояния, выводим свойства прямоугольника на экран
$rectangle->x = 10;
$rectangle->y = 80;
$rectangle->width = 30;
$rectangle->height = 40;
$rectangle->color = 'green';
$snapshot = $rectangle->saveSnapshot();
$historySnapshots->add($snapshot);
echo 'СОСТОЯНИЕ 2:<br>';
echo '-------------------<br>';
$rectangle->printProps();

В третий раз изменяем свойства прямоугольника и выводим все свойства на экран. Обратите внимание, что помимо других свойств, было изменено название фигуры (свойство name). 
//Снова изменяем свойства прямоугольника и выводим их на экран
$rectangle->name = 'Прямоугольник';
$rectangle->x = 400;
$rectangle->y = 400;
$rectangle->width = 350;
$rectangle->height = 800;
$rectangle->color = 'black';
echo 'СОСТОЯНИЕ 3:<br>';
echo '-------------------<br>';
$rectangle->printProps();

Итак, мы три раза меняли свойства прямоугольника и во время двух первых правок создали и сохранили в историю два снимка. Извлекаем из истории последний снимок и восстанавливаем из него состояние прямоугольника. После восстановления выводим список свойств.
/*
Восстанавливаем свойства прямоугольника из последнего снимка (с удалением снимка после восстановления), 
затем выводим свойства прямоугольника на экран
*/
$snapshot = $historySnapshots->getLast();
$historySnapshots->deleteLast();
if ($snapshot) {
    $rectangle->loadFromSnapshot($snapshot);
} else {
    echo 'Снимка состояния прямоугольника в истории нету<br>';
}
echo 'СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 2:<br>';
echo '-------------------<br>';
$rectangle->printProps();

Повторяем действие – извлекаем из истории следующий снимок, а затем восстанавливаем состояние объекта. После восстановления выводим список свойств.
/*
Восстанавливаем свойства прямоугольника из первого снимка (с удалением снимка после восстановления), 
затем выводим свойства прямоугольника на экран
*/
$snapshot = $historySnapshots->getLast();
$historySnapshots->deleteLast();
if ($snapshot) {
    $rectangle->loadFromSnapshot($snapshot);
} else {
    echo 'Снимка состояния прямоугольника в истории нету<br>';
}
echo 'СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 1:<br>';
echo '-------------------<br>';
$rectangle->printProps();

Полный код скрипта test.php
<?
//Подключаем классы для работы с прямоугольником
require_once(__DIR__.'/rectangle.classes.php');

//Создаём прямоугольник и выводим его свойства на экран
$rectangle = new Rectangle(0, 0, 500, 200);
$rectangle->name = 'Фигура';
$rectangle->color = '#FF4500';
echo 'СОСТОЯНИЕ 1:<br>';
echo '-------------------<br>';
$rectangle->printProps();

//Создаём снимок состояния объекта прямоугольника
$snapshot = $rectangle->saveSnapshot();

//Создаём историю снимков и помещаем в неё наш снимок 
$historySnapshots = new HistorySnapshots();
$historySnapshots->add($snapshot);

//Изменяем свойства прямоугольника, делаем и сохраняем снимок состояния, выводим свойства прямоугольника на экран
$rectangle->x = 10;
$rectangle->y = 80;
$rectangle->width = 30;
$rectangle->height = 40;
$rectangle->color = 'green';
$snapshot = $rectangle->saveSnapshot();
$historySnapshots->add($snapshot);
echo 'СОСТОЯНИЕ 2:<br>';
echo '-------------------<br>';
$rectangle->printProps();

//Снова изменяем свойства прямоугольника и выводим их на экран
$rectangle->name = 'Прямоугольник';
$rectangle->x = 400;
$rectangle->y = 400;
$rectangle->width = 350;
$rectangle->height = 800;
$rectangle->color = 'black';
echo 'СОСТОЯНИЕ 3:<br>';
echo '-------------------<br>';
$rectangle->printProps();

/*
Восстанавливаем свойства прямоугольника из последнего снимка (с удалением снимка после восстановления), 
затем выводим свойства прямоугольника на экран
*/
$snapshot = $historySnapshots->getLast();
$historySnapshots->deleteLast();
if ($snapshot) {
    $rectangle->loadFromSnapshot($snapshot);
} else {
    echo 'Снимка состояния прямоугольника в истории нету<br>';
}
echo 'СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 2:<br>';
echo '-------------------<br>';
$rectangle->printProps();

/*
Восстанавливаем свойства прямоугольника из первого снимка (с удалением снимка после восстановления), 
затем выводим свойства прямоугольника на экран
*/
$snapshot = $historySnapshots->getLast();
$historySnapshots->deleteLast();
if ($snapshot) {
    $rectangle->loadFromSnapshot($snapshot);
} else {
    echo 'Снимка состояния прямоугольника в истории нету<br>';
}
echo 'СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 1:<br>';
echo '-------------------<br>';
$rectangle->printProps();

?>

Результат выполнения скрипта test.php
СОСТОЯНИЕ 1:
-------------------

Array
(
    [name] => Фигура
    [x] => 0
    [y] => 0
    [width] => 500
    [height] => 200
    [color] => #FF4500
)

СОСТОЯНИЕ 2:
-------------------

Array
(
    [name] => Фигура
    [x] => 10
    [y] => 80
    [width] => 30
    [height] => 40
    [color] => green
)

СОСТОЯНИЕ 3:
-------------------

Array
(
    [name] => Прямоугольник
    [x] => 400
    [y] => 400
    [width] => 350
    [height] => 800
    [color] => black
)

СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 2:
-------------------

Array
(
    [name] => Прямоугольник
    [x] => 10
    [y] => 80
    [width] => 30
    [height] => 40
    [color] => green
)

СОСТОЯНИЕ ПОСЛЕ ВОССТАНОВЛЕНИЯ ИЗ КОНТРОЛЬНОЙ ТОЧКИ 1:
-------------------

Array
(
    [name] => Прямоугольник
    [x] => 0
    [y] => 0
    [width] => 500
    [height] => 200
    [color] => #FF4500
)

Как можно увидеть, при загрузке состояний из контрольных точек 1 и 2 восстановились все свойства, кроме свойства name, которое мы исключали из снимка.