Хранитель (Memento) — шаблон проектирования
2 февраля 2020
Шаблон предлагает схему для сохранения свойств объекта в виде снимка, а также восстановление значений свойств из ранее сохранённого снимка.
Код реализации паттерна на языке 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, код:
Результат работы скрипта test_serialize.php:
Если мы хотим делать восстановление только определённых свойств и чтобы другие свойства объекта не затирались, то есть два варианта решения:
Шаблон «Хранитель» состоит из следующих классов:
Рассмотрим классы подробнее.
методы:
Далее создаём снимок состояния прямоугольника. Это будет первая контрольная точка сохранения
Затем заводим историю снимков и помещаем в неё созданный снимок
Меняем свойства прямоугольника, вновь делаем снимок состояния и сохраняем его в историю (вторая контрольная точка сохранения), выводим свойства фигуры на экран
В третий раз изменяем свойства прямоугольника и выводим все свойства на экран. Обратите внимание, что помимо других свойств, было изменено название фигуры (свойство name).
Итак, мы три раза меняли свойства прямоугольника и во время двух первых правок создали и сохранили в историю два снимка. Извлекаем из истории последний снимок и восстанавливаем из него состояние прямоугольника. После восстановления выводим список свойств.
Повторяем действие – извлекаем из истории следующий снимок, а затем восстанавливаем состояние объекта. После восстановления выводим список свойств.
Полный код скрипта test.php
Результат выполнения скрипта test.php
Как можно увидеть, при загрузке состояний из контрольных точек 1 и 2 восстановились все свойства, кроме свойства name, которое мы исключали из снимка.
ПРИМЕР ЗАДАЧИ: для процесса правки геометрической фигуры «Прямоугольник» реализовать сохранение и загрузку свойств: 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
)
Если мы хотим делать восстановление только определённых свойств и чтобы другие свойства объекта не затирались, то есть два варианта решения:
- Заводить новый метод, использующий сериализацию и внедрять в него обработки для исключения затирки значений свойств, которые не попали в сериализацию;
- Использовать схему, предложенную шаблоном «Хранитель». Этим мы и займёмся.
Шаблон «Хранитель» состоит из следующих классов:
- Хозяин (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, которое мы исключали из снимка.