JavaRush /Курси /C# SELF /Патерн «Спостерігач» ( Obse...

Патерн «Спостерігач» ( 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();
// Виведення:
// 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 є справді необхідним інструментом.

7. Особливості, типові помилки та їх запобігання

Потенційні труднощі під час використання Observer

Витоки памʼяті. Якщо підписник підписався на подію, але не відписався (особливо в довгоживучих обʼєктах), збирач сміття не зможе звільнити цей обʼєкт, тому що видавець усе ще тримає на нього посилання через делегат події. Це критично, якщо підписник більше не потрібен, а видавець продовжує жити.

Множинна підписка. Якщо один і той самий обробник підписаний двічі, він викличеться двічі — отримаєте дублювання дій і неочікувані ефекти.

Винятки в обробниках. Якщо один із обробників кине виняток, виконання наступних підписників може перерватися. Продумайте стійкість обробників і, за потреби, викликайте їх вручну в try-catch, щоб інші підписники відпрацювали навіть у разі збою одного.

Поширена схема витоків

flowchart LR
    Publisher["Видавець
(Worker)"] -- подія --> ObserverA["Слухач A (Живий!)"] Publisher -- подія --> ObserverB["Слухач B (Витік: забули відписатися)"]
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ