Перейти к основному содержимому

Паттерн "Наблюдатель" в C# — события, IObservable и утечки

Разработчику Архитектору

Базовая идея Observer описана в поведенческих паттернах. Эта глава показывает C#-практику: event, EventHandler, IObservable<T> и правила безопасной подписки.

Загрузка редактора схем…

Суть паттерна

Наблюдатель задаёт связь "один ко многим":

  • издатель публикует событие;
  • подписчики реагируют каждый по своей логике;
  • издатель не знает детали подписчиков.

Это снижает связанность и упрощает расширение.

Загрузка ArchiStyler…

Observer в C# через event

В .NET паттерн встроен в язык:

public class PriceFeed
{
public event EventHandler<PriceChangedEventArgs>? PriceChanged;

public void Update(decimal newPrice)
{
PriceChanged?.Invoke(this, new PriceChangedEventArgs(newPrice));
}
}

Подписка:

feed.PriceChanged += OnPriceChanged;

void OnPriceChanged(object? sender, PriceChangedEventArgs e)
{
logger.LogInformation("New price: {Price}", e.Price);
}

Событие позволяет извне только += и -=. Вызывать Invoke может только издатель. Это безопасная инкапсуляция наблюдателя на уровне языка.


Где появляется IObservable<T>

event удобен для большинства доменных и UI-событий. IObservable<T> и IObserver<T> полезны, когда нужен поток:

  • с композицией операторов;
  • с единым API завершения и ошибки (OnCompleted, OnError);
  • с более явным управлением подпиской через IDisposable.
public sealed class PriceObserver : IObserver<decimal>
{
public void OnNext(decimal value) => Console.WriteLine(value);
public void OnError(Exception error) => Console.WriteLine(error.Message);
public void OnCompleted() => Console.WriteLine("Completed");
}

Эта модель особенно полезна в реактивных сценариях и интеграциях с Rx.


Главная практическая проблема — отписка

Самая частая ошибка в Observer на C# — забытая отписка от долгоживущего издателя.

Симптомы:

  • рост памяти;
  • подписчик продолжает получать события после завершения сценария;
  • дублирующиеся обработчики.

Базовое правило:

feed.PriceChanged += OnPriceChanged;
try
{
// работа
}
finally
{
feed.PriceChanged -= OnPriceChanged;
}

Для IObservable<T> то же делает IDisposable из Subscribe().


Когда Observer уместен

СитуацияПочему подходит
UI и пользовательские действияЕстественная модель событий
Domain eventsОдин факт запускает несколько независимых реакций
Интеграционные уведомленияЛёгкое подключение новых обработчиков
Телеметрия и мониторингПодписчики добавляются без правок источника

Когда лучше без него

Если у вас один вызов и одна зависимость, прямой сервисный метод проще:

await billingService.UpdateInvoiceAsync(invoiceId, ct);

Паттерн Observer полезен, когда подписчиков действительно несколько и жизненный цикл подписок контролируется явно.


Чек-лист

ВопросВыбор
Нужны простые уведомления внутри процессаevent EventHandler<T>
Нужны потоковые операторы и модель OnError/OnCompletedIObservable<T>
Издатель живёт дольше подписчикаОбязательная отписка (-= или Dispose)
Один вызов без fan-outПрямой сервисный вызов
Память и подписки

События удобны, но ссылку на обработчик держит издатель. Если подписчик не отписался, GC не сможет освободить объект до завершения жизни издателя.


Итог

Observer в C# реализован нативно через event, делегаты и интерфейсы IObservable<T>/IObserver<T>. Это сильный инструмент слабой связанности при условии дисциплины подписок и отписок.


См. также

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").