Decorator Pattern (GoF) — Декоратор
Расширяет поведение объекта без наследования. Оборачивает один объект в другой с тем же интерфейсом, добавляя метаданные/поведение.
Проблема
Нужно добавить функциональность к классу: логирование, кэширование, профилирование, метаданные, авторизация. Наследование плодит подклассы (LoggedCachedAuthorized — комбинаторный ад). Хочется компонуемое решение, следующее Open/Closed Principle.
Где используется
- Кэширование методов без изменения исходного класса
- Логирование вызовов функций и методов
- Авторизация: проверка прав перед выполнением операции
- Мемоизация: декоратор-обёртка над функцией
- TypeScript/Angular:
@Component,@Injectable,@Log— встроенные декораторы
Решение
- Decorator оборачивает целевой объект
- Реализует тот же интерфейс
- Делегирует вызовы внутрь + добавляет своё поведение
Реализации
Классический GoF на классах
class Coffee {
cost { return 5; }
}
class MilkDecorator {
constructor(coffee) { this.coffee = coffee; }
cost { return this.coffee.cost + 2; }
}
class WhippedCreamDecorator {
constructor(coffee) { this.coffee = coffee; }
cost { return this.coffee.cost + 3; }
}
const order = new WhippedCreamDecorator(new MilkDecorator(new Coffee));
order.cost; // 5 + 2 + 3 = 10
Функциональные декораторы (JS-стиль)
function withLogging(fn, name = fn.name) {
return function(...args) {
console.log(`[${name}] call`, args);
const result = fn.apply(this, args);
console.log(`[${name}] result`, result);
return result;
};
}
function withMemo(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
function withTiming(fn, name = fn.name) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
console.log(`${name}: ${(performance.now() - start).toFixed(2)}ms`);
return result;
};
}
// Комбинирование
const fastFib = withTiming(withMemo(withLogging(fibonacci)));
Декоратор класса (репозиторий с кэшем и логом)
class UserRepository {
async findById(id) { return db.query('SELECT * FROM users WHERE id = ?', [id]); }
async save(user) { return db.query('INSERT INTO users ...', [user]); }
}
class CachedUserRepository {
constructor(repo) { this.repo = repo; this.cache = new Map(); }
async findById(id) {
if (this.cache.has(id)) return this.cache.get(id);
const user = await this.repo.findById(id);
this.cache.set(id, user);
return user;
}
async save(user) {
const result = await this.repo.save(user);
this.cache.set(user.id, user);
return result;
}
}
class LoggedUserRepository {
constructor(repo) { this.repo = repo; }
async findById(id) {
console.log(`findById(${id})`);
return this.repo.findById(id);
}
async save(user) {
console.log('save:', user);
return this.repo.save(user);
}
}
const repo = new LoggedUserRepository(new CachedUserRepository(new UserRepository));
JavaScript decorators (TC39 Stage 3)
function logged(target, ctx) {
return function (...args) {
console.log(ctx.name);
return target.apply(this, args);
};
}
class Service {
@logged
request { return 'ok'; }
}
Где используется в JS-экосистеме
- NestJS —
@Controller,@Get,@Injectable— синтаксис декораторов для метаданных - TypeORM/Prisma — декораторы для описания схем
- MobX —
@observable,@action - Express middleware — функционально похоже на декорирование (но через chain)
Подводные камни
- Декоратор-синтаксис ≠ GoF Decorator — решают похожую задачу разными средствами.
- TS-декораторы vs JS-декораторы: были разные, теперь сольются в JS-версию (TC39 stage 3).
@privateв TS ≠#field—#fieldне наследуется, TS-private наследуется как в Java.- Декоратор-цепочка сложна для дебага: трудно понять, какой декоратор вызывается.
- Потеря
thisв декораторе-обёртке безfn.apply(this, args). - Потеря
fn.lengthиfn.nameпри оборачивании — теряются метаданные функции. - Путаница с Proxy:
Proxyмощнее, но тяжелее; Decorator проще и явнее.
Главные тезисы автора
- «Декоратор расширяет поведение объектов без наследования».
- «Декоратор-синтаксис ≠ GoF-паттерн» — разные вещи, решающие похожую задачу.
- В JS GoF-декоратор реализуется через композицию: класс с полем-ссылкой на оборачиваемый объект.
- Метаданные — основная цель: куда роутить результат, как обрабатывать ошибки, в какой формат конвертировать.
- TypeScript-декораторы постепенно сольются с JS-стандартом (как было с
Disposable). - «Adapter не трогает обе абстракции; Decorator оборачивает одну» — ключевое различие.
🎓 Источники
- 🎓 Примеси, обертки, декораторы, мемоизация · 2019-09-28
- Wrapper как HOF
- Универсальный wrapper с before/after
- Потеря
fn.lengthпри оборачивании
- 🎓 Декораторы в JS, TS, GoF паттерн · 2026-02-20
- JS-декораторы и TS-декораторы сольются
- Разница между синтаксисом и GoF-паттерном
#fieldvs TS-private
- 🎓 Адаптер, Декоратор, Прокси, Фасад — В чём разница · 2026-02-09
- 🎓 GoF Patterns Обзор всех паттернов · 2025-04-29
- refactoring.guru — Decorator
- javascript.info — Декораторы и переадресация