Рефакторинг кода, работающего со STOMP.js, обычно связан с несколькими типовыми проблемами:
На ранних этапах разработки код со STOMP.js часто выглядит компактным и понятным, однако по мере роста количества каналов, подписок и обработчиков проект быстро становится трудно поддерживаемым.
Типичный пример «разрастающегося» кода:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
});
client.onConn ect = () => {
client.subscribe('/topic/orders', (message) => {
const data = JSON.parse(message.body);
updateOrders(data);
});
client.subscribe('/topic/users', (message) => {
const data = JSON.parse(message.body);
updateUsers(data);
});
client.subscribe('/topic/statistics', (message) => {
const data = JSON.parse(message.body);
updateStatistics(data);
});
};
client.activate();
Проблемы такого подхода:
Одно из главных направлений рефакторинга — изоляция транспортного слоя.
Вместо прямой работы со STOMP-клиентом внутри компонентов создаётся отдельный сервис.
function initChat() {
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
});
client.onConn ect = () => {
client.subscribe('/topic/chat', (message) => {
renderMessage(JSON.parse(message.body));
});
document
.querySelector('#send')
.addEventListener('click', () => {
client.publish({
destination: '/app/chat',
body: JSON.stringify({
text: 'Hello'
})
});
});
};
client.activate();
}
Недостатки:
После рефакторинга появляется отдельный сервис.
import { Client } from '@stomp/stompjs';
class StompService {
constructor() {
this.client = new Client({
brokerURL: 'ws://localhost:15674/ws',
});
}
connect(onConnect) {
this.client.onConn ect = onConnect;
this.client.activate();
}
subscribe(destination, callback) {
return this.client.subscribe(destination, (message) => {
callback(JSON.parse(message.body));
});
}
publish(destination, data) {
this.client.publish({
destination,
body: JSON.stringify(data),
});
}
}
export default new StompService();
Теперь UI работает только с абстракцией:
import stompService from './services/StompService';
stompService.connect(() => {
stompService.subscribe('/topic/chat', (message) => {
renderMessage(message);
});
});
Преимущества:
Во многих проектах reconnect-логика размазывается по компонентам.
Плохой пример:
client.onWebSocketCl ose = () => {
setTimeout(() => {
client.activate();
}, 5000);
};
При большом количестве модулей появляется:
После рефакторинга reconnect становится частью сервиса.
class StompService {
constructor() {
this.client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000,
});
this.client.onConn ect = () => {
console.log('Connected');
};
this.client.onStompEr ror = (frame) => {
console.error(frame);
};
}
}
STOMP.js уже содержит встроенный reconnect-механизм, поэтому ручная реализация часто оказывается лишней.
Распространённая проблема — повторение одинаковой логики:
client.subscribe('/topic/orders', (message) => {
const data = JSON.parse(message.body);
handleOrders(data);
});
client.subscribe('/topic/users', (message) => {
const data = JSON.parse(message.body);
handleUsers(data);
});
Повторяются:
После рефакторинга создаётся общий слой.
subscribe(destination, handler) {
return this.client.subscribe(destination, (message) => {
try {
const data = JSON.parse(message.body);
handler(data);
} catch (error) {
console.error('Parse error', error);
}
});
}
Теперь вся сериализация централизована.
Антипаттерн:
client.subscribe('/topic/orders', (message) => {
const data = JSON.parse(message.body);
if (data.status === 'NEW') {
showNotification(data);
}
if (data.total > 100000) {
sendAudit(data);
}
if (data.user.vip) {
updateVipDashboard(data);
}
});
Такой обработчик:
После рефакторинга:
client.subscribe('/topic/orders', (message) => {
const order = JSON.parse(message.body);
processOrder(order);
});
Бизнес-логика переносится:
function processOrder(order) {
handleNotifications(order);
handleAudit(order);
handleVip(order);
}
Теперь транспорт отвечает только за доставку сообщения.
Большие проекты со временем превращаются в «лес callback-функций».
Пример:
client.onConn ect = () => {
loadSettings(() => {
loadPermissions(() => {
client.subscribe('/topic/data', (message) => {
processData(message, () => {
updateUI();
});
});
});
});
};
Такой код:
Хотя STOMP.js остаётся event-driven библиотекой, часть инфраструктурного кода можно упростить.
async function initialize() {
await loadSettings();
await loadPermissions();
connectSocket();
}
Логика становится линейной и предсказуемой.
Без рефакторинга подписки создаются хаотично:
const sub1 = client.subscribe(...);
const sub2 = client.subscribe(...);
const sub3 = client.subscribe(...);
Проблемы:
Хорошая практика — хранение подписок в registry.
class SubscriptionRegistry {
constructor() {
this.subscriptions = new Map();
}
add(name, subscription) {
this.subscriptions.set(name, subscription);
}
remove(name) {
const subscription = this.subscriptions.get(name);
if (subscription) {
subscription.unsubscribe();
}
this.subscriptions.delete(name);
}
clear() {
this.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.subscriptions.clear();
}
}
Преимущества:
Во многих системах destination-строки разбросаны по проекту:
client.subscribe('/topic/orders', ...);
client.subscribe('/topic/users', ...);
client.subscribe('/topic/chat', ...);
Проблемы:
export const CHANNELS = {
ORDERS: '/topic/orders',
USERS: '/topic/users',
CHAT: '/topic/chat',
};
Использование:
client.subscribe(CHANNELS.ORDERS, handler);
Теперь изменение маршрутов происходит централизованно.
В больших проектах сообщения начинают иметь сложную структуру.
Без типизации:
const data = JSON.parse(message.body);
console.log(data.user.name);
Ошибка структуры обнаруживается только во время выполнения.
Рефакторинг часто сопровождается миграцией на TypeScript.
interface OrderMessage {
id: number;
status: string;
total: number;
}
Подписка:
subscribe<OrderMessage>(
CHANNELS.ORDERS,
(order) => {
console.log(order.status);
}
);
Generic-подход:
subscribe<T>(destination: string, handler: (data: T) => void) {
return this.client.subscribe(destination, (message) => {
const data: T = JSON.parse(message.body);
handler(data);
});
}
Преимущества:
Плохой пример:
console.log(message);
console.log(error);
console.log('Connected');
В крупных приложениях такое логирование становится бесполезным.
class Logger {
info(message, payload = null) {
console.info(message, payload);
}
error(message, payload = null) {
console.error(message, payload);
}
}
Использование:
logger.info('STOMP connected');
logger.error('Subscription error', error);
Теперь возможно:
Антипаттерн:
client.subscribe('/topic/orders', (message) => {
const data = JSON.parse(message.body);
process(data);
});
Любая ошибка ломает callback.
client.subscribe('/topic/orders', (message) => {
try {
const data = JSON.parse(message.body);
process(data);
} catch (error) {
logger.error('Order processing error', error);
}
});
После рефакторинга ошибки становятся локализованными.
Со временем сервис начинает разрастаться:
class StompService {
connect() {}
reconnect() {}
subscribe() {}
unsubscribe() {}
log() {}
retry() {}
validate() {}
cache() {}
metrics() {}
}
Появляется «god object».
После рефакторинга:
TransportLayer
SubscriptionManager
ReconnectManager
MessageSerializer
Logger
MetricsService
Каждый модуль отвечает только за одну задачу.
Плохой вариант:
client.publish({
destination: '/app/orders',
body: JSON.stringify(order),
});
Подобный код повторяется десятки раз.
class OrderApi {
create(order) {
stomp.publish('/app/orders/create', order);
}
cancel(orderId) {
stomp.publish('/app/orders/cancel', {
orderId,
});
}
}
Преимущества:
Антипаттерн:
if (client.connected) {
sendMessage();
}
Проблема в том, что состояние может измениться между проверкой и отправкой.
class ConnectionState {
constructor() {
this.connected = false;
}
setConnected(value) {
this.connected = value;
}
isConnected() {
return this.connected;
}
}
Интеграция:
client.onConn ect = () => {
state.setConnected(true);
};
client.onDisconn ect = () => {
state.setConnected(false);
};
Плохой пример:
new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
Конфигурация дублируется в нескольких файлах.
export const STOMP_CONFIG = {
brokerURL: process.env.STOMP_URL,
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
};
Использование:
const client = new Client(STOMP_CONFIG);
Некоторые проекты реализуют собственные heartbeat-механизмы поверх встроенных возможностей STOMP.js.
Плохой пример:
setInterval(() => {
client.publish({
destination: '/app/ping',
body: 'ping',
});
}, 5000);
STOMP.js уже поддерживает heartbeat:
const client = new Client({
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
Удаление лишнего кода уменьшает сложность системы.
Крупные приложения часто начинают зависеть от прямых callback-вызовов.
client.subscribe('/topic/orders', (message) => {
updateOrders(message);
updateStatistics(message);
updateDashboard(message);
});
Такой код создаёт жёсткую связанность.
После рефакторинга:
eventBus.emit('order.created', order);
Компоненты подписываются независимо:
eventBus.on('order.created', updateOrders);
eventBus.on('order.created', updateStatistics);
eventBus.on('order.created', updateDashboard);
STOMP превращается в транспортный слой, а не центральный механизм бизнес-логики.
Плохой пример:
client.subscribe('/topic/metrics', (message) => {
render(JSON.parse(message.body));
});
Если сообщения приходят сотни раз в секунду, UI начинает деградировать.
После рефакторинга:
const queue = [];
client.subscribe('/topic/metrics', (message) => {
queue.push(JSON.parse(message.body));
});
setInterval(() => {
if (queue.length > 0) {
renderBatch(queue.splice(0));
}
}, 100);
Преимущества:
Неправильный подход:
import { Client } from '@stomp/stompjs';
const client = new Client(...);
Код жёстко связан с библиотекой.
После рефакторинга:
class StompService {
constructor(client) {
this.client = client;
}
}
Теперь возможно тестирование через mock-объекты:
const fakeClient = {
subscribe: jest.fn(),
publish: jest.fn(),
};
const service = new StompService(fakeClient);
Изоляция зависимости делает архитектуру устойчивой к изменениям.