JavaRush /Курсы /C# SELF /Паттерн "Наблюдатель" ( Obs...

Паттерн "Наблюдатель" ( Observer)

C# SELF
53 уровень , 3 лекция
Открыта

1. Введение

Представьте, что у вас есть объект, который выполняет какие-то действия — например, кнопка или наш Worker. При этом существует множество других объектов, которые должны реагировать на эти действия. Если в классе Worker жёстко прописывать всех возможных «слушателей», то поддержка такого кода превратится в настоящий кошмар: любые изменения в списке подписчиков потребуют правок внутри самого Worker.

Это нарушает принцип открытости-закрытости (OCP) и считается плохой архитектурной практикой.

Паттерн Observer: общая идея

Паттерн "Наблюдатель" (Observer) решает эту проблему. Он позволяет объекту-издателю оповещать любое количество заинтересованных объектов-слушателей о произошедших изменениях, не зная ничего о том, кто эти слушатели и что они делают. Издатель просто "бросает клич", а желающие реагируют, как хотят.

Аналогия: подписка на рассылку новостей. Редакция или канал (издатель) отправляет новое письмо, а все подписчики (наблюдатели) получают его. Редакция не знает, кто все эти люди, да ей это и не нужно.

Интересный факт: "Наблюдатель" настолько популярен, что официально входит в "банду четырех" паттернов проектирования (GoF).

Observer в C#: воплощение через события и делегаты

В C# паттерн "Наблюдатель" реализован "из коробки" через механизмы событий и делегатов. Событие — это "точка расширения", на которую могут подписываться разные обработчики. Вместо ручного ведения списка подписчиков всё делает языковой механизм событий. Ниже посмотрим на «ручную» реализацию, а затем на реализацию через события.

2. Классическая реализация Observer без использования событий

Рассмотрим, как бы это могло выглядеть, если бы событий в языке не было:


// Интерфейс наблюдателя
public interface IObserver
{
    void Update(string message);
}

// Издатель
public class Worker
{
    private List<IObserver> observers = new List<IObserver>();

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    public void Unsubscribe(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void DoWork()
    {
        Console.WriteLine("Worker работает...");
        NotifyObservers("Работа завершена!");
    }

    private void NotifyObservers(string message)
    {
        foreach (var observer in observers)
        {
            observer.Update(message);
        }
    }
}

// Конкретный наблюдатель
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"
WorkListener получил сообщение: {message}");
    }
}

Инициализация:


var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();

На заметку: Здесь список подписчиков (List<IObserver> observers) ведется вручную, и подписка/отписка — явные методы Subscribe/Unsubscribe.

3. События и делегаты — "высокоуровневая" реализация Observer

Мы можем реализовать то же самое проще и изящнее с помощью событий. Это и есть Observer в стиле C#:


public class Worker
{
    public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;

    public void DoWork()
    {
        Console.WriteLine("Worker работает...");
        OnWorkCompleted("Работа завершена!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; set; }
}

public class WorkListener
{
    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"WorkListener получил сообщение: {e.Message}");
    }
}

// Подписка:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();

Плюсы такого подхода:

  • Нет необходимости вручную поддерживать список подписчиков.
  • Доступны все возможности событий: множественная подписка, отписка, лямбды.
  • Гарантирована безопасность: только издатель может вызвать событие.
  • Слабая связанность: издатель не знает ничего о слушателях.

4. Как "наблюдатель" вписывается в наше приложение

Давайте интегрируем паттерн Observer в наше консольное приложение. Пусть Worker имеет любое количество обработчиков, которые будут реагировать на завершение работы по-разному: кто-то пишет в консоль, кто-то считает количество выполненных работ, кто-то отправляет письмо "Шеф! Всё сделано!".

Расширяем код примерами


// Второй слушатель-счетчик
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"Работа учтена. Всего: {Count} выполнено.");
    }
}

// Создаем объекты
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// Обе подписки
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// Имитация нескольких работ
worker.DoWork();
worker.DoWork();
// Output:
// Worker работает...
// WorkListener получил сообщение: Работа завершена!
// Работа учтена. Всего: 1 выполнено.
// Worker работает...
// WorkListener получил сообщение: Работа завершена!
// Работа учтена. Всего: 2 выполнено.

Таким образом, вы добавляете "наблюдателей" по мере необходимости, не меняя ни одной строчки в коде Worker. Класс Worker остается неизменным, а поведение всей системы расширяется через подписчиков.

5. Полезные нюансы

Реальный пример: Observer в интерфейсах и GUI

Observer-паттерн — основа всех GUI-фреймворков. В Windows Forms или WPF нажатие кнопки вызывает событие Click. Вы пишете обработчики (наблюдатели), которые будут реагировать на это событие — и ни вашему классу Button, ни библиотеке .NET ничего о ваших подписчиках знать не нужно.


// В WPF или WinForms (примерно)
myButton.Click += (s, e) => MessageBox.Show("Пользователь нажал на кнопку!");

Observer в реальных проектах

  • Пользовательский интерфейс (реакция на клики, изменения, таймеры и т.п.).
  • Системы уведомлений и событий.
  • Плагины для расширяемых систем (ядро генерирует события, расширения подписываются).
  • Распределённые системы и игровые движки (слабосвязанные реакционные цепочки).

Короче говоря, если вам нужно сделать расширяемую систему, где одни части могут реагировать на изменения других — Observer must have!

7. Особенности, типичные ошибки и их предотвращение

Потенциальные трудности при использовании Observer

Утечки памяти. Если подписчик подписался на событие, но не отписался (особенно в долгоживущих объектах), то сборщик мусора не сможет освободить этот объект, потому что издатель всё ещё хранит на него ссылку через делегат события. Это критично, если подписчик больше не нужен, а издатель продолжает жить.

Множественная подписка. Если один и тот же обработчик подписан дважды, он вызовется два раза — получите дублирование действий и неожиданные эффекты.

Исключения в обработчиках. Если один из обработчиков бросит исключение, выполнение следующих подписчиков может прерваться. Продумайте устойчивость обработчиков и, при необходимости, вызывать их вручную в try-catch, чтобы остальные подписчики сработали даже при сбое одного.

Распространенная схема утечек

flowchart LR
    Publisher["Издатель
(Worker)"] -- событие --> ObserverA["Слушатель A (Жив!)"] Publisher -- событие --> ObserverB["Слушатель B (Утечка: забыли отписаться)"]
2
Задача
C# SELF, 53 уровень, 3 лекция
Недоступна
Использование событий для реализации Observer
Использование событий для реализации Observer
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ