Перейти к содержанию

Observer (Наблюдатель)

Категория: паттерн поведения.

Проблема

Одному событию в системе (изменилась цена акции, обновились данные, что-то пошло не так) должны отреагировать несколько независимых частей системы - логирование, UI, отправка алертов, кеш. При этом источник события (субъект) не должен: - знать заранее, сколько подписчиков и какие именно; - быть жёстко связанным с конкретными классами подписчиков (иначе при добавлении нового подписчика пришлось бы менять код субъекта).

Решение

  1. Субъект хранит список подписчиков (наблюдателей) и предоставляет способ подписаться/отписаться.
  2. При изменении состояния субъект уведомляет всех подписчиков, вызывая у каждого условный метод Update/обработчик события.
  3. Подписчики реализуют этот метод по-своему, реагируя на уведомление независимо друг от друга.
  4. Субъект работает с подписчиками через общую абстракцию (интерфейс или делегат) и не знает о конкретных типах подписчиков.

Варианты реализации в .NET

  • События (event) и делегаты - идиоматичный для C# способ реализовать Observer: event EventHandler<T> в субъекте, методы-обработчики у подписчиков, +=/-= для подписки/отписки. Это встроенный в язык вариант паттерна, показанный в примере выше.
  • Методы обратного вызова (callbacks) - более простой, "ручной" вариант без выделенного механизма event, например поле типа Action<T>.
  • Строго типизированный интерфейс наблюдателя (IObserver/собственный интерфейс с методом Notify) - когда наблюдателей нужно хранить и явно перебирать как объекты (например, чтобы вызывать доп. методы, а не только обработку уведомления).
  • IObservable<T>/IObserver<T> и Reactive Extensions (Rx) - развитая версия паттерна с поддержкой завершения потока событий (OnCompleted), ошибок (OnError) и богатым набором операторов для комбинирования потоков событий.

Структура

  • Subject - хранит состояние, список наблюдателей, методы подписки/отписки и уведомления.
  • Observer - интерфейс/делегат с методом реакции на уведомление.
  • ConcreteObserver - конкретная реализация реакции на событие.

Когда применять

  • Изменение состояния одного объекта должно приводить к обновлению произвольного числа других объектов, заранее не известных субъекту.
  • Нужно избежать жёсткой связи между источником события и его обработчиками, чтобы подписчиков можно было добавлять/убирать без изменения субъекта.

Плюсы

  • Слабая связанность: субъект и наблюдатели знают только про общий контракт уведомления.
  • Подписчиков можно добавлять и убирать в рантайме, не трогая код субъекта.
  • Хорошо ложится на событийно-ориентированную архитектуру (UI, доменные события).

Минусы

  • Порядок вызова обработчиков обычно не гарантирован и не должен быть значимым - если он важен, Observer не лучший инструмент.
  • Утечки памяти: если подписчик не отписывается от event долгоживущего субъекта, субъект держит на него ссылку и не даёт собрать сборщику мусора (частая проблема в .NET, особенно в UI-приложениях).
  • Цепочки уведомлений (наблюдатель сам является субъектом для других наблюдателей) могут привести к сложно отслеживаемым каскадным обновлениям.

Пример в .NET Framework / BCL

  • event/EventHandler - механизм событий встроен в CLR как часть языка именно ради этого паттерна.
  • INotifyPropertyChanged/INotifyCollectionChanged - основа биндинга данных в WPF, MAUI, Blazor.
  • IObservable<T>/IObserver<T> из System и библиотека Reactive Extensions (Rx.NET).
  • FileSystemWatcher - уведомляет подписчиков об изменениях в файловой системе через события.

Пример реализации на C#

Observer.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Behavioral.Observer
{
    // Аргументы события - что именно изменилось
    public sealed class PriceChangedEventArgs : EventArgs
    {
        public decimal OldPrice { get; }
        public decimal NewPrice { get; }

        public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
        {
            OldPrice = oldPrice;
            NewPrice = newPrice;
        }
    }

    // Субъект (издатель) - хранит состояние и уведомляет подписчиков об изменениях
    // через стандартный для .NET механизм событий (это и есть Observer, встроенный в язык).
    public sealed class Stock
    {
        public string Symbol { get; }
        private decimal _price;

        public event EventHandler<PriceChangedEventArgs>? PriceChanged;

        public Stock(string symbol, decimal initialPrice)
        {
            Symbol = symbol;
            _price = initialPrice;
        }

        public void UpdatePrice(decimal newPrice)
        {
            if (newPrice == _price) return;

            var args = new PriceChangedEventArgs(_price, newPrice);
            _price = newPrice;

            // Уведомляем всех подписчиков. Stock ничего не знает о том,
            // кто именно подписан и что они будут делать с этой информацией.
            PriceChanged?.Invoke(this, args);
        }
    }

    // Наблюдатели - независимые друг от друга, каждый реагирует по-своему
    public sealed class PriceLogger
    {
        public void Subscribe(Stock stock) => stock.PriceChanged += OnPriceChanged;

        private void OnPriceChanged(object? sender, PriceChangedEventArgs e)
        {
            var stock = (Stock)sender!;
            Console.WriteLine($"[Лог] {stock.Symbol}: {e.OldPrice} -> {e.NewPrice}");
        }
    }

    public sealed class PriceAlert
    {
        private readonly decimal _threshold;

        public PriceAlert(decimal threshold) => _threshold = threshold;

        public void Subscribe(Stock stock) => stock.PriceChanged += OnPriceChanged;

        private void OnPriceChanged(object? sender, PriceChangedEventArgs e)
        {
            if (e.NewPrice >= _threshold)
            {
                Console.WriteLine($"[Алерт] Цена достигла порога {_threshold}!");
            }
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var stock = new Stock("ACME", 100m);

            var logger = new PriceLogger();
            var alert = new PriceAlert(threshold: 120m);

            logger.Subscribe(stock);
            alert.Subscribe(stock);

            stock.UpdatePrice(110m);
            stock.UpdatePrice(125m); // сработает и лог, и алерт
        }
    }
}

Открыть Observer.cs отдельно Скачать Observer.cs