Паттерн "Наблюдатель" в C# — события, IObservable и утечки
Базовая идея Observer описана в поведенческих паттернах. Эта глава показывает C#-практику: event, EventHandler, IObservable<T> и правила безопасной подписки.
Суть паттерна
Наблюдатель задаёт связь "один ко многим":
- издатель публикует событие;
- подписчики реагируют каждый по своей логике;
- издатель не знает детали подписчиков.
Это снижает связанность и упрощает расширение.
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/OnCompleted | IObservable<T> |
| Издатель живёт дольше подписчика | Обязательная отписка (-= или Dispose) |
| Один вызов без fan-out | Прямой сервисный вызов |
События удобны, но ссылку на обработчик держит издатель. Если подписчик не отписался, GC не сможет освободить объект до завершения жизни издателя.
Итог
Observer в C# реализован нативно через event, делегаты и интерфейсы IObservable<T>/IObserver<T>. Это сильный инструмент слабой связанности при условии дисциплины подписок и отписок.
См. также
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Паттерн — это повторяющийся шаблон, узор или схема. Паттерны встречаются повсюду — в природе, архитектуре, поведении людей и, конечно, в программировании. Порождающие паттерны проектирования — это группа шаблонов, направленных на решение задач, связанных с созданием объектов. Структурные паттерны — это группа шаблонов проектирования, решающих задачи организации классов и объектов таким образом, чтобы обеспечить гибкую архитектуру программного обеспечения. Поведенческие паттерны — это группа шаблонов проектирования, которые определяют способы взаимодействия объектов и распределения ответственности между ними. Архитектурные паттерны — это проверенные решения для организации структуры программного обеспечения. Интеграция систем — одна из центральных задач в современной разработке программного обеспечения. Паттерны доменного моделирования представляют собой проверенные решения для организации бизнес-логики в программных системах. Паттерн Strategy в C# — классическая реализация через интерфейс, замена на Func и Action, DI и критерии выбора без лишних абстракций. Паттерн Iterator в C# — ручной IEnumerator, генерация итератора компилятором через yield return, ленивость, LINQ и случаи, когда класс писать всё же нужно. Abstract Factory в C# и .NET — классическая схема через интерфейсы, замена через DI-контейнер, фабричный делегат и keyed services в .NET 8. Паттерн Command в C# — классическая схема, делегаты, MediatR, очередь задач, undo и критерии выбора между объектом команды и простым вызовом сервиса.Обзор паттернов проектирования
Порождающие паттерны
Структурные паттерны
Поведенческие паттерны
Архитектурные паттерны
Паттерны интеграции внешних систем
Паттерны проектирования доменных моделей
Стратегия в C#
Итератор в C#
Фабрика в C#
Команда в C#