handbookНаписать

patterns/creational/prototype/README.md · тема · 4 языка · 19 тестов

+ практика: КП из шаблона

Прототип (Prototype) ​

Новый объект создаётся копией уже настроенного, а глубину копии решает сам объект.

Проблема ​

Юристы ведут типовые документы: договор, оферта, NDA. Новый договор почти всегда «как прошлый, но для другого клиента». Собирать его конструктором с нуля — повторять десятки разделов; копировать поля руками — забыть одно и получить документ, который делит разделы с шаблоном, так что правка в копии меняет шаблон.

Решение ​

Объект сам умеет себя копировать: Document знает, какие части надо продублировать (разделы — изменяемые объекты), а какие можно переписать значением. Вызывающий код не знает устройства документа, он просит копию и правит её. Реестр Templates хранит именованные прототипы и на каждый запрос отдаёт свежую копию, причём при регистрации тоже копирует: шаблон не делит память ни с тем, кто его зарегистрировал, ни с теми, кто его получил.

Главный вопрос прототипа — глубина копии. Поверхностная копия делит вложенные объекты, глубокая дублирует всё. Правильный ответ почти всегда посередине и известен только самому объекту.

Структура ​

prototypes

clone()

Document

+title: string

+sections: Section[]

+tags: string[]

+clone() : Document

+withTitle(title) : Document

Section

+heading: string

+text: string

Templates

-items: Map<string, Document>

+add(name, prototype)

+create(name) : Document

Section намеренно изменяемая: именно поэтому копия документа обязана дублировать разделы, а не ссылки на них. Будь разделы неизменяемыми значениями, их можно было бы делить, и это дешевле: см. практику.

Как это собирается ​

Шаблоны регистрируются один раз при старте приложения: в жизни их читают из файлов или правят в админке, в примере Templates получает договор с двумя разделами и тегом legal. Дальше каждый менеджер просит create('contract'), переименовывает копию через withTitle() и правит разделы под клиента: стороны договора и, если клиент договорился, сроки оплаты.

В выводе видно, ради чего копия глубокая: Acme получил свои сроки и тег, Beta остался на стандартных, а свежая копия шаблона всё так же говорит Net 30. Запрос несуществующего шаблона invoice контроллер превращает в ответ 404.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Реестр — то, ради чего прототип применяют в приложениях: набор настроенных экземпляров, из которых штампуют новые. Он копирует на входе и на выходе, поэтому единственный, кто владеет прототипом, — сам реестр.

ЯзыкОсобенность
PHPclone встроен, но поверхностный; __clone() дублирует разделы
GoКопия структуры — присваивание, но срезы делят массив; метод Clone() клонирует их через slices.Clone
Pythoncopy.deepcopy копирует всё по ссылкам; dataclasses.replace даёт поверхностную копию с изменёнными полями
TypeScriptstructuredClone теряет класс и методы, поэтому явный 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} — поверхностный, поэтому для каждого уровня вложенности он пишется явно.

Практика ​

Учебная копия «просто глубокая». В проекте у объекта есть поля, которые копировать нельзя (номер, статус, дата), которые копировать обязательно (строки с количеством) и которые копировать вредно (справочники). Практика: КП из шаблона показывает копию коммерческого предложения для нового клиента с этими тремя сортами полей и шаблоны, замороженные в момент сохранения.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS