handbookНаписать

patterns/behavioral/observer/README.md · тема · 4 языка · 23 теста

Наблюдатель (Observer) ​

Объект сообщает об изменениях всем, кто подписался, и не знает, кто они: подписчиков можно добавлять и убирать, не трогая источник.

Проблема ​

Биржевой тикер получает новую цену акции. Её нужно показать на табло, проверить по порогу оповещения, а завтра ещё записать в график и отправить в мобильное приложение. Если тикер сам вызывает board.update(), alert.check() и chart.add(), он знает обо всех потребителях: каждый новый — правка тикера, тест тикера тянет за собой табло и почту, а отключить оповещения на время без if не получится.

Решение ​

Перевернуть зависимость. Источник — издатель StockTicker — хранит список подписчиков и при изменении цены обходит его. О подписчиках он знает одно: у них есть метод из контракта PriceObserver — priceChanged(symbol, price).

Подписчики PriceBoard и PriceAlert реализуют контракт и сами решают, что делать с новостью: табло запоминает последнюю цену, оповещение срабатывает выше порога. Подписка и отписка — методы издателя, их вызывает тот, кто собирает систему. Новый потребитель — новый класс и одна строка subscribe, тикер не меняется.

Структура ​

notifies

«interface»

PriceObserver

+priceChanged(symbol, price)

StockTicker

-observers: PriceObserver[]

-price: int

+subscribe(observer)

+unsubscribe(observer)

+setPrice(price)

PriceBoard

+prices: map

+priceChanged(symbol, price)

PriceAlert

-threshold: int

+alerts: list

+priceChanged(symbol, price)

Издатель держит подписчиков в порядке подписки и рассылает уведомление, только если цена действительно изменилась: повтор той же цены никого не будит. В уведомлении — символ акции и цена (модель «push»): одному табло можно подписаться на несколько тикеров и различать их по символу. Цены — целые центы, без float.

Как это собирается ​

Систему собирает терминал при старте: по списку символов из избранного создаётся StockTicker на каждый, к каждому подписывается одно общее PriceBoard и своё PriceAlert с порогом из настроек пользователя. Тикеры складываются в карту "символ -> тикер", и дальше обработчик биржевого потока только находит тикер по символу и зовёт setPrice. Кто и что сделает с ценой, обработчик не знает.

Поток приходит как есть: котировка неизвестного символа XYZ не роняет обработку, обработчик её пропускает и пишет в лог. Повтор той же цены ACME никого не будит, поэтому оповещение не срабатывает дважды. В конце табло показывает последние цены обоих тикеров, а оповещения - все пересечения порогов.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Подписчики не знают ни друг о друге, ни о том, сколько их всего. У каждого своё состояние: табло — словарь «символ -> цена», оповещение — порог и список сработок. Один и тот же объект может слушать разные тикеры.

ЯзыкОсобенность
PHPИнтерфейс PriceObserver; подписчики хранятся по spl_object_id, поэтому повторная подписка того же объекта не дублирует его
GoSubscribe возвращает функцию отписки: подписка помнит свой номер, сравнивать подписчиков не нужно (функции в Go несравнимы)
PythonПодписчик — любой вызываемый объект Callable[[str, int], None]; подписывают связанный метод board.price_changed
TypeScriptSet<PriceObserver> хранит порядок вставки и удаляет по ссылке; subscribe возвращает функцию отписки

Запуск: make test T=patterns/behavioral/observer

Плюсы и минусы ​

Тесты одинаковы во всех языках и проверяют пять случаев: табло показывает последнюю цену; оба подписчика получают одно и то же изменение (табло — 12000, оповещение с порогом 11000 — одна сработка); после отписки табло застывает на старой цене; одна и та же цена дважды даёт одно уведомление; одно табло следит за двумя тикерами.

  • ✅ Издатель не зависит от конкретных подписчиков: новые добавляются без правки источника.
  • ✅ Подписку можно менять во время работы: включить оповещение, отключить табло.
  • ✅ Издатель и подписчики тестируются отдельно.
  • ❌ Порядок и время уведомлений неявны: из кода тикера не видно, кто и что сделает при setPrice.
  • ❌ Ошибка одного подписчика здесь прерывает рассылку остальным; отписка и новая публикация изнутри уведомления — отдельные ловушки. Всё это разбирает практика.
  • ❌ Забытая отписка держит подписчика в памяти (утечка «висящих слушателей»).

Когда не применять ​

  • Потребитель один и не меняется: прямой вызов понятнее.
  • Нужен ответ от получателя (вернуть значение, отказать): это не уведомление, а вызов. Подойдёт Стратегия или цепочка обработчиков.
  • Подписчики зависят друг от друга и должны идти в строгом порядке с общим результатом: это сценарий, его лучше написать явно.
  • Событие должно пережить падение процесса или дойти до другого сервиса: внутрипроцессный наблюдатель этого не гарантирует, нужна очередь сообщений или outbox.

Отличия между языками ​

  • PHP — классический вариант: интерфейс и объекты. В SPL есть готовые SplSubject и SplObserver, но их update(SplSubject $subject) передаёт сам издатель (модель «pull») и не типизирует данные, поэтому свой узкий интерфейс удобнее. Массив с ключом spl_object_id даёт порядок и отписку за O(1).
  • Go — интерфейс с одним методом; часто подписчиком делают просто func(symbol string, price int). Функции в Go несравнимы, поэтому отписка по значению невозможна, и идиома — вернуть из Subscribe функцию отмены (как context.WithCancel). Для асинхронной рассылки в Go берут каналы, но это уже другой контракт: буфер, закрытие, горутины.
  • Python — интерфейс не нужен: подписчик — любой вызываемый объект. Связанные методы board.price_changed при каждом обращении создаются заново, но сравниваются по == (тот же объект и та же функция), поэтому list.remove их находит. Лямбду так не отпишешь, её надо сохранить в переменную.
  • TypeScript — Set сохраняет порядок вставки и не допускает дублей; функция отписки из subscribe — привычная идиома (как removeEventListener наоборот). В браузере и Node есть готовые EventTarget и EventEmitter — это тот же паттерн с событиями по имени.

Практика ​

  • Учебный тикер рассылает одно событие подписчикам, которые не падают и ничего не публикуют в ответ. В жизни так не бывает: практика: доменные события заказа показывает диспетчер с подпиской по типу события, детерминированным порядком, сбором ошибок подписчиков, безопасной отпиской во время рассылки и очередью вместо рекурсии, когда склад отменяет заказ в ответ на его создание.
  • Учебный подписчик не помнит прошлых событий и не знает о времени. Практика: поток показаний приборов учёта показывает подписчиков с состоянием: тревогу по порогу, контроль связи, который видит тишину только по явному тику часов, суточный расход по UTC и монтажника, который отписывается сам посреди рассылки.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS