Прототип (Prototype)
Новый объект создаётся копией уже настроенного, а глубину копии решает сам объект.
Проблема
Юристы ведут типовые документы: договор, оферта, NDA. Новый договор почти всегда «как прошлый, но для другого клиента». Собирать его конструктором с нуля — повторять десятки разделов; копировать поля руками — забыть одно и получить документ, который делит разделы с шаблоном, так что правка в копии меняет шаблон.
Решение
Объект сам умеет себя копировать: Document знает, какие части надо продублировать (разделы — изменяемые объекты), а какие можно переписать значением. Вызывающий код не знает устройства документа, он просит копию и правит её. Реестр Templates хранит именованные прототипы и на каждый запрос отдаёт свежую копию, причём при регистрации тоже копирует: шаблон не делит память ни с тем, кто его зарегистрировал, ни с теми, кто его получил.
Главный вопрос прототипа — глубина копии. Поверхностная копия делит вложенные объекты, глубокая дублирует всё. Правильный ответ почти всегда посередине и известен только самому объекту.
Структура
Section намеренно изменяемая: именно поэтому копия документа обязана дублировать разделы, а не ссылки на них. Будь разделы неизменяемыми значениями, их можно было бы делить, и это дешевле: см. практику.
Как это собирается
Шаблоны регистрируются один раз при старте приложения: в жизни их читают из файлов или правят в админке, в примере Templates получает договор с двумя разделами и тегом legal. Дальше каждый менеджер просит create('contract'), переименовывает копию через withTitle() и правит разделы под клиента: стороны договора и, если клиент договорился, сроки оплаты.
В выводе видно, ради чего копия глубокая: Acme получил свои сроки и тег, Beta остался на стандартных, а свежая копия шаблона всё так же говорит Net 30. Запрос несуществующего шаблона invoice контроллер превращает в ответ 404.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Реестр — то, ради чего прототип применяют в приложениях: набор настроенных экземпляров, из которых штампуют новые. Он копирует на входе и на выходе, поэтому единственный, кто владеет прототипом, — сам реестр.
| Язык | Особенность |
|---|---|
| PHP | clone встроен, но поверхностный; __clone() дублирует разделы |
| Go | Копия структуры — присваивание, но срезы делят массив; метод Clone() клонирует их через slices.Clone |
| Python | copy.deepcopy копирует всё по ссылкам; dataclasses.replace даёт поверхностную копию с изменёнными полями |
| TypeScript | structuredClone теряет класс и методы, поэтому явный clone() со спредом |
Запуск: make test T=patterns/creational/prototype
Плюсы и минусы
Тесты проверяют независимость с трёх сторон: правка копии не трогает оригинал; две копии из реестра не видят друг друга; правка объекта после регистрации не меняет шаблон. Четвёртый тест — неизвестный шаблон падает сразу.
- ✅ Новый объект без знания его класса и конструктора: реестр отдаёт копию по имени.
- ✅ Настроенные экземпляры дешевле собирать копией, чем заново (особенно если настройка дорогая).
- ✅ Глубина копии инкапсулирована в объекте.
- ❌ Глубокое копирование графов с циклами и общими частями — отдельная задача; наивный deep copy дублирует то, что должно быть общим.
- ❌ Легко забыть обновить
clone()при добавлении поля-объекта.
Когда не применять
- Объект неизменяемый: копия не нужна, его можно делить.
- Объект создаётся дёшево из нескольких параметров: конструктор или фабрика понятнее.
- Нужна не копия, а вариант с другими значениями: часто хватает
with…/replaceбез реестра.
Отличия между языками
- PHP —
cloneплюс__clone()— классическая форма. Дляreadonlyклассов клон с изменением возможен только внутри__clone()(PHP 8.3+), поэтому изменяемый документ здесь проще. - Go — встроенного клона нет, и это честно: автор типа пишет
Clone()и решает, что копировать. Копирование структуры по значению делает поверхностную копию бесплатно, опасность только в срезах, картах и указателях. - Python —
copy.copy/copy.deepcopyи протокол__copy__/__deepcopy__. Для dataclass обычно хватаетreplace, когда вложенных изменяемых объектов нет. - TypeScript —
structuredCloneподходит для чистых данных, но не для классов. Спред{...s}— поверхностный, поэтому для каждого уровня вложенности он пишется явно.
Практика
Учебная копия «просто глубокая». В проекте у объекта есть поля, которые копировать нельзя (номер, статус, дата), которые копировать обязательно (строки с количеством) и которые копировать вредно (справочники). Практика: КП из шаблона показывает копию коммерческого предложения для нового клиента с этими тремя сортами полей и шаблоны, замороженные в момент сохранения.
Связанные темы
- Практика: КП из шаблона — что сбросить, что скопировать, что делить
- Строитель (Builder) — другой путь к настроенному объекту: по шагам вместо копии.