Снимок
Также известен как: Хранитель, Memento
Снимок — это поведенческий паттерн проектирования, который позволяет сохранять и восстанавливать прошлые состояния объектов, не раскрывая подробностей их реализации.

Проблема
Предположим, что вы пишете программу текстового редактора. Помимо обычного редактирования, ваш редактор позволяет менять форматирование текста, вставлять картинки и прочее.
В какой-то момент вы решили сделать все эти действия отменяемыми. Для этого вам нужно сохранять текущее состояние редактора перед тем, как выполнить любое действие. Если потом пользователь решит отменить своё действие, вы достанете копию состояния из истории и восстановите старое состояние редактора.

Перед выполнением команды вы можете сохранить копию состояния редактора, чтобы потом иметь возможность отменить операцию.
Чтобы сделать копию состояния объекта, достаточно скопировать значение его полей. Таким образом, если вы сделали класс редактора достаточно открытым, то любой другой класс сможет заглянуть внутрь, чтобы скопировать его состояние.
Казалось бы, что ещё нужно? Ведь теперь любая операция сможет сделать резервную копию редактора перед своим действием. Но такой наивный подход обеспечит вам уйму проблем в будущем. Ведь если вы решите провести рефакторинг — убрать или добавить парочку полей в класс редактора — то придётся менять код всех классов, которые могли копировать состояние редактора.

Как команде создать снимок состояния редактора, если все его поля приватные?
Но это ещё не все. Давайте теперь рассмотрим сами копии состояния редактора. Из чего состоит состояние редактора? Даже самый примитивный редактор должен иметь несколько полей для хранения текущего текста, позиции курсора и прокрутки экрана. Чтобы сделать копию состояния, вам нужно записать значения всех этих полей в некий «контейнер».
Скорее всего, вам понадобится хранить массу таких контейнеров в качестве истории операций, поэтому удобнее всего сделать их объектами одного класса. Этот класс должен иметь много полей, но практически никаких методов. Чтобы другие объекты могли записывать и читать из него данные, вам придётся сделать его поля публичными. Но это приведёт к той же проблеме, что и с открытым классом редактора. Другие классы станут зависимыми от любых изменений в классе контейнера, который подвержен тем же изменениям, что и класс редактора.
Получается, нам придётся либо открыть классы для всех желающих, испытывая массу хлопот с поддержкой кода, либо оставить классы закрытыми, отказавшись от идеи отмены операций. Нет ли какого-то другого пути?
Решение
Все проблемы, описанные выше, возникают из-за нарушения инкапсуляции. Это когда одни объекты пытаются сделать работу за других, влезая в их приватную зону, чтобы собрать необходимые для операции данные.
Паттерн Снимок поручает создание копии состояния объекта самому объекту, который этим состоянием владеет. Вместо того, чтобы делать снимок «извне», наш редактор сам сделает копию своих полей, ведь ему доступны все поля, даже приватные.
Паттерн предлагает держать копию состояния в специальном объекте-снимке с ограниченным интерфейсом, позволяющим, например, узнать дату изготовления или название снимка. Но, с другой стороны, снимок должен быть открыт для своего создателя, позволяя прочесть и восстановить его внутреннее состояние.

Снимок полностью открыт для создателя, но лишь частично открыт для опекунов.
Такая схема позволяет создателям производить снимки и отдавать их для хранения другим объектам, называемым опекунами. Опекунам будет доступен только ограниченный интерфейс снимка, поэтому они никак не смогут повлиять на «внутренности» самого снимка. В нужный момент опекун может попросить создателя восстановить своё состояние, передав ему соответствующий снимок.
В примере с редактором вы можете сделать опекуном отдельный класс, который будет хранить список выполненных операций. Ограниченный интерфейс снимков позволит демонстрировать пользователю красивый список с названиями и датами выполненных операций. А когда пользователь решит откатить операцию, класс истории возьмёт последний снимок из стека и отправит его объекту редактор для восстановления.
Структура
Классическая реализация на вложенных классах
Классическая реализация паттерна полагается на механизм вложенных классов, который доступен лишь в некоторых языках программирования (C++, C#, Java).

- Создатель может производить снимки своего состояния, а также воспроизводить прошлое состояние, если подать в него готовый снимок.
- Снимок — это простой объект данных, содержащий состояние создателя. Надёжнее всего сделать объекты снимков неизменяемыми, передавая в них состояние только через конструктор.
- Опекун должен знать, когда делать снимок создателя и когда его нужно восстанавливать.
Опекун может хранить историю прошлых состояний создателя в виде стека из снимков. Когда понадобится отменить выполненную операцию, он возьмёт «верхний» снимок из стека и передаст его создателю для восстановления. - В данной реализации снимок — это внутренний класс по отношению к классу создателя. Именно поэтому он имеет полный доступ к полям и методам создателя, даже приватным. С другой стороны, опекун не имеет доступа ни к состоянию, ни к методам снимков и может всего лишь хранить ссылки на эти объекты.
Реализация с пустым промежуточным интерфейсом
Подходит для языков, не имеющих механизма вложенных классов (например, PHP).

- В этой реализации создатель работает напрямую с конкретным классом снимка, а опекун — только с его ограниченным интерфейсом.
- Благодаря этому достигается тот же эффект, что и в классической реализации. Создатель имеет полный доступ к снимку, а опекун — нет.
Снимки с повышенной защитой
Когда нужно полностью исключить возможность доступа к состоянию создателей и снимков.

- Эта реализация разрешает иметь несколько видов создателей и снимков. Каждому классу создателей соответствует свой класс снимков. Ни создатели, ни снимки не позволяют другим объектам прочесть своё состояние.
- Здесь опекун ещё более жёстко ограничен в доступе к состоянию создателей и снимков. Но, с другой стороны, опекун становится независим от создателей, поскольку метод восстановления теперь находится в самих снимках.
- Снимки теперь связаны с теми создателями, из которых они сделаны. Они по-прежнему получают состояние через конструктор. Благодаря близкой связи между классами, снимки знают, как восстановить состояние своих создателей.
Псевдокод
В этом примере паттерн Снимок используется совместно с паттерном Команда и позволяет хранить резервные копии сложного состояния текстового редактора и восстанавливать его, если потребуется.

Пример сохранения снимков состояния текстового редактора.
Объекты команд выступают в роли опекунов и запрашивают снимки у редактора перед тем, как выполнить своё действие. Если потребуется отменить операцию, команда сможет восстановить состояние редактора, используя сохранённый снимок.
При этом снимок не имеет публичных полей, поэтому другие объекты не имеют доступа к его внутренним данным. Снимки связаны с определённым редактором, который их создал. Они же и восстанавливают состояние своего редактора. Это позволяет программе иметь одновременно несколько объектов редакторов, например, разбитых по разным вкладкам программы.
Применимость
Когда вам нужно сохранять мгновенные снимки состояния объекта (или его части), чтобы впоследствии объект можно было восстановить в том же состоянии.
Паттерн Снимок позволяет создавать любое количество снимков объекта и хранить их, независимо от объекта, с которого делают снимок. Снимки часто используют не только для реализации операции отмены, но и для транзакций, когда состояние объекта нужно «откатить», если операция не удалась.
Когда прямое получение состояния объекта раскрывает приватные детали его реализации, нарушая инкапсуляцию.
Паттерн предлагает изготовить снимок самому исходному объекту, поскольку ему доступны все поля, даже приватные.
Шаги реализации
- Определите класс создателя, объекты которого должны создавать снимки своего состояния.
- Создайте класс снимка и опишите в нём все те же поля, которые имеются в оригинальном классе-создателе.
- Сделайте объекты снимков неизменяемыми. Они должны получать начальные значения только один раз, через свой конструктор.
- Если ваш язык программирования это позволяет, сделайте класс снимка вложенным в класс создателя. Если нет, извлеките из класса снимка пустой интерфейс, который будет доступен остальным объектам программы. Впоследствии вы можете добавить в этот интерфейс некоторые вспомогательные методы, дающие доступ к метаданным снимка, однако прямой доступ к данным создателя должен быть исключён.
- Добавьте в класс создателя метод получения снимков. Создатель должен создавать новые объекты снимков, передавая значения своих полей через конструктор.
Сигнатура метода должна возвращать снимки через ограниченный интерфейс, если он у вас есть. Сам класс должен работать с конкретным классом снимка. - Добавьте в класс создателя метод восстановления из снимка. Что касается привязки к типам, руководствуйтесь той же логикой, что и в пункте 4.
- Опекуны, будь то история операций, объекты команд или нечто иное, должны знать о том, когда запрашивать снимки у создателя, где их хранить и когда восстанавливать.
- Связь опекунов с создателями можно перенести внутрь снимков. В этом случае каждый снимок будет привязан к своему создателю и должен будет сам восстанавливать его состояние. Но это будет работать либо если классы снимков вложены в классы создателей, либо если создатели имеют соответствующие сеттеры для установки значений своих полей.
Преимущества и недостатки
Преимущества
- Не нарушает инкапсуляции исходного объекта.
- Упрощает структуру исходного объекта. Ему не нужно хранить историю версий своего состояния.
Недостатки
- Требует много памяти, если клиенты слишком часто создают снимки.
- Может повлечь дополнительные издержки памяти, если объекты, хранящие историю, не освобождают ресурсы, занятые устаревшими снимками.
- В некоторых языках (например, PHP, Python, JavaScript) сложно гарантировать, чтобы только исходный объект имел доступ к состоянию снимка.
Примеры реализации паттерна
Python
- Сложность: 3/3
- Популярность: 1/3
- Применимость: Снимок на Python чаще всего реализуют с помощью сериализации. Но это не единственный, да и не самый эффективный метод сохранения состояния объектов во время выполнения программы.
Этот пример показывает структуру паттерна Снимок, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
Originator: My initial state is: Super-duper-super-puper-super.
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
wQAehHYOqVSlpEXjyIcgobrxsZUnat
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
lHxNORKcsgMWYnJqoXjVCbQLEIeiSp
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
cvIYsRilNOtwynaKdEZpDCQkFAXVMf
Caretaker: Here's the list of mementos:
2019-01-26 21:11:24 / (Super-dup...)
2019-01-26 21:11:24 / (wQAehHYOq...)
2019-01-26 21:11:24 / (lHxNORKcs...)
Client: Now, let's rollback!
Caretaker: Restoring state to: 2019-01-26 21:11:24 / (lHxNORKcs...)
Originator: My state has changed to: lHxNORKcsgMWYnJqoXjVCbQLEIeiSp
Client: Once more!
Caretaker: Restoring state to: 2019-01-26 21:11:24 / (wQAehHYOq...)
Originator: My state has changed to: wQAehHYOqVSlpEXjyIcgobrxsZUnat
PHP
- Сложность: 3/3
- Популярность: 1/3
- Применимость: Реальная применимость паттерна Снимок в мире PHP под большим вопросом. Чаще всего задачу хранения копии состояния можно решить куда проще при помощи сериализации.
Этот пример показывает структуру паттерна Снимок, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом. После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
Originator: My initial state is: Super-duper-super-puper-super.
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
srGIngezAEboNPDjBkuvymJKUtMSFX
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
UwCZQaHJOiERLlchyVuMbXNtpqTgWF
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to:
incqsdoJXkbDUuVOvRFYyKBgfzwZCQ
Caretaker: Here's the list of mementos:
2018-06-04 14:50:39 / (Super-dup...)
2018-06-04 14:50:39 / (srGIngezA...)
2018-06-04 14:50:39 / (UwCZQaHJO...)
Client: Now, let's rollback!
Caretaker: Restoring state to: 2018-06-04 14:50:39 / (UwCZQaHJO...)
Originator: My state has changed to: UwCZQaHJOiERLlchyVuMbXNtpqTgWF
Client: Once more!
Caretaker: Restoring state to: 2018-06-04 14:50:39 / (srGIngezA...)
Originator: My state has changed to: srGIngezAEboNPDjBkuvymJKUtMSFX
Задачи
Задача 1. Текстовый редактор с undo/redo
Реализуйте текстовый редактор с полноценной поддержкой undo и redo. Редактор поддерживает операции: ввод текста, удаление, изменение позиции курсора. Каждое действие сохраняет снимок состояния перед выполнением.
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class EditorMemento:
text: str
cursor_pos: int
selection_start: int
selection_end: int
label: str = ""
class TextEditor:
def __init__(self) -> None:
self._text = ""
self._cursor_pos = 0
self._selection_start = 0
self._selection_end = 0
def save(self, label: str = "") -> EditorMemento:
# TODO: сохранить текущее состояние в снимок
pass
def restore(self, memento: EditorMemento) -> None:
# TODO: восстановить состояние из снимка
pass
def type_text(self, text: str) -> None:
# TODO: вставить text в позицию курсора, сдвинуть курсор
pass
def delete_char(self) -> None:
# TODO: удалить символ перед курсором (backspace)
pass
def move_cursor(self, pos: int) -> None:
self._cursor_pos = max(0, min(pos, len(self._text)))
def select(self, start: int, end: int) -> None:
self._selection_start = start
self._selection_end = end
def delete_selection(self) -> None:
# TODO: удалить выделенный текст
pass
@property
def text(self) -> str:
return self._text
@property
def cursor_pos(self) -> int:
return self._cursor_pos
class EditorHistory:
def __init__(self, editor: TextEditor) -> None:
self._editor = editor
self._undo_stack: list[EditorMemento] = []
self._redo_stack: list[EditorMemento] = []
def save(self, label: str = "") -> None:
# TODO: сохранить снимок, очистить redo-стек
pass
def undo(self) -> bool:
# TODO: восстановить предыдущее состояние,
# текущее положить в redo-стек
pass
def redo(self) -> bool:
# TODO: применить отменённое действие,
# текущее положить в undo-стек
pass
def show_history(self) -> None:
for m in self._undo_stack:
print(f" - {m.label or '(без метки)'}: '{m.text}'")
# Ожидаемый результат:
# editor = TextEditor()
# history = EditorHistory(editor)
# history.save("initial")
# editor.type_text("Hello")
# history.save("typed Hello")
# editor.type_text(", World")
# history.save("typed , World")
# print(editor.text) # "Hello, World"
# history.undo()
# print(editor.text) # "Hello"
# history.undo()
# print(editor.text) # ""
# history.redo()
# print(editor.text) # "Hello"
<?php
class EditorMemento
{
public function __construct(
private readonly string $text,
private readonly int $cursorPos,
private readonly int $selectionStart,
private readonly int $selectionEnd,
private readonly string $label = "",
) {}
public function getText(): string { return $this->text; }
public function getCursorPos(): int { return $this->cursorPos; }
public function getSelectionStart(): int { return $this->selectionStart; }
public function getSelectionEnd(): int { return $this->selectionEnd; }
public function getLabel(): string { return $this->label; }
}
class TextEditor
{
private string $text = "";
private int $cursorPos = 0;
private int $selectionStart = 0;
private int $selectionEnd = 0;
public function save(string $label = ""): EditorMemento
{
// TODO: сохранить состояние в снимок
}
public function restore(EditorMemento $memento): void
{
// TODO: восстановить состояние из снимка
}
public function typeText(string $text): void
{
// TODO: вставить text в позицию курсора
}
public function deleteChar(): void
{
// TODO: backspace — удалить символ перед курсором
}
public function moveCursor(int $pos): void
{
$this->cursorPos = max(0, min($pos, mb_strlen($this->text)));
}
public function select(int $start, int $end): void
{
$this->selectionStart = $start;
$this->selectionEnd = $end;
}
public function deleteSelection(): void
{
// TODO: удалить выделенный текст
}
public function getText(): string { return $this->text; }
public function getCursorPos(): int { return $this->cursorPos; }
}
class EditorHistory
{
private array $undoStack = [];
private array $redoStack = [];
public function __construct(private readonly TextEditor $editor) {}
public function save(string $label = ""): void
{
// TODO: сохранить снимок, очистить redoStack
}
public function undo(): bool
{
// TODO: откатить к предыдущему состоянию,
// положить текущее в redoStack
}
public function redo(): bool
{
// TODO: применить отменённое действие,
// положить текущее в undoStack
}
public function showHistory(): void
{
foreach ($this->undoStack as $m) {
$label = $m->getLabel() ?: "(без метки)";
echo " - $label: '{$m->getText()}'\n";
}
}
}
// Ожидаемый результат:
// $editor = new TextEditor();
// $history = new EditorHistory($editor);
// $history->save("initial");
// $editor->typeText("Hello");
// $history->save("typed Hello");
// $editor->typeText(", World");
// $history->save("typed , World");
// echo $editor->getText() . "\n"; // "Hello, World"
// $history->undo();
// echo $editor->getText() . "\n"; // "Hello"
// $history->undo();
// echo $editor->getText() . "\n"; // ""
// $history->redo();
// echo $editor->getText() . "\n"; // "Hello"
Задача 2. Настройки игры с несколькими слотами сохранения
Реализуйте систему сохранений для игры с несколькими именованными слотами. Состояние персонажа включает: уровень, HP, позицию, инвентарь. Опекун позволяет сохранить в слот, загрузить из слота и показать все слоты с метаданными.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from copy import deepcopy
@dataclass
class GameMemento:
level: int
hp: int
position: tuple[float, float]
inventory: list[str]
saved_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
slot_name: str = ""
def __post_init__(self) -> None:
# Инвентарь должен быть независимой копией
self.inventory = list(self.inventory)
class GameCharacter:
def __init__(self, name: str) -> None:
self.name = name
self.level = 1
self.hp = 100
self.position = (0.0, 0.0)
self.inventory: list[str] = []
def save(self, slot_name: str = "") -> GameMemento:
# TODO: создать снимок текущего состояния
pass
def restore(self, memento: GameMemento) -> None:
# TODO: восстановить состояние из снимка
pass
def level_up(self) -> None:
self.level += 1
self.hp = min(100, self.hp + 20)
def move_to(self, x: float, y: float) -> None:
self.position = (x, y)
def pick_up(self, item: str) -> None:
self.inventory.append(item)
def take_damage(self, amount: int) -> None:
self.hp = max(0, self.hp - amount)
def __str__(self) -> str:
return (f"{self.name} | Lvl:{self.level} HP:{self.hp} "
f"Pos:{self.position} Inv:{self.inventory}")
class SaveManager:
def __init__(self) -> None:
self._slots: dict[str, GameMemento] = {}
def save(self, character: GameCharacter, slot: str) -> None:
# TODO: сохранить снимок персонажа в слот
pass
def load(self, character: GameCharacter, slot: str) -> bool:
# TODO: загрузить снимок из слота, вернуть False если слот пуст
pass
def list_slots(self) -> None:
# TODO: вывести все слоты с метаданными (имя, дата, уровень)
pass
def delete_slot(self, slot: str) -> bool:
# TODO: удалить слот
pass
# Ожидаемый результат:
# hero = GameCharacter("Aragorn")
# saves = SaveManager()
# hero.pick_up("Sword"); hero.move_to(10, 5)
# saves.save(hero, "slot1")
# hero.level_up(); hero.pick_up("Shield"); hero.move_to(50, 30)
# saves.save(hero, "slot2")
# hero.take_damage(80)
# print(hero) # Aragorn | Lvl:2 HP:20 Pos:(50, 30) Inv:['Sword', 'Shield']
# saves.load(hero, "slot1")
# print(hero) # Aragorn | Lvl:1 HP:100 Pos:(10, 5) Inv:['Sword']
# saves.list_slots()
<?php
class GameMemento
{
private readonly string $savedAt;
public function __construct(
private readonly int $level,
private readonly int $hp,
private readonly array $position,
private readonly array $inventory,
private readonly string $slotName = "",
) {
$this->savedAt = date("Y-m-d H:i:s");
}
public function getLevel(): int { return $this->level; }
public function getHp(): int { return $this->hp; }
public function getPosition(): array { return $this->position; }
public function getInventory(): array { return $this->inventory; }
public function getSavedAt(): string { return $this->savedAt; }
public function getSlotName(): string { return $this->slotName; }
}
class GameCharacter
{
public int $level = 1;
public int $hp = 100;
public array $position = [0.0, 0.0];
public array $inventory = [];
public function __construct(private readonly string $name) {}
public function save(string $slotName = ""): GameMemento
{
// TODO: создать снимок текущего состояния
}
public function restore(GameMemento $memento): void
{
// TODO: восстановить состояние из снимка
}
public function levelUp(): void { $this->level++; $this->hp = min(100, $this->hp + 20); }
public function moveTo(float $x, float $y): void { $this->position = [$x, $y]; }
public function pickUp(string $item): void { $this->inventory[] = $item; }
public function takeDamage(int $amount): void { $this->hp = max(0, $this->hp - $amount); }
public function __toString(): string
{
$pos = implode(", ", $this->position);
$inv = implode(", ", $this->inventory);
return "{$this->name} | Lvl:{$this->level} HP:{$this->hp} Pos:($pos) Inv:[$inv]";
}
}
class SaveManager
{
private array $slots = [];
public function save(GameCharacter $character, string $slot): void
{
// TODO: сохранить снимок в слот
}
public function load(GameCharacter $character, string $slot): bool
{
// TODO: загрузить из слота, вернуть false если пусто
}
public function listSlots(): void
{
// TODO: вывести все слоты с метаданными
}
public function deleteSlot(string $slot): bool
{
// TODO: удалить слот
}
}
// Ожидаемый результат:
// $hero = new GameCharacter("Aragorn");
// $saves = new SaveManager();
// $hero->pickUp("Sword"); $hero->moveTo(10, 5);
// $saves->save($hero, "slot1");
// $hero->levelUp(); $hero->pickUp("Shield"); $hero->moveTo(50, 30);
// $saves->save($hero, "slot2");
// $hero->takeDamage(80);
// echo $hero . "\n"; // Aragorn | Lvl:2 HP:20 Pos:(50, 30) Inv:[Sword, Shield]
// $saves->load($hero, "slot1");
// echo $hero . "\n"; // Aragorn | Lvl:1 HP:100 Pos:(10, 5) Inv:[Sword]
// $saves->listSlots();
Задача 3. Транзакционный менеджер конфигурации
Реализуйте менеджер конфигурации приложения с поддержкой транзакций. Можно начать транзакцию (begin), вносить изменения, а затем либо зафиксировать (commit), либо откатить (rollback). Поддерживаются вложенные транзакции (savepoint).
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass, field
@dataclass
class ConfigMemento:
data: dict
savepoint_name: str = ""
def __post_init__(self) -> None:
self.data = deepcopy(self.data)
class ConfigManager:
def __init__(self, initial: dict | None = None) -> None:
self._config: dict = deepcopy(initial or {})
self._transaction_stack: list[ConfigMemento] = []
self._in_transaction = False
def get(self, key: str, default=None):
# TODO: получить значение по ключу (поддержка вложенных: "db.host")
pass
def set(self, key: str, value) -> None:
# TODO: установить значение (поддержка вложенных: "db.host")
pass
def delete(self, key: str) -> bool:
# TODO: удалить ключ
pass
def begin(self, savepoint: str = "") -> None:
# TODO: начать транзакцию — сохранить снимок текущего состояния
pass
def commit(self) -> None:
# TODO: зафиксировать транзакцию — убрать снимок из стека
pass
def rollback(self, savepoint: str = "") -> None:
# TODO: откатить к снимку (по имени savepoint или к последнему)
pass
def all(self) -> dict:
return deepcopy(self._config)
# Ожидаемый результат:
# cfg = ConfigManager({"db": {"host": "localhost", "port": 5432}})
# print(cfg.get("db.host")) # localhost
# cfg.begin("before_update")
# cfg.set("db.host", "prod.example.com")
# cfg.set("app.debug", False)
# print(cfg.get("db.host")) # prod.example.com
# cfg.rollback("before_update")
# print(cfg.get("db.host")) # localhost
# print(cfg.get("app.debug")) # None
#
# cfg.begin()
# cfg.set("db.port", 5433)
# cfg.commit()
# print(cfg.get("db.port")) # 5433
<?php
class ConfigMemento
{
private readonly array $data;
public function __construct(array $data, private readonly string $savepointName = "")
{
$this->data = $data; // массивы копируются по значению в PHP
}
public function getData(): array { return $this->data; }
public function getSavepointName(): string { return $this->savepointName; }
}
class ConfigManager
{
private array $config;
private array $transactionStack = [];
public function __construct(array $initial = [])
{
$this->config = $initial;
}
public function get(string $key, mixed $default = null): mixed
{
// TODO: получить значение по ключу (поддержка "db.host")
}
public function set(string $key, mixed $value): void
{
// TODO: установить значение (поддержка "db.host")
}
public function delete(string $key): bool
{
// TODO: удалить ключ
}
public function begin(string $savepoint = ""): void
{
// TODO: сохранить снимок текущего состояния
}
public function commit(): void
{
// TODO: зафиксировать — убрать снимок из стека
}
public function rollback(string $savepoint = ""): void
{
// TODO: откатить к снимку по имени или к последнему
}
public function all(): array
{
return $this->config;
}
}
// Ожидаемый результат:
// $cfg = new ConfigManager(["db" => ["host" => "localhost", "port" => 5432]]);
// echo $cfg->get("db.host") . "\n"; // localhost
// $cfg->begin("before_update");
// $cfg->set("db.host", "prod.example.com");
// $cfg->set("app.debug", false);
// echo $cfg->get("db.host") . "\n"; // prod.example.com
// $cfg->rollback("before_update");
// echo $cfg->get("db.host") . "\n"; // localhost
// var_dump($cfg->get("app.debug")); // NULL
//
// $cfg->begin();
// $cfg->set("db.port", 5433);
// $cfg->commit();
// echo $cfg->get("db.port") . "\n"; // 5433
Задача 4. Версионирование документа
Реализуйте систему версионирования документа в стиле Git: каждый commit сохраняет снимок с сообщением и автором, поддерживаются checkout (переключение на версию), diff (список изменений между версиями) и log (история коммитов).
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
@dataclass(frozen=True)
class DocumentSnapshot:
content: str
title: str
author: str
message: str
timestamp: str = field(
default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
commit_hash: str = field(default="")
def __post_init__(self) -> None:
import hashlib
object.__setattr__(
self, "commit_hash",
hashlib.md5(
f"{self.content}{self.timestamp}".encode()
).hexdigest()[:8]
)
class Document:
def __init__(self, title: str, author: str) -> None:
self.title = title
self.author = author
self.content = ""
def edit(self, new_content: str) -> None:
self.content = new_content
def save(self, message: str) -> DocumentSnapshot:
# TODO: создать снимок текущего состояния
pass
def restore(self, snapshot: DocumentSnapshot) -> None:
# TODO: восстановить состояние из снимка
pass
class VersionControl:
def __init__(self, document: Document) -> None:
self._document = document
self._history: list[DocumentSnapshot] = []
self._current_index = -1
def commit(self, message: str) -> DocumentSnapshot:
# TODO: сохранить снимок, обрезать историю после current_index
pass
def checkout(self, commit_hash: str) -> bool:
# TODO: переключиться на версию по хешу
pass
def log(self) -> None:
# TODO: вывести историю коммитов (от новых к старым)
pass
def diff(self, hash1: str, hash2: str) -> list[str]:
# TODO: вернуть список строк, которые добавились/удалились
# (простое построчное сравнение)
pass
@property
def current(self) -> DocumentSnapshot | None:
if self._current_index >= 0:
return self._history[self._current_index]
return None
# Ожидаемый результат:
# doc = Document("README", "Alice")
# vc = VersionControl(doc)
# doc.edit("# Hello\nInitial content")
# v1 = vc.commit("Initial commit")
# doc.edit("# Hello\nUpdated content\nNew line")
# v2 = vc.commit("Add new line")
# vc.log()
# # [abc12345] 2026-01-01 12:00:00 Alice: Add new line
# # [def67890] 2026-01-01 11:59:00 Alice: Initial commit
# vc.checkout(v1.commit_hash)
# print(doc.content) # "# Hello\nInitial content"
<?php
class DocumentSnapshot
{
public readonly string $commitHash;
public readonly string $timestamp;
public function __construct(
private readonly string $content,
private readonly string $title,
private readonly string $author,
private readonly string $message,
) {
$this->timestamp = date("Y-m-d H:i:s");
$this->commitHash = substr(md5($this->content . $this->timestamp), 0, 8);
}
public function getContent(): string { return $this->content; }
public function getTitle(): string { return $this->title; }
public function getAuthor(): string { return $this->author; }
public function getMessage(): string { return $this->message; }
}
class Document
{
public string $content = "";
public function __construct(
public string $title,
public string $author,
) {}
public function edit(string $newContent): void
{
$this->content = $newContent;
}
public function save(string $message): DocumentSnapshot
{
// TODO: создать снимок текущего состояния
}
public function restore(DocumentSnapshot $snapshot): void
{
// TODO: восстановить состояние из снимка
}
}
class VersionControl
{
private array $history = [];
private int $currentIndex = -1;
public function __construct(private readonly Document $document) {}
public function commit(string $message): DocumentSnapshot
{
// TODO: сохранить снимок, обрезать историю после currentIndex
}
public function checkout(string $commitHash): bool
{
// TODO: переключиться на версию по хешу
}
public function log(): void
{
// TODO: вывести историю от новых к старым
}
public function diff(string $hash1, string $hash2): array
{
// TODO: построчное сравнение двух версий
}
public function getCurrent(): ?DocumentSnapshot
{
return $this->currentIndex >= 0 ? $this->history[$this->currentIndex] : null;
}
}
// Ожидаемый результат:
// $doc = new Document("README", "Alice");
// $vc = new VersionControl($doc);
// $doc->edit("# Hello\nInitial content");
// $v1 = $vc->commit("Initial commit");
// $doc->edit("# Hello\nUpdated content\nNew line");
// $v2 = $vc->commit("Add new line");
// $vc->log();
// // [abc12345] 2026-01-01 12:00:00 Alice: Add new line
// // [def67890] 2026-01-01 11:59:00 Alice: Initial commit
// $vc->checkout($v1->commitHash);
// echo $doc->content; // "# Hello\nInitial content"
Задача 5. Снимки состояния сложного объекта (глубокое копирование)
Реализуйте систему снимков для объекта Canvas (холст), содержащего список фигур. Каждая фигура — это объект со своими свойствами. Снимок должен быть независимой глубокой копией. Поддержите операции draw, move, resize, delete с возможностью отмены через снимки.
from __future__ import annotations
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any
@dataclass
class Shape:
shape_id: str
kind: str # "rect", "circle", "line"
x: float
y: float
width: float = 0.0
height: float = 0.0
color: str = "black"
extra: dict = field(default_factory=dict)
def move(self, dx: float, dy: float) -> None:
self.x += dx
self.y += dy
def resize(self, factor: float) -> None:
self.width *= factor
self.height *= factor
class CanvasMemento:
def __init__(self, shapes: list[Shape]) -> None:
# TODO: сохранить независимую глубокую копию списка фигур
pass
def get_shapes(self) -> list[Shape]:
# TODO: вернуть глубокую копию сохранённых фигур
pass
class Canvas:
def __init__(self) -> None:
self._shapes: list[Shape] = []
def draw(self, shape: Shape) -> None:
self._shapes.append(deepcopy(shape))
def move(self, shape_id: str, dx: float, dy: float) -> bool:
# TODO: переместить фигуру по id
pass
def resize(self, shape_id: str, factor: float) -> bool:
# TODO: изменить размер фигуры по id
pass
def delete(self, shape_id: str) -> bool:
# TODO: удалить фигуру по id
pass
def save(self) -> CanvasMemento:
# TODO: создать снимок текущего состояния холста
pass
def restore(self, memento: CanvasMemento) -> None:
# TODO: восстановить состояние из снимка
pass
def render(self) -> None:
for s in self._shapes:
print(f" [{s.shape_id}] {s.kind} at ({s.x},{s.y}) "
f"size={s.width}x{s.height} color={s.color}")
@property
def shapes(self) -> list[Shape]:
return self._shapes
class CanvasHistory:
def __init__(self, canvas: Canvas) -> None:
self._canvas = canvas
self._stack: list[CanvasMemento] = []
def push(self) -> None:
# TODO: сохранить снимок перед изменением
pass
def undo(self) -> bool:
# TODO: откатить к предыдущему снимку
pass
# Ожидаемый результат:
# canvas = Canvas()
# history = CanvasHistory(canvas)
# r1 = Shape("r1", "rect", x=0, y=0, width=100, height=50, color="red")
# c1 = Shape("c1", "circle", x=200, y=200, width=30, height=30, color="blue")
# history.push(); canvas.draw(r1)
# history.push(); canvas.draw(c1)
# history.push(); canvas.move("r1", 10, 20)
# canvas.render()
# # [r1] rect at (10,20) size=100x50 color=red
# # [c1] circle at (200,200) size=30x30 color=blue
# history.undo() # отмена move
# canvas.render()
# # [r1] rect at (0,0) size=100x50 color=red
# # [c1] circle at (200,200) size=30x30 color=blue
# history.undo() # отмена draw c1
# canvas.render()
# # [r1] rect at (0,0) size=100x50 color=red
<?php
class Shape
{
public function __construct(
public readonly string $shapeId,
public readonly string $kind,
public float $x,
public float $y,
public float $width = 0.0,
public float $height = 0.0,
public string $color = "black",
public array $extra = [],
) {}
public function move(float $dx, float $dy): void
{
$this->x += $dx;
$this->y += $dy;
}
public function resize(float $factor): void
{
$this->width *= $factor;
$this->height *= $factor;
}
}
class CanvasMemento
{
private array $shapes;
public function __construct(array $shapes)
{
// TODO: сохранить глубокую копию фигур (clone каждой)
}
public function getShapes(): array
{
// TODO: вернуть глубокую копию сохранённых фигур
}
}
class Canvas
{
private array $shapes = [];
public function draw(Shape $shape): void
{
$this->shapes[] = clone $shape;
}
public function move(string $shapeId, float $dx, float $dy): bool
{
// TODO: переместить фигуру по id
}
public function resize(string $shapeId, float $factor): bool
{
// TODO: изменить размер фигуры по id
}
public function delete(string $shapeId): bool
{
// TODO: удалить фигуру по id
}
public function save(): CanvasMemento
{
// TODO: создать снимок
}
public function restore(CanvasMemento $memento): void
{
// TODO: восстановить из снимка
}
public function render(): void
{
foreach ($this->shapes as $s) {
echo " [{$s->shapeId}] {$s->kind} at ({$s->x},{$s->y}) "
. "size={$s->width}x{$s->height} color={$s->color}\n";
}
}
}
class CanvasHistory
{
private array $stack = [];
public function __construct(private readonly Canvas $canvas) {}
public function push(): void
{
// TODO: сохранить снимок
}
public function undo(): bool
{
// TODO: откатить к предыдущему снимку
}
}
// Ожидаемый результат:
// $canvas = new Canvas();
// $history = new CanvasHistory($canvas);
// $r1 = new Shape("r1", "rect", x: 0, y: 0, width: 100, height: 50, color: "red");
// $c1 = new Shape("c1", "circle", x: 200, y: 200, width: 30, height: 30, color: "blue");
// $history->push(); $canvas->draw($r1);
// $history->push(); $canvas->draw($c1);
// $history->push(); $canvas->move("r1", 10, 20);
// $canvas->render();
// // [r1] rect at (10,20) size=100x50 color=red
// // [c1] circle at (200,200) size=30x30 color=blue
// $history->undo(); // отмена move
// $canvas->render();
// // [r1] rect at (0,0) size=100x50 color=red
// // [c1] circle at (200,200) size=30x30 color=blue
// $history->undo(); // отмена draw c1
// $canvas->render();
// // [r1] rect at (0,0) size=100x50 color=red