Event Bus и паттерн Observer в TypeScript
Паттерн Observer (Наблюдатель) и его практическая реализация — Event Bus (Шина событий) — являются фундаментальными инструментами для построения слабосвязанных, масштабируемых приложений. В этой статье мы подробно разберём, как реализовать Event Bus на TypeScript, используя строгую типизацию, и рассмотрим его применение в образовательных проектах.
Что такое паттерн Observer?
Паттерн Observer определяет отношение "один ко многим" между объектами. Когда один объект (издатель, subject) изменяет своё состояние, все зависимые от него объекты (наблюдатели, observers) автоматически уведомляются и обновляются. Это позволяет избежать жёсткой связанности компонентов.
Что такое Event Bus?
Event Bus (шина событий) — это централизованный механизм, реализующий паттерн Observer. Вместо того чтобы напрямую подписывать наблюдателей на издателя, все компоненты общаются через единую шину. Компонент может:
- Подписаться (subscribe/listen) на определённое событие.
- Опубликовать (emit/publish) событие, которое будет передано всем подписчикам.
- Отписаться (unsubscribe) от события, когда это больше не нужно.
Этот подход идеально подходит для:
- Взаимодействия между модулями, которые не должны знать друг о друге.
- Обработки пользовательских действий (клики, ввод данных).
- Реагирования на изменения состояния приложения (загрузка данных, ошибки).
Реализация Event Bus на TypeScript
TypeScript добавляет мощную систему типов, что делает Event Bus безопасным и предсказуемым. Мы создадим типизированную шину, где каждое событие имеет строго определённый набор аргументов.
1. Определение типов событий
Сначала определим интерфейс для маппинга названий событий и их аргументов:
// Определяем карту событий: ключ — имя события, значение — тип аргументов
interface EventMap {
'user:login': { userId: string; timestamp: number };
'data:loaded': { items: string[] };
'error:occurred': { message: string; code: number };
// Можно добавить любые другие события
}
2. Создание класса EventBus
Теперь создадим сам класс, используя дженерики для строгой типизации:
type Listener<T> = (payload: T) => void;
class EventBus<T extends Record<string, unknown>> {
private listeners: Map<keyof T, Set<Listener<unknown>>> = new Map();
// Подписка на событие
subscribe<K extends keyof T>(event: K, listener: Listener<T[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
const currentListeners = this.listeners.get(event)!;
currentListeners.add(listener as Listener<unknown>);
// Возвращаем функцию для отписки
return () => {
currentListeners.delete(listener as Listener<unknown>);
};
}
// Публикация события
emit<K extends keyof T>(event: K, payload: T[K]): void {
const currentListeners = this.listeners.get(event);
if (currentListeners) {
currentListeners.forEach((listener) => {
listener(payload);
});
}
}
// Отписка от всех событий (полезно для очистки)
clear(): void {
this.listeners.clear();
}
}
// Создаём экземпляр шины с нашей картой событий
export const appEventBus = new EventBus<EventMap>();
3. Пример использования
Рассмотрим, как различные модули могут взаимодействовать через Event Bus:
// Модуль аутентификации
class AuthService {
login(userId: string) {
// ... логика входа ...
appEventBus.emit('user:login', {
userId,
timestamp: Date.now()
});
}
}
// Модуль UI, реагирующий на вход пользователя
class UserDashboard {
private unsubscribe: () => void;
constructor() {
// Подписываемся на событие
this.unsubscribe = appEventBus.subscribe('user:login', (payload) => {
console.log(`Пользователь ${payload.userId} вошёл в ${new Date(payload.timestamp).toLocaleString()}`);
this.renderDashboard(payload.userId);
});
}
private renderDashboard(userId: string) {
// Обновляем интерфейс
}
destroy() {
// Обязательно отписываемся при уничтожении компонента
this.unsubscribe();
}
}
// Модуль аналитики
class AnalyticsTracker {
constructor() {
appEventBus.subscribe('user:login', (payload) => {
// Отправляем