Декоратор для расширения функциональности

Паттерн «декоратор» используется для динамического расширения поведения объекта без изменения его исходного кода. В контексте работы с STOMP.js он особенно полезен для добавления логирования, повторных попыток подключения, обработки ошибок, трассировки сообщений и модификации payload без вмешательства в базовую реализацию клиента.

Основная идея заключается в том, что каждый слой декоратора оборачивает оригинальный объект STOMP-клиента и перехватывает его методы, расширяя или изменяя их поведение.


Базовая модель STOMP-клиента и точка расширения

STOMP-клиент в STOMP.js обычно создаётся через Client, который предоставляет методы:

  • activate()
  • deactivate()
  • publish()
  • subscribe()
  • обработчики событий (onConnect, onDisconnect, onStompError)

Именно эти точки становятся ключевыми для декорирования.

import { Client } from '@stomp/stompjs';

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
});

Декоратор в данном случае не заменяет клиента, а оборачивает его:

class StompClientDecorator {
  constructor(client) {
    this.client = client;
  }

  activate() {
    return this.client.activate();
  }

  deactivate() {
    return this.client.deactivate();
  }

  publish(frame) {
    return this.client.publish(frame);
  }

  subscribe(destination, callback) {
    return this.client.subscribe(destination, callback);
  }
}

Декоратор логирования сообщений

Наиболее частый сценарий — логирование входящих и исходящих сообщений.

Декоратор перехватывает publish и subscribe, добавляя дополнительную поведенческую прослойку.

class LoggingStompDecorator extends StompClientDecorator {
  publish(frame) {
    console.log('[STOMP OUT]', frame.destination, frame.body);
    return super.publish(frame);
  }

  subscribe(destination, callback) {
    console.log('[STOMP SUBSCRIBE]', destination);

    const wrappedCallback = (message) => {
      console.log('[STOMP IN]', message.destination, message.body);
      callback(message);
    };

    return super.subscribe(destination, wrappedCallback);
  }
}

Логирование реализуется без модификации оригинального клиента, что сохраняет совместимость с обновлениями библиотеки.


Декоратор повторных попыток публикации

В распределённых системах WebSocket соединение может быть нестабильным. Декоратор позволяет добавить retry-механику.

class RetryPublishDecorator extends StompClientDecorator {
  constructor(client, options = {}) {
    super(client);
    this.maxRetries = options.maxRetries || 3;
    this.delay = options.delay || 1000;
  }

  async publish(frame) {
    let attempt = 0;

    while (attempt < this.maxRetries) {
      try {
        return super.publish(frame);
      } catch (e) {
        attempt++;

        if (attempt >= this.maxRetries) {
          throw e;
        }

        await new Promise(r => setTimeout(r, this.delay));
      }
    }
  }
}

Такой слой полезен при временных разрывах соединения или перегрузке брокера.


Декоратор трансформации сообщений

Часто требуется изменять формат сообщений перед отправкой или после получения.

Пример: автоматическое добавление метаданных.

class MetadataDecorator extends StompClientDecorator {
  publish(frame) {
    const enrichedFrame = {
      ...frame,
      body: JSON.stringify({
        payload: frame.body,
        timestamp: Date.now(),
        source: 'web-client'
      })
    };

    return super.publish(enrichedFrame);
  }
}

Или трансформация входящих сообщений:

subscribe(destination, callback) {
  return super.subscribe(destination, (message) => {
    try {
      message.parsedBody = JSON.parse(message.body);
    } catch {
      message.parsedBody = null;
    }

    callback(message);
  });
}

Декоратор контроля соединения

STOMP-клиент часто требует контроля состояния соединения: автоматическое переподключение, буферизация сообщений при offline-состоянии.

class ConnectionAwareDecorator extends StompClientDecorator {
  constructor(client) {
    super(client);
    this.queue = [];
    this.connected = false;

    this.client.onConn ect = () => {
      this.connected = true;
      this.flushQueue();
    };

    this.client.onDisconn ect = () => {
      this.connected = false;
    };
  }

  publish(frame) {
    if (!this.connected) {
      this.queue.push(frame);
      return;
    }

    return super.publish(frame);
  }

  flushQueue() {
    while (this.queue.length > 0) {
      const frame = this.queue.shift();
      super.publish(frame);
    }
  }
}

Такой подход превращает STOMP-клиент в устойчивый к нестабильным сетям слой.


Комбинирование декораторов

Основное преимущество паттерна проявляется при композиции нескольких декораторов.

const baseClient = new Client({ brokerURL: 'ws://localhost:8080/ws' });

let client = new StompClientDecorator(baseClient);
client = new LoggingStompDecorator(client);
client = new ConnectionAwareDecorator(client);
client = new MetadataDecorator(client);

Каждый слой добавляет свою ответственность:

  • логирование
  • управление соединением
  • трансформация сообщений

Порядок оборачивания становится частью архитектуры и влияет на поведение системы.


Перехват событий STOMP-клиента

STOMP.js активно использует события (onConnect, onStompError, onWebSocketClose). Декоратор позволяет централизовать обработку.

class EventDecorator extends StompClientDecorator {
  constructor(client, handlers = {}) {
    super(client);

    this.client.onConn ect = handlers.onConnect;
    this.client.onStompEr ror = handlers.onError;
    this.client.onWebSocketCl ose = handlers.onClose;
  }
}

Дополнительно можно расширять события:

this.client.onConn ect = (frame) => {
  console.log('Connected:', frame.headers);
  handlers.onConnect?.(frame);
};

Декоратор как альтернатива middleware-архитектуре

В отличие от middleware-цепочек, декоратор:

  • работает на уровне объекта, а не потока вызовов
  • не требует централизованного диспетчера
  • проще комбинируется с OOP-кодом
  • не зависит от внутренней реализации STOMP.js

Однако при глубокой вложенности декораторов появляется риск усложнения трассировки вызовов и отладки.


Ограничения применения

При использовании декоратора в STOMP-клиенте проявляются несколько архитектурных ограничений:

  • невозможность полного контроля над внутренним состоянием клиента
  • риск нарушения контракта методов при неправильном проксировании
  • необходимость синхронного поддержания API при обновлении STOMP.js
  • потенциальное дублирование логики при множественных слоях

Особенно важно сохранять сигнатуры методов publish и subscribe, так как любые изменения приводят к нарушению цепочки декораторов.


Декоратор на основе Proxy

В современных реализациях JavaScript возможно использовать Proxy вместо ручного оборачивания методов.

function createStompProxy(client) {
  return new Proxy(client, {
    get(target, prop) {
      if (prop === 'publish') {
        return (frame) => {
          console.log('[PROXY OUT]', frame);
          return target.publish(frame);
        };
      }

      if (prop === 'subscribe') {
        return (destination, cb) => {
          return target.subscribe(destination, (msg) => {
            console.log('[PROXY IN]', msg.body);
            cb(msg);
          });
        };
      }

      return target[prop];
    }
  });
}

Proxy снижает количество шаблонного кода и делает декоратор более гибким, но усложняет явную типизацию и отслеживание поведения.


Декоратор как слой инфраструктурной логики

В архитектуре WebSocket-приложений STOMP-клиент часто становится точкой входа в систему обмена сообщениями. Декоратор в этом контексте выполняет роль инфраструктурного слоя:

  • наблюдение за трафиком
  • обеспечение устойчивости
  • нормализация сообщений
  • интеграция с аналитикой
  • управление политиками доставки

STOMP.js остаётся неизменным ядром, а декораторы формируют адаптированную под конкретное приложение оболочку поведения.