Паттерн «декоратор» используется для динамического расширения поведения объекта без изменения его исходного кода. В контексте работы с STOMP.js он особенно полезен для добавления логирования, повторных попыток подключения, обработки ошибок, трассировки сообщений и модификации payload без вмешательства в базовую реализацию клиента.
Основная идея заключается в том, что каждый слой декоратора оборачивает оригинальный объект 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.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-цепочек, декоратор:
Однако при глубокой вложенности декораторов появляется риск усложнения трассировки вызовов и отладки.
При использовании декоратора в STOMP-клиенте проявляются несколько архитектурных ограничений:
Особенно важно сохранять сигнатуры методов publish и
subscribe, так как любые изменения приводят к нарушению
цепочки декораторов.
В современных реализациях 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 остаётся неизменным ядром, а декораторы формируют адаптированную под конкретное приложение оболочку поведения.