Event-Driven архитектура на C#: Как управлять событиями как профи

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

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

Начать курс

Почему ваш код просыпается только по звонку?

Представьте, что вы — диспетчер в огромном аэропорту. Самолеты прибывают, пассажиры спешат, грузовики с топливом маневрируют. Если бы вы лично ходили к каждому пилоту и спрашивали: «Ну что, приземлился? А теперь? А сейчас?» — вы бы сошли с ума. Вместо этого вы говорите: «Когда приземлитесь — позвоните». И система работает идеально.

Это и есть event-driven архитектура на C#. Вместо того чтобы постоянно опрашивать объекты (pull), мы подписываемся на события (push). Код становится реактивным, масштабируемым и, что самое важное, — живым. В этой статье мы разберем, как строить такие системы на C# с нуля, используя встроенные механизмы языка и продвинутые паттерны.

Что такое event-driven архитектура? Не скучная теория

Event-driven (событийно-ориентированная) архитектура — это подход, при котором поток выполнения программы определяется событиями: кликами, сообщениями из очередей, изменениями данных или внешними сигналами. В C# эта концепция встроена прямо в язык через делегаты (delegates) и события (events).

Главный принцип — слабая связанность (loose coupling). Отправитель события (издатель) ничего не знает о получателях (подписчиках). Он просто кричит: «Случилось что-то важное!» — а кто и как на это отреагирует, решается динамически.

Почему это круто для реальных проектов?

  • Масштабируемость: Добавить нового подписчика — как подписать его на рассылку. Код издателя не меняется.
  • Тестируемость: Вы можете тестировать подписчиков изолированно, подсовывая им фейковые события.
  • Реактивность: Система сама реагирует на изменения, не тратя ресурсы на пустые опросы.

Базовый механизм: Делегаты и события в C#

Прежде чем строить архитектуру, давайте разберемся с фундаментом. В C# событие — это просто специализированный делегат с модификатором event. Делегат — это тип, который хранит ссылку на метод (или несколько методов).

Пишем первый event-driven код

Представьте, что мы пишем систему умного дома. Датчик движения посылает сигнал, когда кто-то входит.

public class MotionSensor
{
    // 1. Объявляем делегат (тип события)
    public delegate void MotionDetectedHandler(object sender, MotionEventArgs e);
    
    // 2. Объявляем событие
    public event MotionDetectedHandler OnMotionDetected;

    public void Detect(string location)
    {
        Console.WriteLine($"[Сенсор] Движение в {location}!");
        
        // 3. Вызываем событие (только если есть подписчики)
        OnMotionDetected?.Invoke(this, new MotionEventArgs(location));
    }
}

public class MotionEventArgs : EventArgs
{
    public string Location { get; }
    public DateTime Timestamp { get; } = DateTime.UtcNow;

    public MotionEventArgs(string location)
    {
        Location = location;
    }
}

// Подписчик — лампа
public class SmartLight
{
    public void OnMotion(object sender, MotionEventArgs e)
    {
        if (e.Location == "Гостиная")
            Console.WriteLine($"[Лампа] Включаю свет в {e.Location}!");
        else
            Console.WriteLine($"[Лампа] Игнорирую {e.Location}.");
    }
}

// Использование
var sensor = new MotionSensor();
var light = new SmartLight();

// Подписываем лампу на событие сенсора
sensor.OnMotionDetected += light.OnMotion;

sensor.Detect("Гостиная"); // Лампа сработает
sensor.Detect("Кухня");    // "Игнорирую Кухня."

Важно: Использование ?.Invoke — это потокобезопасный способ вызова события. Без проверки на null вы рискуете получить NullReferenceException, если подписчиков нет.

Продвинутый уровень: EventBus и слабая связанность

Прямые подписки — это хорошо для маленьких проектов. Но когда у вас 50 классов, каждый из которых подписывается на события других, код превращается в паутину. Тут на помощь приходит EventBus (или Event Aggregator).

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

Реализация простого EventBus на C#

public interface IEventBus
{
    void Publish<T>(T @event) where T : class;
    void Subscribe<T>(Action<T> handler) where T : class;
    void Unsubscribe<T>(Action<T> handler) where T : class;
}

public class EventBus : IEventBus
{
    private readonly ConcurrentDictionary<Type, List<object>> _subscribers = new();

    public void Publish<T>(T @event) where T : class
    {
        if (_subscribers.TryGetValue(typeof(T), out var handlers))
        {
            foreach (var handler in handlers.Cast<Action<T>>())
            {
                handler.Invoke(@event);
            }
        }
    }

    public void Subscribe<T>(Action<T> handler) where T : class
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type))
            _subscribers[type] = new List<object>();

        _subscribers[type].Add(handler);
    }

    public void Unsubscribe<T>(Action<T> handler) where T : class
    {
        var type = typeof(T);
        if (_subscribers.TryGetValue(type, out var handlers))
        {
            handlers.Remove(handler);
        }
    }
}

// Пример использования
public class OrderPlacedEvent
{
    public int OrderId { get; set; }
    public string CustomerEmail { get; set; }
}

public class EmailService
{
    public void SendConfirmation(OrderPlacedEvent e)
    {
        Console.WriteLine($"[Email] Отправляем подтверждение заказа {e.OrderId} на {e.CustomerEmail}");
    }
}

// В Program.cs
var bus = new EventBus();
var emailService = new EmailService();

bus.Subscribe<OrderPlacedEvent>(emailService.SendConfirmation);

bus.Publish(new OrderPlacedEvent { OrderId = 123, CustomerEmail = "user@example.com" });

Почему это круто? Теперь вы можете добавлять новые сервисы (SmsService, AnalyticsService) простой подпиской — без изменения существующего кода. Это и есть событийно-ориентированное программирование в действии.

Паттерн Observer: классика жанра

Если вы работаете с WinForms, WPF или Unity, вы уже используете Observer pattern. Это прародитель event-driven подхода. В C# он реализуется через интерфейсы IObservable<T> и IObserver<T>.

Пример с IObservable

public class WeatherStation : IObservable<WeatherData>
{
    private readonly List<IObserver<WeatherData>> _observers = new();

    public IDisposable Subscribe(IObserver<WeatherData> observer)
    {
        _observers                

Блоги

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