Также известен как Издатель-Подписчик, Слушатель, Observer

Наблюдатель

Суть паттерна

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

Проблема

Представьте, что у вас есть два объекта — Покупатель и Магазин. В магазин вот-вот должны завезти новый товар, который интересен покупателю.

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

С другой стороны, магазин может разослать спам каждому своему покупателю. Многих это расстроит, так как товар специфический и не всем он нужен.

Получается конфликт — либо один объект работает неэффективно, тратя ресурсы на периодические проверки, либо второй объект оповещает слишком широкий круг пользователей, тоже тратя ресурсы впустую.

Решение

Давайте будем называть объекты, которые содержат интересное состояние Издателями. А другие объекты, которым интересно это состояние давайте звать Подписчиками.

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

Теперь самое интересное. Каждый раз, когда в издателе будет происходить что-нибудь, он должен проходился по списку своих подписчиков и оповещать их, вызывая определённый метод в их объектах.

Издателю всё равно какого конкретно подписчика он оповещает, так как все подписчики договорились иметь один и тот же метод оповещения, то есть иметь общий интерфейс.

Увидев как складно всё работает, вы можете выделить общий интерфейс и для всех издателей. Он получит методы подписки и отписки. После этого подписчики смогут однотипно работать с издателями, а также получать оповещения от них через один и тот же метод.

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

Подписка на газеты

После того как вы оформили подписку на газету или журнал, вам больше не нужно ездить в супермаркет и проверять не вышел ли очередной номер. Вместо этого, издательство будет присылать новые номера сразу после выхода прямо к вам домой.

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

Структура

Структура классов паттерна «Наблюдатель»
  1. Издатель владеет внутренним состоянием, изменение которого интересно для подписчиков. Он содержит механизм подписки — список подписчиков, а также методы подписки/отписки.

    Когда внутреннее состояние издателя меняется, он оповещает своих подписчиков. Для этого издатель проходит по списку подписчиков и вызывает их метод оповещения, заданный в интерфейсе Наблюдателя.

  2. Подписчик определяет интерфейс, которым пользуется Издатель для оповещения. В большинстве случаев, достаточно одного метода оповестить().

  3. Конкретный подписчик выполняет что-то в ответ на оповещение пришедшее от Издателя. Реализует общий интерфейс Подписчиков.

    По приходу оповещения, подписчику нужно получить обновлённое состояние издателя. Для этого издатель может передавать ссылку на собственный объект в параметрах метода оповещения. Как вариант, подписчик может постоянно хранить ссылку на объект издателя, переданный ему в конструкторе.

  4. Клиент создаёт объекты издателей и подписчиков, а затем регистрирует Подписчиков на обновления в Издателях.

    Иногда удобно иметь прямой доступ из подписчика к издателю. Такая связь обычно устанавливается через конструктор подписчика. Это позволяет подписчику получать обновленное состояние непосредственно из объекта издателя после получения уведомлений.

Псевдокод

В этом примере Наблюдатель позволяет объекту текстового редактора оповещать другие объекты об изменениях своего состояния.

Список подписчиков составляется динамически, объекты могут как подписываться на определённые события, так и отписываться от них прямо во время выполнения программы.

В этой реализации, редактор не ведёт список подписчиков сам, а делегирует это вложенному объекту. Это даёт возможность использовать механизм подписки и в других объектах программы, а не только в классе редактора. Таким образом, паттерн Наблюдатель позволяет динамически настраивать обработчики тех или иных событий, происходящих с объектом.

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

// Базовый класс-издатель. Содержит код управления подписчиками и их оповещения.
class EventManager is
    private field listeners: hash map of eventTypes and EventListeners

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

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

    method notify(eventType, a) is
        foreach listeners.of(eventType) as listener
            listener.update(a)

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

    constructor Editor() is
        events = new EventManager()

    // Методы бизнес-логики, которые оповещают подписчиков об изменениях.
    method openFile(filename) is
        this.file = new File(filename)
        events.notify('open', filename)

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


// Общий интерфейс подписчиков. В современных языках можно обойтись без этого
// интерфейса и конкретных слушателей, подписывая на обновления функции
// вместо объектов.
interface EventListener is
    method update(a)

// Набор конкретных слушателей. Они реализуют добавочную функциональность,
// реагируя на извещения от издателя.
class LogOpenListener is
    private field log: File

    constructor LogOpenListener(log_filename) is
        this.log = new File(log_filename)

    method update(filename) is
        log.write("Opened: " + filename)

class EmailNotificationListener is
    private field email: string

    constructor EmailNotificationListener(email) is
        this.email = email

    method update(filename) is
        system.email(email, "Someone has changed the file: " + filename)


// Приложение может сконфигурировать издателей и слушателей как угодно, в
// зависимости от целей и конфигурации.
class Application is
    method config() is
        editor = new TextEditor()
        editor.events.subscribe("open",
            new LogOpenListener("/path/to/log/file.txt"))
        editor.events.subscribe("save",
            new EmailNotificationListener("admin@example.com"))

Применимость

Когда при изменении состояния одного объекта требуется что-то сделать в других, но вы не знаете наперёд какие именно объекты должны отреагировать.

Например, при разработке GUI фреймворка вам нужно дать возможность сторонним классам реагировать на клики по кнопкам.

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

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

Издатели ведут динамические списки. Все наблюдатели могут подписываться или отписываться на обновления прямо во время выполнения программы.

Шаги реализации

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

  2. Создайте интерфейс Подписчика. В большинстве случаев, достаточно определить один метод update().

  3. Создайте интерфейс Издателя и опишите в нём операции управления подпиской. Помните, что издатель должен работать только с общим интерфейсом Подписчиков.

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

Но если вы интегрируете паттерн в существующие классы, то создать новый базовый класс возможно будет затруднительно. В этом случае, вы можете поместить логику подписки во вспомогательный объект и делегировать ему работу из издателей.

  1. Создайте или измените классы Конкретных издателей так, чтобы при каждом изменении состояния, они слали оповещения всем своим подписчикам.

  2. Реализуйте метод оповещения в Конкретных подписчиках. Издатели может отправлять какие-то данные вместе с оповещением (например, в параметрах). Возможен и другой вариант, когда подписчик, получив оповещение, сам берёт из объекта издателя нужные данные. Но при этом подписчик привяжет себя к конкретному классу издателя.

  3. Клиент создаёт необходимое количество объектов подписчиков и регистрирует их у издателей.

Преимущества и недостатки

  • Издатель не зависит от конкретных классов подписчиков.
  • Вы можете подписывать и отписывать получателей на лету.
  • Реализует принцип открытости/закрытости.
  • Наблюдатели оповещаются в случайном порядке.

Отношения с другими паттернами

  • Цепочка обязанностей, Команда, Посредник и Наблюдатель показывают различные способы работы отправителей запросов с их получателями:

    • Цепочка обязанностей передаёт запрос последовательно через цепочку потенциальных получателей, ожидая, что какой-то из них обработает запрос.

    • Команда устанавливает косвенную одностороннюю связь от отправителей к получателям.

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

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

  • Разница между Посредником и Наблюдателем не всегда очевидна. Чаще всего они выступают как конкуренты, но иногда могут работать вместе.

    Цель Посредника — убрать обоюдные зависимости между компонентами системы. Вместо этого они становятся зависимыми от самого посредника. С другой стороны, цель Наблюдателя — обеспечить динамическую одностороннюю связь, в которой одни объекты косвенно зависят от других.

    Довольно популярна реализация Посредника при помощи Наблюдателя. При этом объект посредника будет выступать издателем, а все остальные компоненты станут подписчиками и смогут динамически следить за событиями, происходящими в посреднике. В этом случае трудно понять, чем же отличаются оба паттерна.

    Но Посредник имеет и другие реализации, когда отдельные компоненты жёстко привязаны к объекту посредника. Такой код вряд ли будет напоминать Наблюдателя, но всё же останется Посредником.

    Теперь представьте другую программу, в которой каждый компонент системы становится издателем. Компоненты могут подписываться друг на друга, в то же время, не привязываясь к конкретным классам. Программа будет состоять из целой сети Наблюдателей, не имея центрального объекта Посредника.

Реализация в различных языках программирования

Java