Event Bus и паттерн Observer в C# (.NET): от теории к реализации
В современной разработке на C# (.NET) часто возникает необходимость организовать взаимодействие между компонентами системы, сохраняя их независимость. Паттерн Observer и его эволюция — Event Bus (Шина событий) — являются ключевыми инструментами для реализации слабого связывания (loose coupling). В этой статье мы разберём, как работают эти механизмы, и создадим простую, но функциональную реализацию Event Bus на C#.
Паттерн Observer: классический подход
Паттерн Observer определяет отношение "один ко многим" между объектами: когда один объект (издатель) меняет своё состояние, все зависимые объекты (подписчики) автоматически уведомляются. В .NET этот паттерн встроен через события (event) и делегаты.
Недостатки классической реализации:
- Прямая зависимость подписчика от издателя (нужно хранить ссылку на объект).
- Сложность управления подписками в больших системах.
- Проблемы с потоками (многопоточность требует дополнительной синхронизации).
Event Bus: централизованный посредник
Event Bus решает эти проблемы, выступая в роли центрального посредника. Вместо того чтобы подписываться напрямую на конкретный объект, компоненты регистрируются в шине событий. Издатели публикуют события в шину, а шина рассылает их всем подписчикам. Это реализует паттерн Pub/Sub (Publish/Subscribe).
Преимущества Event Bus:
- Слабая связанность: издатели и подписчики ничего не знают друг о друге.
- Масштабируемость: легко добавлять новых подписчиков.
- Универсальность: подходит для GUI (WPF/WinForms), микросервисов и игр (Unity).
Реализация Event Bus на C# (.NET)
Мы создадим базовую реализацию Event Bus, которая поддерживает асинхронную обработку и типизированные события. Для этого используем интерфейс IEvent и обобщённый класс EventBus.
Шаг 1: Определяем базовые типы.
// Базовый интерфейс для всех событий
public interface IEvent { }
// Пример события - сообщение пользователя
public class UserCreatedEvent : IEvent
{
public int UserId { get; }
public string Email { get; }
public UserCreatedEvent(int userId, string email)
{
UserId = userId;
Email = email;
}
}
Шаг 2: Создаём шину событий.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
public class EventBus
{
// Хранилище подписчиков: ключ - тип события, значение - список обработчиков
private readonly ConcurrentDictionary<Type, ConcurrentBag<object>> _subscribers = new();
// Подписка на событие
public void Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : IEvent
{
var eventType = typeof(TEvent);
var handlers = _subscribers.GetOrAdd(eventType, _ => new ConcurrentBag<object>());
handlers.Add(handler);
}
// Отписка (упрощённая - удаляем все обработчики данного типа)
public void Unsubscribe<TEvent>() where TEvent : IEvent
{
_subscribers.TryRemove(typeof(TEvent), out _);
}
// Публикация события (асинхронная)
public async Task PublishAsync<TEvent>(TEvent eventData) where TEvent : IEvent
{
if (!_subscribers.TryGetValue(typeof(TEvent), out var handlers))
return;
foreach (var handler in handlers)
{
if (handler is Func<TEvent, Task> typedHandler)
{
await typedHandler(eventData);
}
}
}
}
Шаг 3: Использование в приложении.
public class EmailService
{
public async Task OnUserCreated(UserCreatedEvent evt)
{
Console.WriteLine($"Отправка приветственного письма на {evt.Email}");
await Task.Delay(100); // Имитация отправки
}
}
class Program
{
static async Task Main(string[] args)
{
var bus = new EventBus();
var emailService = new EmailService();
// Подписка
bus.Subscribe<UserCreatedEvent>(emailService.OnUserCreated);
// Публикация события
var userEvent = new UserCreatedEvent(1, "user@example.com");
await bus.PublishAsync(userEvent);
Console.WriteLine("Событие обработано.");
}
}
Расширенная реализация: синглтон и синхронный режим
В реальных проектах часто используют <