handbookНаписать

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

база: Прототип

Практика: КП из шаблона (Quote from template) ​

Копия коммерческого предложения для нового клиента: сбросить идентичность, продублировать строки, оставить общими товары каталога. Глубина копии — бизнес-решение, а не техническая деталь.

Базовый пример: Прототип. Там копия «просто глубокая». В жизни у объекта есть поля трёх сортов: те, что копировать нельзя (id, статус, даты), те, что копировать обязательно (строки с количеством и скидкой), и те, что копировать вредно (справочники, которые и так общие).

Задача ​

Менеджер по продажам делает коммерческое предложение (КП) новому клиенту «как прошлое для Acme, только поменять количество». КП состоит из строк: товар из каталога, количество, скидка; плюс условия оплаты. У принятого КП есть номер, статус и дата.

Первая версия копировала КП через сериализацию. Копия приходила с тем же номером и статусом accepted, менеджер забывал их сбросить, и в отчёте появлялись два принятых КП с одним номером. Потом сделали «глубокую копию всего» — и каждое КП таскало за собой копию карточек товаров, которые потом разошлись с каталогом по цене.

Что урезано ​

  • Нет хранилища и генерации номеров: новый номер и сегодняшняя дата передаются параметрами.
  • Каталог — два товара, созданные в тесте.
  • Статусы — строки, без переходов.
  • Деньги — целые копейки, скидка целыми процентами.

Решение ​

Части намеренно разные: Product неизменяемый и общий, QuoteLine изменяемая и личная для каждого КП.

Quote сам знает своё устройство. Копия дублирует строки, но не товары внутри них. А cloneFor() поверх копии делает то, что сериализация забывала: новый номер, нового клиента, статус draft, сегодняшнюю дату. Условия оплаты и строки переезжают как есть.

QuoteTemplates — «сохранить как шаблон» и «новое КП из шаблона». Шаблон замораживается в момент сохранения: дальнейшие правки исходного КП его не трогают, и каждое новое КП независимо от предыдущих.

Структура ​

copied

shared

frozen copies

Quote

+id: string

+customer: string

+status: string

+createdAt: string

+lines: QuoteLine[]

+terms: string

+total() : int

+clone() : Quote

+cloneFor(customer, id, today) : Quote

QuoteLine

+product: Product

+qty: int

+discountPercent: int

+total() : int

Product

+sku: string

+name: string

+priceCents: int

QuoteTemplates

+save(name, quote)

+newQuote(name, customer, id, today) : Quote

Три сорта полей видны на схеме: Quote владеет строками (копировать), строка ссылается на товар (делить), а поля идентичности Quote сбрасываются в cloneFor().

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

Шаблон рождается из реального КП: принятое предложение для Acme загружено из базы, товары в нём из каталога, и менеджер жмёт "сохранить как шаблон" - QuoteTemplates::save() замораживает копию. Новое КП для клиента собирает обработчик "КП из шаблона": номер берётся из последовательности, дата из часов, а newQuote() отдаёт черновик со своими строками, который менеджер правит дальше.

В выводе видно, что строки у каждого КП свои: Globex урезал количество ноутбуков, Initech получил скидку на мышей, а исходное КП Acme осталось принятым и с прежней суммой. Шаблон с несуществующим именем обработчик превращает в сообщение "шаблон не найден", черновик не создаётся.

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

Реализации ​

Реестр копирует при сохранении, поэтому «сохранить как шаблон» — это снимок, а не ссылка на живое КП. Это отличие от многих CRM, где шаблон «плывёт» вместе с исходным документом и удивляет менеджеров.

ЯзыкОсобенность
PHP__clone() дублирует строки; Product — readonly, его можно делить без риска
GoСтроки — значения в срезе, slices.Clone их копирует; товар — указатель, остаётся общим
PythonЯвный replace по строкам вместо deepcopy: deepcopy размножил бы каталог
TypeScriptЯвный clone() со спредом строк; Product — readonly интерфейс

Запуск: make test T=patterns/creational/prototype/practice

Что проверяют тесты ​

Тесты одинаковы во всех языках. Копия получает новый номер, клиента, статус draft и дату, но те же условия и ту же сумму. Второй тест — главный: изменение количества в копии не трогает шаблон, а товар в строке — тот же самый объект. Третий — шаблон заморожен в момент сохранения. Четвёртый — два КП из одного шаблона не видят друг друга.

Где граница ​

  • Граф объектов с циклами и общими узлами — ручной clone() на каждом уровне становится хрупким; тогда либо deepcopy с явным списком того, что делить (__deepcopy__ с memo), либо отказ от копирования в пользу неизменяемых структур.
  • Копия нужна ради «отменить изменения» — это Снимок (Memento), а не прототип.
  • Копируется только ради нескольких других значений — хватит with…/replace без реестра.
  • Если «шаблон» должен меняться вместе с источником, прототип не подходит: это ссылка, а не копия, и это надо назвать явно.

Отличия между языками ​

  • PHP — clone поверхностный, поэтому __clone() обязателен для строк. readonly у Product делает общий доступ безопасным на уровне языка.
  • Go — глубина копии видна по типам: срез значений копируется slices.Clone, указатель делится. Это удобно читать: *Product в структуре строки сам говорит «общий».
  • Python — copy.deepcopy здесь был бы ошибкой: он скопировал бы и Product. Явный replace по строкам короче __deepcopy__ и честнее.
  • TypeScript — structuredClone потерял бы класс Quote и размножил бы товары; спред по строкам делает ровно то, что нужно.

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

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