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 (Витік: забули відписатися)"]
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ