Event Bus и паттерн Observer в C# (.NET): от теории к практике
В современной разработке на C# (.NET) одним из ключевых принципов является слабая связанность (loose coupling) между компонентами системы. Паттерн Observer (Наблюдатель) и его эволюция — Event Bus (Шина событий) — предоставляют элегантный механизм для организации взаимодействия между объектами без прямых зависимостей. В этой статье мы подробно разберём, как реализовать Event Bus в C# (.NET), рассмотрим его архитектуру и приведём практические примеры.
Что такое паттерн Observer?
Паттерн Observer определяет отношение «один ко многим» между объектами: когда один объект (субъект) изменяет своё состояние, все зависимые от него объекты (наблюдатели) автоматически уведомляются и обновляются. В C# этот паттерн традиционно реализуется через события (event) и делегаты.
Проблема классической реализации
Хотя встроенные события C# работают хорошо, у них есть ограничения:
- Прямая связь: издатель должен явно знать подписчиков.
- Сложность отписки: если подписчик забыл отписаться, возникает утечка памяти.
- Ограниченная гибкость: сложно организовать асинхронную обработку или фильтрацию событий.
Что такое Event Bus?
Event Bus — это централизованная шина (посредник), которая реализует паттерн Publisher-Subscriber (Издатель-Подписчик). В отличие от классического Observer, издатель и подписчик вообще не знают друг о друге. Они общаются только через шину:
- Издатель отправляет сообщение (событие) в шину.
- Шина находит всех подписчиков на этот тип события и уведомляет их.
- Подписчик регистрируется на определённый тип события и обрабатывает его.
Преимущества Event Bus
- Слабая связанность: компоненты не зависят друг от друга.
- Масштабируемость: легко добавлять новых подписчиков.
- Тестируемость: можно подменять шину моками.
- Асинхронность: можно обрабатывать события в фоновых потоках.
Реализация Event Bus в C# (.NET)
Рассмотрим простую, но функциональную реализацию. Создадим класс EventBus, который будет хранить словарь подписчиков.
public interface IEventBus
{
void Publish<TEvent>(TEvent eventData) where TEvent : class;
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
}
public class EventBus : IEventBus
{
private readonly Dictionary<Type, List<Delegate>> _subscribers = new();
private readonly object _lock = new();
public void Publish<TEvent>(TEvent eventData) where TEvent : class
{
List<Delegate> handlers;
lock (_lock)
{
if (!_subscribers.TryGetValue(typeof(TEvent), out var list))
return;
handlers = new List<Delegate>(list);
}
foreach (var handler in handlers)
{
if (handler is Action<TEvent> action)
action(eventData);
}
}
public IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class
{
lock (_lock)
{
if (!_subscribers.ContainsKey(typeof(TEvent)))
_subscribers[typeof(TEvent)] = new List<Delegate>();
_subscribers[typeof(TEvent)].Add(handler);
}
return new Unsubscriber(() =>
{
lock (_lock)
{
if (_subscribers.TryGetValue(typeof(TEvent), out var list))
list.Remove(handler);
}
});
}
private class Unsubscriber : IDisposable
{
private readonly Action _unsubscribe;
public Unsubscriber(Action unsubscribe) => _unsubscribe = unsubscribe;
public void Dispose() => _unsubscribe?.Invoke();
}
}
Ключевые моменты реализации
- Потокобезопасность: используется блокировка
lockпри доступе к словарю. - Автоматическая отписка: метод
SubscribeвозвращаетIDisposable, который при вызовеDispose()отписывает подписчика. Это предотвращает утечки памяти. - Типобезопасность: используется дженерик
TEvent.
Пример использования Event Bus
Создадим простое приложение, где пользователь регистрируется, и после регистрации отправляется приветственное письмо.
Также в библиотеке
Рекомендаций