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

Наблюдатель

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

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

Проблема

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

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

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

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

Есть ещё один минус — в обоих случаях вы жёстко свяжете друг с другом конкретные классы Покупателя и Магазина.

Решение

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

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

Подписчики могут оформлять и закрывать подписку динамически, прямо во время выполнения программы.

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

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

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

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

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

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

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

Структура

Схема структуры классов паттерна Наблюдатель
  1. Издатель владеет каким-то состоянием, интересным для подписчиков.

    Он содержит механизм подписки — список подписчиков, а также методы подписки/отписки.

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

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

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

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

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

Псевдокод

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

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

// Базовый класс-издатель. Содержит код управления подписчиками и их оповещения.
class EventManager is
    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
    field events: EventManager
    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
    field log: File

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

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

class EmailNotificationListener is
    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. Независимое ядро станет Издателем. Зависимые части станут Подписчиками.

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

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

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

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

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

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

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

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

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

    • Цепочка обязанностей передаёт запрос отправителя через цепочку потенциальных получателей, ожидая, что какой-то из них обработает запрос.
    • Команда соединяет отправителя и получателя через подкласс.
    • Посредник заставляет отправителя и получателя коммуницировать только через себя.
    • Наблюдатель позволяет динамически подключать и отключать обработчиков запроса.
  • Посредник и Наблюдатель – это паттерны конкуренты. Дополнительную путаницу вводит то, что Посредник можно построить при помощи Наблюдателя.

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

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

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

Java