Observer — Наблюдатель
Вы подписаны на YouTube-канал. Когда автор публикует новое видео — вы получаете уведомление. Вы не обязаны постоянно проверять канал вручную: платформа сама вас уведомит. Именно эту идею реализует паттерн Observer (Наблюдатель) — объект сообщает всем заинтересованным о своих изменениях автоматически.
📬 Аналогия: рассылка новостей
Прежде чем писать код, разберёмся с идеей на пальцах. Представьте, что есть интернет-магазин. У него есть блог с акциями. Люди могут подписаться на этот блог — и тогда при публикации новой акции они автоматически получат письмо, SMS или push-уведомление.
Издатель (Subject)
Это блог магазина. Он знает список подписчиков и умеет отправлять им уведомления.
Подписчик (Observer)
Это читатель. Он подписался и ждёт уведомлений. Когда приходит новость — реагирует на неё.
Уведомление (notify)
Момент, когда издатель обходит всех подписчиков и вызывает у каждого метод update().
Ключевая особенность: издатель не знает, кто именно его слушает и что они с этим сделают. Кто-то отправит email, кто-то — SMS, кто-то — push. Издателю всё равно: он просто вызывает update() у каждого подписчика, а дальше — их дело.
💡 Именно в этом суть паттерна: объект-издатель и объекты-подписчики слабо связаны друг с другом. Издатель не импортирует классы подписчиков. Подписчики могут добавляться и удаляться динамически — без изменения кода издателя.
😤 Проблема: жёсткая связь без Observer
Допустим, вы пишете класс Newsletter (новостная рассылка). Когда выходит новая статья, нужно уведомить подписчиков по email и SMS. Самое очевидное — вызвать нужные методы напрямую внутри Newsletter:
class EmailService:
def send_email(self, message: str):
print(f"[Email] Отправляем письмо: {message}")
class SMSService:
def send_sms(self, message: str):
print(f"[SMS] Отправляем SMS: {message}")
class Newsletter:
def __init__(self):
self.email_service = EmailService()
self.sms_service = SMSService()
def publish(self, article: str):
print(f"Опубликована статья: {article}")
# 🔴 ПЛОХО: Newsletter жёстко завязан на конкретные сервисы
self.email_service.send_email(f"Новая статья: {article}")
self.sms_service.send_sms(f"Новая статья: {article}")
newsletter = Newsletter()
newsletter.publish("10 советов по Python")
🔥 Что здесь не так?
- Newsletter знает о EmailService и SMSService напрямую. Если нужно добавить push-уведомления — придётся лезть в класс Newsletter и менять его.
- Нельзя динамически добавлять/убирать подписчиков. Хотите временно отключить SMS? Нужно менять код Newsletter.
- Тестирование сложное: чтобы проверить Newsletter, нужно тянуть за собой EmailService и SMSService.
Паттерн Observer решает это радикально: издатель знает только об абстрактном интерфейсе Observer. Кто именно подписан — ему неважно.
🏗️ Структура паттерна Observer
Observer состоит из двух частей — Subject (издатель) и Observer (подписчик). Обе части описываются через абстрактные классы с помощью ABC.
📡 Subject (Observable)
Издатель. Хранит список подписчиков и три метода:
attach(observer)— добавить подписчикаdetach(observer)— удалить подписчикаnotify()— уведомить всех
👁 Observer
Подписчик. Имеет единственный абстрактный метод:
update(data)— реакция на уведомление
Каждый конкретный подписчик реализует этот метод по-своему: отправляет email, SMS, push и т.д.
Давайте запишем эту структуру в коде. Сначала — абстрактные классы:
from abc import ABC, abstractmethod
from typing import List
# ─────────────────────────────────────────────
# OBSERVER — абстрактный подписчик
# ─────────────────────────────────────────────
class Observer(ABC):
"""
Интерфейс подписчика. Каждый, кто хочет
получать уведомления, должен реализовать update().
"""
@abstractmethod
def update(self, subject: "Subject") -> None:
"""
Вызывается издателем, когда происходит событие.
subject — ссылка на издателя (можно взять его данные).
"""
pass
# ─────────────────────────────────────────────
# SUBJECT — абстрактный издатель
# ─────────────────────────────────────────────
class Subject(ABC):
"""
Интерфейс издателя. Умеет хранить и уведомлять подписчиков.
"""
@abstractmethod
def attach(self, observer: Observer) -> None:
"""Добавить подписчика в список."""
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
"""Удалить подписчика из списка."""
pass
@abstractmethod
def notify(self) -> None:
"""Уведомить всех подписчиков — вызвать у каждого update()."""
pass
✅ Обратите внимание: метод update() принимает subject — ссылку на самого издателя. Это позволяет подписчику спросить у издателя любые нужные данные. Такой подход называется pull model (подписчик «тянет» данные сам), в отличие от push model (издатель «толкает» данные прямо в аргументы). Оба подхода корректны.
📰 Реализуем издателя: класс Newsletter
Теперь напишем конкретный класс Newsletter — он наследуется от Subject и реализует все три метода. Плюс у него будет собственное состояние — текущая статья:
class Newsletter(Subject):
"""
Конкретный издатель — новостная рассылка.
Хранит список подписчиков и текущую статью.
"""
def __init__(self) -> None:
self._observers: List[Observer] = [] # список подписчиков
self._latest_article: str = "" # последняя опубликованная статья
# ── Управление подписчиками ───────────────────
def attach(self, observer: Observer) -> None:
if observer not in self._observers:
self._observers.append(observer)
print(f" [+] Подписчик {observer.__class__.__name__} добавлен.")
def detach(self, observer: Observer) -> None:
try:
self._observers.remove(observer)
print(f" [-] Подписчик {observer.__class__.__name__} удалён.")
except ValueError:
pass # если подписчика не было — ничего не делаем
def notify(self) -> None:
"""Обходим всех подписчиков и вызываем update() у каждого."""
print(f" [Newsletter] Уведомляем {len(self._observers)} подписчиков...")
for observer in self._observers:
observer.update(self)
# ── Бизнес-логика ────────────────────────────
@property
def latest_article(self) -> str:
"""Подписчики смогут прочитать статью через это свойство."""
return self._latest_article
def publish(self, article: str) -> None:
"""Опубликовать новую статью и уведомить всех подписчиков."""
print(f"\n📰 Опубликована статья: «{article}»")
self._latest_article = article
self.notify() # автоматически уведомляем всех
🔍 Разбор кода:
self._observers— обычный список Python. Можно добавлять и удалять элементы в любой момент.attachпроверяетobserver not in self._observers— чтобы один подписчик не добавился дважды.- Метод
publishсначала меняет состояние (self._latest_article), затем вызываетnotify(). Порядок важен: подписчики должны видеть уже обновлённые данные.
👥 Конкретные подписчики: Email, SMS, Push
Теперь создадим несколько конкретных подписчиков. Каждый наследуется от Observer и реализует свой вариант update():
class EmailSubscriber(Observer):
"""Подписчик по email — получает статью на почту."""
def __init__(self, email: str) -> None:
self.email = email
def update(self, subject: Subject) -> None:
# Берём данные у издателя (pull model)
article = subject.latest_article
print(f" 📧 [Email → {self.email}] Новая статья: «{article}»")
class SMSSubscriber(Observer):
"""Подписчик по SMS — получает короткое уведомление."""
def __init__(self, phone: str) -> None:
self.phone = phone
def update(self, subject: Subject) -> None:
article = subject.latest_article
# SMS обычно короткие — берём только первые 30 символов
preview = article[:30] + "..." if len(article) > 30 else article
print(f" 📱 [SMS → {self.phone}] Новость: {preview}")
class PushSubscriber(Observer):
"""Подписчик push-уведомлений — для мобильного приложения."""
def __init__(self, device_id: str) -> None:
self.device_id = device_id
def update(self, subject: Subject) -> None:
article = subject.latest_article
print(f" 🔔 [Push → устройство {self.device_id}] 🔔 «{article}»")
💡 Обратите внимание:
Каждый подписчик получает ссылку на издателя (subject) и сам вытягивает нужные данные через subject.latest_article. Издатель не знает, что именно они с этим делают. Email-подписчик отправляет полный текст, SMS — сокращает до 30 символов. Это их личное дело.
🚀 Демонстрация: подписка, публикация, отписка
Соберём всё вместе и посмотрим на работу паттерна в действии:
# Создаём издателя
newsletter = Newsletter()
# Создаём подписчиков
email_user = EmailSubscriber("alice@example.com")
sms_user = SMSSubscriber("+7 900 123-45-67")
push_user = PushSubscriber("device-abc-123")
# Подписываем
newsletter.attach(email_user)
newsletter.attach(sms_user)
newsletter.attach(push_user)
# Публикуем первую статью — все три получат уведомление
newsletter.publish("10 советов по Python для начинающих")
# Отписываем SMS-подписчика
newsletter.detach(sms_user)
# Публикуем вторую статью — SMS уже не получит уведомление
newsletter.publish("Как работают декораторы в Python")
Вывод программы:
[+] Подписчик EmailSubscriber добавлен.
[+] Подписчик SMSSubscriber добавлен.
[+] Подписчик PushSubscriber добавлен.
📰 Опубликована статья: «10 советов по Python для начинающих»
[Newsletter] Уведомляем 3 подписчиков...
📧 [Email → alice@example.com] Новая статья: «10 советов по Python для начинающих»
📱 [SMS → +7 900 123-45-67] Новость: 10 советов по Python для...
🔔 [Push → устройство device-abc-123] 🔔 «10 советов по Python для начинающих»
[-] Подписчик SMSSubscriber удалён.
📰 Опубликована статья: «Как работают декораторы в Python»
[Newsletter] Уведомляем 2 подписчиков...
📧 [Email → alice@example.com] Новая статья: «Как работают декораторы в Python»
🔔 [Push → устройство device-abc-123] 🔔 «Как работают декораторы в Python»
🏆 Ключевые моменты демонстрации
- После
detach(sms_user)SMS-подписчик больше не получает уведомления — и мы не меняли кодNewsletterвообще. - Добавление нового типа подписчика (например,
TelegramSubscriber) — это написать новый класс и вызватьattach(). Код издателя не трогаем. - Все три подписчика отреагировали по-разному на одно и то же событие.
💻 Полный код: всё вместе
Соберём финальную версию в одном месте — удобно скопировать и запустить:
from abc import ABC, abstractmethod
from typing import List
# ══════════════════════════════════════════════
# АБСТРАКТНЫЕ КЛАССЫ (интерфейсы)
# ══════════════════════════════════════════════
class Observer(ABC):
@abstractmethod
def update(self, subject: "Subject") -> None:
pass
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer) -> None:
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
# ══════════════════════════════════════════════
# КОНКРЕТНЫЙ ИЗДАТЕЛЬ
# ══════════════════════════════════════════════
class Newsletter(Subject):
def __init__(self) -> None:
self._observers: List[Observer] = []
self._latest_article: str = ""
def attach(self, observer: Observer) -> None:
if observer not in self._observers:
self._observers.append(observer)
print(f" [+] {observer.__class__.__name__} подписан.")
def detach(self, observer: Observer) -> None:
try:
self._observers.remove(observer)
print(f" [-] {observer.__class__.__name__} отписан.")
except ValueError:
pass
def notify(self) -> None:
for observer in self._observers:
observer.update(self)
@property
def latest_article(self) -> str:
return self._latest_article
def publish(self, article: str) -> None:
print(f"\n📰 Публикуем: «{article}»")
self._latest_article = article
self.notify()
# ══════════════════════════════════════════════
# КОНКРЕТНЫЕ ПОДПИСЧИКИ
# ══════════════════════════════════════════════
class EmailSubscriber(Observer):
def __init__(self, email: str) -> None:
self.email = email
def update(self, subject: Subject) -> None:
print(f" 📧 Email → {self.email}: «{subject.latest_article}»")
class SMSSubscriber(Observer):
def __init__(self, phone: str) -> None:
self.phone = phone
def update(self, subject: Subject) -> None:
preview = subject.latest_article[:30]
print(f" 📱 SMS → {self.phone}: {preview}...")
class PushSubscriber(Observer):
def __init__(self, device_id: str) -> None:
self.device_id = device_id
def update(self, subject: Subject) -> None:
print(f" 🔔 Push → {self.device_id}: «{subject.latest_article}»")
# ══════════════════════════════════════════════
# ИСПОЛЬЗОВАНИЕ
# ══════════════════════════════════════════════
if __name__ == "__main__":
newsletter = Newsletter()
alice = EmailSubscriber("alice@example.com")
bob = SMSSubscriber("+7 900 123-45-67")
carol = PushSubscriber("device-abc-123")
# Все подписываются
newsletter.attach(alice)
newsletter.attach(bob)
newsletter.attach(carol)
# Первая статья — получат все трое
newsletter.publish("Введение в ООП на Python")
# Bob отписывается
newsletter.detach(bob)
# Вторая статья — получат Alice и Carol
newsletter.publish("Паттерн Observer на практике")
⚡ Push-модель vs Pull-модель
Существуют два способа передачи данных от издателя подписчику. Давайте разберём оба:
📤 Push-модель
Издатель сам передаёт данные в update():
def notify(self):
for obs in self._observers:
obs.update(self._latest_article)
def update(self, article: str):
print(f"Получили: {article}")
Плюс: просто. Минус: подписчик получает только то, что решил передать издатель.
📥 Pull-модель
Издатель передаёт ссылку на себя, подписчик берёт нужное:
def notify(self):
for obs in self._observers:
obs.update(self) # self = весь издатель
def update(self, subject):
# Берём только что нужно
data = subject.latest_article
Плюс: гибкость — подписчик сам решает, что взять. Минус: чуть сложнее.
В нашем примере мы использовали pull-модель — передаём subject, и каждый подписчик сам берёт нужные данные. Это более гибкий подход: один подписчик возьмёт latest_article, другой — author, третий — published_at, не меняя сигнатуру notify().
🌍 Observer в реальном мире
Паттерн Observer — один из самых распространённых. Вы уже используете его, возможно не зная об этом:
Django Signals
post_save, pre_delete — модели Django уведомляют обработчики-сигналы. Чистый Observer под капотом.
События браузера
addEventListener('click', handler) — кнопка (Subject) уведомляет обработчики (Observers) при нажатии.
RxJS (JavaScript)
Observable/Observer — Observer в чистом виде с поддержкой асинхронности. Используется в Angular.
Qt (PyQt / PySide)
Сигналы и слоты в Qt — это Observer. button.clicked.connect(handler) — та же идея.
🦅 Django Signals — Observer в Django
Когда пользователь сохраняет профиль, Django отправляет сигнал post_save. Вы можете «подписать» любую функцию на этот сигнал:
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
print(f"Новый пользователь: {instance.username}. Отправляем приветствие!")
# User (Subject) автоматически вызовет send_welcome_email (Observer)
# после каждого сохранения — без изменения кода модели User
Видите? Модель User не знает о функции send_welcome_email. Она просто сигналит «я сохранена», а зарегистрированные обработчики реагируют. Это Observer в действии в реальном Django-проекте.
🎯 Когда применять Observer?
✅ Используйте Observer, когда:
- Изменение одного объекта должно автоматически обновлять другие
- Количество реагирующих объектов заранее неизвестно
- Объекты слабо связаны — издатель не знает об конкретных подписчиках
- Нужна система событий: UI-кнопки, сигналы, pub/sub
⚠️ Будьте осторожны:
- Цепочки уведомлений: Observer А уведомляет B, B уведомляет C — может быть сложно отследить
- Порядок уведомления подписчиков не гарантирован (если не задать явно)
- Забытые подписчики: если не вызвать
detach(), объект не удалится из памяти
🧹 Важно про утечки памяти: если объект-подписчик добавлен через attach(), но вы удалили переменную, указывающую на него — объект всё равно не удалится! Список self._observers хранит ссылку на него. Всегда вызывайте detach(), когда подписчик больше не нужен.
🎯 Итоги урока
👁 Суть паттерна
Издатель (Subject) хранит список подписчиков (Observer) и уведомляет их при изменениях. Слабая связь — издатель не знает конкретных классов подписчиков.
🔧 Три метода Subject
attach() — подписать, detach() — отписать, notify() — уведомить всех через update().
📤 Push vs Pull
Push — данные передаются в update(data). Pull — передаётся subject, подписчик берёт нужное сам. Pull гибче.
🌍 Где встречается
Django Signals, Qt сигналы, addEventListener в JS, RxJS, pub/sub системы — всё это Observer под разными именами.
✨ Главный выигрыш
Добавить нового подписчика — написать новый класс и вызвать attach(). Код издателя не трогаем вообще.
⚠️ Осторожно
Не забывай вызывать detach() — иначе подписчик «зависнет» в памяти. Следи за цепочками уведомлений.