Весняний РОЗПРОДАЖ

Спостерігач

Також відомий як: Видавець-Підписник, Слухач, Observer

Суть патерна

Спостерігач — це поведінковий патерн проектування, який створює механізм підписки, що дає змогу одним об’єктам стежити й реагувати на події, які відбуваються в інших об’єктах.

Патерн Спостерігач

Проблема

Уявіть, що ви маєте два об’єкти: Покупець і Магазин. До магазину мають ось-ось завезти новий товар, який цікавить покупця.

Покупець може щодня ходити до магазину, щоб перевіряти наявність товару. Але через це він буде дратуватися, даремно витрачаючи свій дорогоцінний час.

Постійне відвідування магазину чи спам?

Постійне відвідування магазину чи спам?

З іншого боку, магазин може розсилати спам кожному своєму покупцеві. Багатьох покупців це засмутить, оскільки товар специфічний і потрібний не всім.

Виходить конфлікт: або покупець гає час на періодичні перевірки, або магазин розтрачує ресурси на непотрібні сповіщення.

Рішення

Давайте називати Видавцями ті об’єкти, які містять важливий або цікавий для інших стан. Решту об’єктів, які хотіли б відстежувати зміни цього стану, назвемо Підписниками.

Патерн Спостерігач пропонує зберігати всередині об’єкта видавця список посилань на об’єкти підписників. Причому видавець не повинен вести список підписки самостійно. Він повинен надати методи, за допомогою яких підписники могли б додавати або прибирати себе зі списку.

Підписка на події

Підписка на події.

Тепер найцікавіше. Коли у видавця відбуватиметься важлива подія, він буде проходитися за списком передплатників та сповіщувати їх про подію, викликаючи певний метод об’єктів-передплатників.

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

Сповіщення про події

Сповіщення про події.

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

Аналогія з життя

Передплата та доставка газет.

Передплата та доставка газет.

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

Видавництво веде список підписників і знає, кому який журнал слати. Ви можете в будь-який момент відмовитися від підписки, й журнал перестане до вас надходити.

Структура

Структура класів патерна СпостерігачСтруктура класів патерна Спостерігач
  1. Видавець володіє внутрішнім станом, зміни якого цікаво відслідковувати підписникам. Видавець містить механізм підписки: список підписників та методи підписки/відписки.

  2. Коли внутрішній стан видавця змінюється, він сповіщає своїх підписників. Для цього видавець проходиться за списком підписників і викликає їхній метод сповіщення, який описаний в загальному інтерфейсі підписників.

  3. Підписник визначає інтерфейс, яким користується видавець для надсилання сповіщень. Здебільшого для цього досить одного методу.

  4. Конкретні підписники виконують щось у відповідь на сповіщення, яке надійшло від видавця. Ці класи мають дотримуватися загального інтерфейсу, щоб видавець не залежав від конкретних класів підписників.

  5. Після отримання сповіщення підписнику необхідно отримати оновлений стан видавця. Видавець може передати цей стан через параметри методу сповіщення. Більш гнучкий варіант — передавати через параметри весь об’єкт видавця, щоб підписник міг сам отримати необхідні дані. Як варіант, підписник може постійно зберігати посилання на об’єкт видавця, переданий йому через конструктор.

  6. Клієнт створює об’єкти видавців і підписників, а потім реєструє підписників на оновлення у видавцях.

Псевдокод

У цьому прикладі Спостерігач дає змогу об’єкту текстового редактора сповіщати інші об’єкти про зміни свого стану.

Структура класів прикладу патерна Спостерігач

Приклад сповіщення об’єктів про події в інших об’єктах.

Список підписників складається динамічно, об’єкти можуть як підписуватися на певні події, так і відписуватися від них прямо під час виконання програми.

У цій реалізації редактор не веде список підписників самостійно, а делегує це вкладеному об’єкту. Це дає змогу використовувати механізм підписки не лише в класі редактора, а і в інших класах програми.

Для додавання до програми нових підписників не потрібно змінювати класи видавців, допоки вони працюють із підписниками через загальний інтерфейс.

// Базовий клас-видавець. Містить код керування підписниками та
// надсилання їм сповіщень.
class EventManager is
    private field listeners: hash map of event types and listeners

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// Конкретний клас-видавець, що містить цікаву для інших
// компонентів бізнес-логіку. Ми могли б зробити його прямим
// нащадком EventManager, але в реальному житті це не завжди є
// можливим (наприклад, якщо в класу вже є предок). Тому тут ми
// підключаємо механізм підписки за допомогою композиції.
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // Методи бізнес-логіки, які сповіщають підписників про
    // зміни.
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)
    // ...


// Загальний інтерфейс підписників. У багатьох мовах, що мають
// функціональні типи, можна обійтися без цього інтерфейсу та
// конкретних класів, замінивши об'єкти підписників функціями.
interface EventListener is
    method update(filename)

// Набір конкретних підписників. Кожен з них виконує якусь
// поведінку, реагуючи на сповіщення від видавця.
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// Програма може сконфігурувати видавців та підписників, як
// завгодно, залежно від цілей та оточення.
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened file: %s");
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

Застосування

Якщо після зміни стану одного об’єкта потрібно щось зробити в інших, але ви не знаєте наперед, які саме об’єкти мають відреагувати.

Описана проблема може виникнути при розробленні бібліотек користувацього інтерфейсу, якщо вам необхідно надати можливість стороннім класам реагувати на кліки по кнопках.

Патерн Спостерігач надає змогу будь-якому об’єкту з інтерфейсом підписника зареєструватися для отримання сповіщень про події, що трапляються в об’єктах-видавцях.

Якщо одні об’єкти мають спостерігати за іншими, але тільки у визначених випадках.

Видавці ведуть динамічні списки. Усі спостерігачі можуть підписуватися або відписуватися від отримання сповіщень безпосередньо під час виконання програми.

Кроки реалізації

  1. Розбийте вашу функціональність на дві частини: незалежне ядро та опціональні залежні частини. Незалежне ядро стане видавцем. Залежні частини стануть підписниками.

  2. Створіть інтерфейс підписників. Зазвичай достатньо визначити в ньому лише один метод сповіщення.

  3. Створіть інтерфейс видавців та опишіть у ньому операції керування підпискою. Пам’ятайте, що видавці повинні працювати з підписниками тільки через їхній загальний інтерфейс.

  4. Вам потрібно вирішити, куди помістити код ведення підписки, адже він зазвичай буває однаковим для всіх типів видавців. Найочевидніший спосіб — це винесення коду до проміжного абстрактного класу, від якого будуть успадковуватися всі видавці.

    Якщо ж ви інтегруєте патерн до існуючих класів, то створити новий базовий клас може бути важко. У цьому випадку ви можете помістити логіку підписки в допоміжний об’єкт та делегувати йому роботу з видавцями.

  5. Створіть класи конкретних видавців. Реалізуйте їх таким чином, щоб після кожної зміні стану вони слали сповіщення всім своїм підписникам.

  6. Реалізуйте метод сповіщення в конкретних підписниках. Не забудьте передбачити параметри, через які видавець міг би відправляти якісь дані, пов’язані з подією, що відбулась.

    Можливий і інший варіант, коли підписник, отримавши сповіщення, сам візьме потрібні дані з об’єкта видавця. Але в цьому разі ви будете змушені прив’язати клас підписника до конкретного класу видавця.

  7. Клієнт повинен створювати необхідну кількість об’єктів підписників та підписувати їх у видавців.

Переваги та недоліки

  • Видавці не залежать від конкретних класів підписників і навпаки.
  • Ви можете підписувати і відписувати одержувачів «на льоту».
  • Реалізує принцип відкритості/закритості.
  • Підписники сповіщуються у випадковій послідовності.

Відносини з іншими патернами

  • Ланцюжок обов’язків, Команда Посередник та Спостерігач показують різні способи роботи тих, хто надсилає запити, та тих, хто їх отримує:

    • Ланцюжок обов’язків передає запит послідовно через ланцюжок потенційних отримувачів, очікуючи, що один з них обробить запит.
    • Команда встановлює непрямий односторонній зв’язок від відправників до одержувачів.
    • Посередник прибирає прямий зв’язок між відправниками та одержувачами, змушуючи їх спілкуватися опосередковано, через себе.
    • Спостерігач передає запит одночасно всім зацікавленим одержувачам, але дозволяє їм динамічно підписуватися або відписуватися від таких повідомлень.
  • Різниця між Посередником та Спостерігачем не завжди очевидна. Найчастіше вони виступають як конкуренти, але іноді можуть працювати разом.

    Мета Посередника — прибрати взаємні залежності між компонентами системи. Замість цього вони стають залежними від самого посередника. З іншого боку, мета Спостерігача — забезпечити динамічний односторонній зв’язок, в якому одні об’єкти опосередковано залежать від інших.

    Досить популярною є реалізація Посередника за допомогою Спостерігача. При цьому об’єкт посередника буде виступати видавцем, а всі інші компоненти стануть передплатниками та зможуть динамічно стежити за подіями, що відбуваються у посереднику. У цьому випадку важко зрозуміти, чим саме відрізняються обидва патерни.

    Але Посередник має й інші реалізації, коли окремі компоненти жорстко прив’язані до об’єкта посередника. Такий код навряд чи буде нагадувати Спостерігача, але залишиться Посередником.

    Навпаки, у разі реалізації посередника з допомогою Спостерігача, представимо чи уявімо таку програму, в якій кожен компонент системи стає видавцем. Компоненти можуть підписуватися один на одного, не прив’язуючись до конкретних класів. Програма складатиметься з цілої мережі Спостерігачів, не маючи центрального об’єкта Посередника.

Приклади реалізації патерна

Спостерігач на C# Спостерігач на C++ Спостерігач на Go Спостерігач на Java Спостерігач на PHP Спостерігач на Python Спостерігач на Ruby Спостерігач на Rust Спостерігач на Swift Спостерігач на TypeScript