Event Bus и паттерн Observer в C# (.NET): реализация и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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

Создадим простое приложение, где пользователь регистрируется, и после регистрации отправляется приветственное письмо.

                
    
    

Рекомендаций