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