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 оборачивает одну» — ключевое различие.

🎓 Источники

См. также