handbookНаписать

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

Декоратор (Decorator) ​

Добавляет объекту поведение обёрткой с тем же интерфейсом: обёртки складываются в любом количестве и порядке, а исходный класс не меняется.

Проблема ​

Кофейня продаёт эспрессо, к нему добавки: молоко, сироп, второе молоко. Если каждая комбинация — подкласс (EspressoWithMilk, EspressoWithMilkAndVanilla…), классов становится столько, сколько сочетаний, а новая добавка удваивает их число. Флаги в одном классе (hasMilk, syrupFlavor) не лучше: цену считает растущий if, «двойное молоко» не выражается без счётчиков, и каждая добавка правит один и тот же класс.

Решение ​

Ввести общий интерфейс — компонент Beverage с ценой и описанием. Клиент знает только его. Конкретный компонент Espresso — то, что оборачивают.

Декораторы Milk и Syrup тоже реализуют Beverage и держат внутри другой Beverage. Каждый вызов они передают внутрь и дописывают своё: + 50 к цене, + milk к описанию. Поскольку обёртка — тоже напиток, её можно обернуть ещё раз: Syrup(Milk(Espresso)), Milk(Milk(Espresso)). Новая добавка — один новый класс, остальные не меняются.

Декоратор нужен, когда поведение добавляется слоями и сочетания заранее неизвестны. Если вариантов два-три и они не комбинируются, проще подкласс или параметр.

Структура ​

wraps

wraps

«interface»

Beverage

+price() : int

+description() : string

Espresso

+price() : int

+description() : string

Milk

-inner: Beverage

+price() : int

+description() : string

Syrup

-inner: Beverage

-flavor: string

+price() : int

+description() : string

Компонент — весь контракт, который видят клиент и декораторы. Чем он уже, тем дешевле декоратор: каждый метод интерфейса обёртка обязана либо изменить, либо передать внутрь. Два метода — две строки делегирования; двадцать методов — повод задуматься.

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

Слои собирает тот, кто знает заказ: обработчик кассы. Экран кассы присылает список кодов добавок, обработчик начинает с Espresso и оборачивает напиток по одному коду за раз: milk даёт Milk, syrup:vanilla даёт Syrup со вкусом. Порядок обёрток - порядок в заказе, а количество ничем не ограничено: два молока - просто два слоя Milk.

Незнакомый код добавки останавливает сборку этого заказа: функция выбора обёртки бросает ошибку, обработчик отклоняет заказ и переходит к следующему. Для готового напитка цена и описание - один вызов price() и description() у внешнего слоя, остальное каждый слой спрашивает у своего inner.

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

Реализации ​

Декораторы не знают, что лежит внутри: эспрессо или другая обёртка. Поэтому их можно вкладывать друг в друга в любом порядке, а конкретный компонент не знает, что его обернули. Состояние обёртки — только ссылка на внутренний объект и собственные параметры (flavor у сиропа); всё неизменяемо, одну и ту же основу можно положить в два разных напитка.

ЯзыкОсобенность
PHPИнтерфейс и final readonly классы; внутренний объект — private свойство конструктора
GoОбёртка встраивает интерфейс (type Milk struct{ Beverage }): непереопределённые методы делегируются сами
PythonProtocol вместо интерфейса, обёртки — frozen dataclass; синтаксис @decorator — это другое (см. ниже)
TypeScriptИнтерфейс и классы с private readonly параметрами конструктора; структурная типизация, implements — для проверки

Запуск: make test T=patterns/structural/decorator

Плюсы и минусы ​

Тесты одинаковы во всех языках и проверяют четыре случая: голый эспрессо стоит 200; молоко и ванильный сироп дают 290 и описание по слоям; одна обёртка дважды — двойное молоко за 300; при смене порядка обёрток цена та же, а описание меняется. Последний случай — первая подсказка: порядок слоёв виден снаружи, и в реальных задачах от него зависит уже не только текст.

  • ✅ Поведение добавляется без правки исходного класса и без взрыва подклассов.
  • ✅ Сочетания собираются во время выполнения, в том числе из конфига.
  • ✅ Каждая обёртка маленькая, одна ответственность, тестируется отдельно.
  • ❌ Много мелких объектов; отладчик показывает матрёшку, стек вызовов глубокий.
  • ❌ Порядок слоёв важен, а из кода сборки это не всегда очевидно: см. практику.
  • ❌ Обёртка скрывает тип: проверка «это эспрессо?» на обёрнутом объекте не сработает.

Когда не применять ​

  • Вариантов мало и они не сочетаются: подкласс или параметр проще.
  • Интерфейс широкий, а меняется один метод из двадцати: обёртка превратится в простыню делегирования. Лучше сузить интерфейс или вынести меняющуюся часть в стратегию.
  • Нужно заменить алгоритм целиком, а не добавить слой: это Стратегия.
  • Нужно не добавить поведение, а решить, пускать ли вызов к объекту (ленивая загрузка, права): это Заместитель. Устроены они одинаково, различаются намерением.

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

  • PHP — классический вариант: интерфейс плюс final readonly обёртки. Базовый класс-декоратор из GoF (абстрактный, с делегированием по умолчанию) здесь не нужен: методов два, и оба переопределяются. Он оправдан при широком интерфейсе.
  • Go — встраивание интерфейса в структуру делает базовый декоратор ненужным: Milk{Beverage} уже реализует всё, что не переопределено. Самый известный декоратор в Go — io.Reader: bufio.NewReader, gzip.NewReader, io.LimitReader оборачивают друг друга. Для функций — middleware func(next Handler) Handler, как в net/http.
  • Python — @decorator в языке — это функция высшего порядка над функцией или классом, а не обёртка объекта. Идея та же (вернуть вызываемое с тем же контрактом и добавленным поведением), и для поведения вокруг одного вызова синтаксис удобнее классов: это показывает практика. Для объектов с несколькими методами — классы, как здесь; Protocol проверяет совместимость без наследования.
  • TypeScript — структурная типизация: любой объект с price() и description() уже Beverage, поэтому обёртку можно написать литералом { price: () => inner.price() + 50, ... }. Синтаксис @decorator в TS (стандарт TC39) тоже про функции и классы при объявлении, а не про обёртку объекта во время выполнения.

Практика ​

  • Учебные добавки к кофе складываются как угодно: цена от порядка не зависит. В жизни декораторами оборачивают клиентов внешних систем — ретраи, кэш, журнал — и там порядок слоёв меняет поведение: кэш снаружи ретрая экономит запросы, а ретрай снаружи кэша повторяет закэшированный отказ. Практика: клиент курсов валют показывает слои поверх одного интерфейса без сети, с фейковым транспортом, ненастоящими паузами и тестом на порядок.
  • Отчёты в админке оборачивают медленный запрос к базе кэшем, замером времени, журналом и лимитом строк, а набор слоёв для каждого экрана задаёт конфиг. Слой, который меняет данные, над общим кэшем опасен: практика: слои над медленным отчётом показывает сборку стека по кодам из конфига и то, как кэш не с той стороны лимита отдаёт выгрузке обрезанный отчёт.

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

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