Практика: паттерны в реальном проекте
Мы изучили три паттерна по отдельности. Теперь соберём их вместе в одну работающую систему — сервис уведомлений. Singleton хранит настройки, Factory создаёт каналы доставки, Observer связывает подписчиков и новостной сервис. Это не учебный пример — это архитектура, которую используют в production.
🗺️ Архитектура проекта: что и зачем
Прежде чем писать код — нарисуем общую картину. В нашем мини-проекте три компонента, каждый решает свою задачу:
Singleton: AppConfig
Конфиг приложения. Один экземпляр на всю программу. Хранит название приложения, версию, дефолтный канал уведомлений.
Factory: NotificationFactory
Создаёт нужный тип уведомления по строке-ключу: "email", "sms", "push". Скрывает детали создания объектов.
Observer: NewsService
Издатель новостей. Пользователи подписываются на него, выбирая канал через Factory. При публикации — уведомляет всех подписчиков.
Как компоненты связаны между собой:
- AppConfig (Singleton) хранит глобальные настройки — например, дефолтный канал уведомлений
- NotificationFactory создаёт объект-уведомление по типу канала
- Пользователь указывает тип канала → Factory создаёт объект → объект подписывается на NewsService
- NewsService публикует новость → обходит всех подписчиков → каждый получает уведомление через свой канал
- Добавить новый канал (Telegram) — только один новый класс + одна строка в Factory. Остальное не трогаем.
💡 Почему три паттерна вместе? Каждый решает свою проблему. Singleton — «кто отвечает за конфиг». Factory — «как создавать нужные объекты». Observer — «как сообщать о событиях». Их комбинация — типичная архитектура уведомительных систем в реальных приложениях.
⚙️ Шаг 1: AppConfig — Singleton
Начнём с самого простого компонента. AppConfig — это глобальный конфиг приложения. Почему Singleton? Потому что настройки одни на всю программу. Если создать два экземпляра конфига, они могут разойтись — и тогда разные части программы будут работать с разными настройками. Singleton гарантирует: конфиг всегда один.
class AppConfig:
"""
Синглтон: конфиг приложения.
Один экземпляр на всю программу — настройки не расходятся.
"""
_instance = None # Хранит единственный экземпляр
def __new__(cls):
# Создаём экземпляр только если его ещё нет
if cls._instance is None:
cls._instance = super().__new__(cls)
# Инициализируем настройки один раз
cls._instance.app_name = "NewsPortal"
cls._instance.version = "1.0.0"
cls._instance.default_channel = "email" # дефолтный канал уведомлений
return cls._instance
def __repr__(self) -> str:
return f"AppConfig(app={self.app_name}, v{self.version})"
🔍 Разбор: почему именно так
_instance = None— атрибут класса, не экземпляра. Общий для всех.__new__вызывается до__init__. Именно здесь мы решаем: создавать новый объект или вернуть существующий.- Настройки ставятся через
cls._instance.attrпрямо в__new__— так они инициализируются ровно один раз. default_channel— именно это поле мы используем в Factory позже: пользователь может не указывать канал, и тогда возьмётся дефолтный.
Убедимся, что Singleton работает:
config1 = AppConfig()
config2 = AppConfig()
print(config1 is config2) # True — один и тот же объект
print(config1.app_name) # NewsPortal
print(config1.default_channel) # email
🏭 Шаг 2: Notification классы и Factory
Теперь создаём классы уведомлений и Factory, которая их производит. Каждый класс уведомления — это будущий подписчик Observer. Он умеет одно: принять и обработать уведомление.
Сначала — абстрактный базовый класс и три конкретных реализации:
from abc import ABC, abstractmethod
# ─────────────────────────────────────────────
# Базовый класс: интерфейс для всех уведомлений
# ─────────────────────────────────────────────
class BaseNotification(ABC):
"""
Абстрактный Observer — базовый класс уведомления.
Все каналы доставки наследуются от него.
"""
def __init__(self, recipient: str) -> None:
self.recipient = recipient # email, телефон, device_id — зависит от канала
@abstractmethod
def send(self, message: str) -> None:
"""Отправить уведомление получателю."""
pass
# Этот метод будет вызываться Observer-паттерном (см. Шаг 3)
def update(self, news_service) -> None:
"""Получить уведомление от NewsService (Observer-интерфейс)."""
self.send(news_service.latest_news)
# ─────────────────────────────────────────────
# Три конкретных канала доставки
# ─────────────────────────────────────────────
class EmailNotification(BaseNotification):
"""Email-уведомление: отправляем полный текст новости."""
def send(self, message: str) -> None:
print(f" 📧 [Email → {self.recipient}]: {message}")
class SmsNotification(BaseNotification):
"""SMS-уведомление: текст обрезаем до 50 символов."""
def send(self, message: str) -> None:
preview = message[:50] + "..." if len(message) > 50 else message
print(f" 📱 [SMS → {self.recipient}]: {preview}")
class PushNotification(BaseNotification):
"""Push-уведомление: короткий заголовок для мобильного приложения."""
def send(self, message: str) -> None:
print(f" 🔔 [Push → устройство {self.recipient}]: {message}")
💡 Ключевое решение: метод update()
Мы поместили метод update() прямо в BaseNotification. Это связывает два паттерна: каждый класс уведомления одновременно является Observer-ом. Он умеет принять сигнал от NewsService (update) и доставить его по нужному каналу (send). Разделение ответственности соблюдено: update — Observer-логика, send — логика доставки.
Теперь — фабрика. Она принимает строку-тип и создаёт нужный объект. Код, который использует уведомления, не знает о конкретных классах:
class NotificationFactory:
"""
Фабрика уведомлений.
Создаёт нужный канал доставки по строке-ключу.
Новый канал — одна строка в словаре _channels.
"""
_channels = {
"email": EmailNotification,
"sms": SmsNotification,
"push": PushNotification,
}
@classmethod
def create(cls, channel: str, recipient: str) -> BaseNotification:
"""
Создать объект уведомления.
channel — тип канала: 'email', 'sms', 'push'
recipient — адресат (email, телефон, device_id)
"""
NotificationClass = cls._channels.get(channel)
if NotificationClass is None:
available = ", ".join(cls._channels.keys())
raise ValueError(f"Неизвестный канал '{channel}'. Доступны: {available}")
return NotificationClass(recipient)
🔍 Почему словарь, а не if/elif?
Словарь _channels — это реестр каналов. Добавить новый тип = добавить одну строку в словарь. Если бы мы писали if channel == "email": ... elif channel == "sms": ... — каждый новый канал требовал бы редактирования цепочки условий. Словарный подход расширяем и читаем.
Проверим Factory в изоляции:
notif = NotificationFactory.create("email", "alice@example.com")
notif.send("Тестовое сообщение")
# 📧 [Email → alice@example.com]: Тестовое сообщение
notif2 = NotificationFactory.create("sms", "+7 900 000-00-00")
notif2.send("Привет от фабрики!")
# 📱 [SMS → +7 900 000-00-00]: Привет от фабрики!
👁 Шаг 3: Observer — NewsService и подписчики
NewsService — это издатель. Он хранит список подписчиков (объектов BaseNotification) и при публикации новости обходит их всех. Подписчики уже умеют принимать уведомления через метод update(), который мы добавили в базовый класс.
from typing import List
class NewsService:
"""
Observer-издатель: сервис новостей.
Хранит список подписчиков (BaseNotification) и уведомляет их при публикации.
"""
def __init__(self) -> None:
self._subscribers: List[BaseNotification] = []
self._latest_news: str = ""
# ── Управление подписчиками ───────────────────
def subscribe(self, notification: BaseNotification) -> None:
"""Добавить подписчика (объект уведомления)."""
if notification not in self._subscribers:
self._subscribers.append(notification)
print(f" [+] Подписан: {notification.__class__.__name__} → {notification.recipient}")
def unsubscribe(self, notification: BaseNotification) -> None:
"""Убрать подписчика."""
try:
self._subscribers.remove(notification)
print(f" [-] Отписан: {notification.__class__.__name__} → {notification.recipient}")
except ValueError:
pass
def _notify_all(self) -> None:
"""Уведомить всех подписчиков — вызвать update() у каждого."""
for subscriber in self._subscribers:
subscriber.update(self)
# ── Свойство для подписчиков (pull-модель) ────
@property
def latest_news(self) -> str:
"""Подписчики читают новость через это свойство."""
return self._latest_news
# ── Бизнес-логика ────────────────────────────
def publish(self, news: str) -> None:
"""Опубликовать новость и уведомить всех подписчиков."""
print(f"\n📰 Новость: «{news}»")
self._latest_news = news
self._notify_all()
✅ Обратите внимание
NewsServiceработает с типомBaseNotification— он не знает, email это или SMS. Это и есть суть Observer: слабая связь.- Метод
publish()сначала обновляет_latest_news, потом вызывает_notify_all(). Порядок важен: подписчики должны видеть уже новые данные. - Свойство
latest_news— pull-модель: подписчики сами вытягивают данные через ссылку на издателя.
🔗 Шаг 4: Связываем всё вместе
Все три компонента готовы. Теперь напишем сценарий использования — именно так архитектура «оживает»:
# ── Шаг 1: Получаем конфиг (Singleton) ───────
config = AppConfig()
print(f"Запуск: {config.app_name} v{config.version}")
print(f"Дефолтный канал: {config.default_channel}")
# ── Шаг 2: Создаём NewsService (Observer) ────
news_service = NewsService()
# ── Шаг 3: Создаём уведомления через Factory ─
# Пользователь Alice хочет email
alice_notif = NotificationFactory.create("email", "alice@example.com")
# Пользователь Bob хочет SMS
bob_notif = NotificationFactory.create("sms", "+7 900 123-45-67")
# Пользователь Carol хочет push
carol_notif = NotificationFactory.create("push", "device-xyz-999")
# Пользователь Dave не указал канал — берём дефолтный из AppConfig
dave_notif = NotificationFactory.create(config.default_channel, "dave@example.com")
# ── Шаг 4: Подписываем всех на NewsService ───
news_service.subscribe(alice_notif)
news_service.subscribe(bob_notif)
news_service.subscribe(carol_notif)
news_service.subscribe(dave_notif)
# ── Шаг 5: Публикуем новость ─────────────────
news_service.publish("Python 4.0 официально анонсирован!")
# ── Шаг 6: Bob отписывается ──────────────────
news_service.unsubscribe(bob_notif)
# ── Шаг 7: Публикуем ещё одну новость ────────
news_service.publish("Конференция PyCon 2026: даты объявлены")
Вывод программы:
Запуск: NewsPortal v1.0.0
Дефолтный канал: email
[+] Подписан: EmailNotification → alice@example.com
[+] Подписан: SmsNotification → +7 900 123-45-67
[+] Подписан: PushNotification → device-xyz-999
[+] Подписан: EmailNotification → dave@example.com
📰 Новость: «Python 4.0 официально анонсирован!»
📧 [Email → alice@example.com]: Python 4.0 официально анонсирован!
📱 [SMS → +7 900 123-45-67]: Python 4.0 официально анонсирован!
🔔 [Push → устройство device-xyz-999]: Python 4.0 официально анонсирован!
📧 [Email → dave@example.com]: Python 4.0 официально анонсирован!
[-] Отписан: SmsNotification → +7 900 123-45-67
📰 Новость: «Конференция PyCon 2026: даты объявлены»
📧 [Email → alice@example.com]: Конференция PyCon 2026: даты объявлены
🔔 [Push → устройство device-xyz-999]: Конференция PyCon 2026: даты объявлены
📧 [Email → dave@example.com]: Конференция PyCon 2026: даты объявлены
🏆 Что здесь происходит на самом деле
- AppConfig отдал дефолтный канал — и Factory использовала его для Dave без лишних условий в коде.
- Factory скрыла детали создания объектов — код подписки одинаков для всех типов уведомлений.
- Observer позаботился о доставке — NewsService не знает о типах уведомлений вообще. Он просто вызывает
update(). - После отписки Bob перестал получать новости — и мы не трогали код NewsService.
✈️ Шаг 5: Добавляем Telegram — минимальные изменения
Это самая важная демонстрация урока. Смотрите, насколько легко расширить систему — добавляем поддержку Telegram. Никакой рефакторинг, никаких изменений в существующем коде.
💡 Принцип открытости-закрытости (Open/Closed Principle): система открыта для расширения (можно добавлять новый функционал) и закрыта для изменения (существующий код не трогаем). Именно это мы сейчас покажем на практике.
Шаг 1 из 2: создаём новый класс — наследуем от BaseNotification:
# ── НОВЫЙ ФАЙЛ или ДОБАВЛЯЕМ В КОНЕЦ ФАЙЛА ───
class TelegramNotification(BaseNotification):
"""
Telegram-уведомление.
Новый канал = новый класс. Существующий код не трогаем.
"""
def send(self, message: str) -> None:
print(f" ✈️ [Telegram → @{self.recipient}]: {message}")
Шаг 2 из 2: регистрируем в фабрике — одна строка:
# В классе NotificationFactory в словаре _channels добавляем:
_channels = {
"email": EmailNotification,
"sms": SmsNotification,
"push": PushNotification,
"telegram": TelegramNotification, # ← ДОБАВИЛИ ТОЛЬКО ЭТО
}
Всё. Telegram готов к работе:
# Используем как обычно — Factory сама разберётся
tg_notif = NotificationFactory.create("telegram", "python_dev")
news_service.subscribe(tg_notif)
news_service.publish("Новый урок по Python уже в эфире!")
# ...
# ✈️ [Telegram → @python_dev]: Новый урок по Python уже в эфире!
✅ Что изменилось
- Добавлен 1 новый класс
- Добавлена 1 строка в словарь
- Итого: 2 строки изменений
🔒 Что не трогали
NewsService— не менялсяAppConfig— не менялсяBaseNotification— не менялся- Все существующие тесты проходят
💻 Финальный код: всё вместе
Собираем полный рабочий файл — скопируйте и запустите прямо сейчас:
from abc import ABC, abstractmethod
from typing import List
# ══════════════════════════════════════════════
# SINGLETON: AppConfig
# ══════════════════════════════════════════════
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.app_name = "NewsPortal"
cls._instance.version = "1.0.0"
cls._instance.default_channel = "email"
return cls._instance
# ══════════════════════════════════════════════
# FACTORY: уведомления
# ══════════════════════════════════════════════
class BaseNotification(ABC):
def __init__(self, recipient: str) -> None:
self.recipient = recipient
@abstractmethod
def send(self, message: str) -> None:
pass
def update(self, news_service) -> None:
self.send(news_service.latest_news)
class EmailNotification(BaseNotification):
def send(self, message: str) -> None:
print(f" 📧 [Email → {self.recipient}]: {message}")
class SmsNotification(BaseNotification):
def send(self, message: str) -> None:
preview = message[:50] + "..." if len(message) > 50 else message
print(f" 📱 [SMS → {self.recipient}]: {preview}")
class PushNotification(BaseNotification):
def send(self, message: str) -> None:
print(f" 🔔 [Push → {self.recipient}]: {message}")
class TelegramNotification(BaseNotification):
def send(self, message: str) -> None:
print(f" ✈️ [Telegram → @{self.recipient}]: {message}")
class NotificationFactory:
_channels = {
"email": EmailNotification,
"sms": SmsNotification,
"push": PushNotification,
"telegram": TelegramNotification,
}
@classmethod
def create(cls, channel: str, recipient: str) -> BaseNotification:
NotificationClass = cls._channels.get(channel)
if NotificationClass is None:
available = ", ".join(cls._channels.keys())
raise ValueError(f"Неизвестный канал '{channel}'. Доступны: {available}")
return NotificationClass(recipient)
# ══════════════════════════════════════════════
# OBSERVER: NewsService
# ══════════════════════════════════════════════
class NewsService:
def __init__(self) -> None:
self._subscribers: List[BaseNotification] = []
self._latest_news: str = ""
def subscribe(self, notification: BaseNotification) -> None:
if notification not in self._subscribers:
self._subscribers.append(notification)
print(f" [+] {notification.__class__.__name__} → {notification.recipient}")
def unsubscribe(self, notification: BaseNotification) -> None:
try:
self._subscribers.remove(notification)
print(f" [-] {notification.__class__.__name__} → {notification.recipient}")
except ValueError:
pass
def _notify_all(self) -> None:
for subscriber in self._subscribers:
subscriber.update(self)
@property
def latest_news(self) -> str:
return self._latest_news
def publish(self, news: str) -> None:
print(f"\n📰 «{news}»")
self._latest_news = news
self._notify_all()
# ══════════════════════════════════════════════
# ДЕМОНСТРАЦИЯ
# ══════════════════════════════════════════════
if __name__ == "__main__":
# Singleton: один конфиг
config = AppConfig()
print(f"=== {config.app_name} v{config.version} ===\n")
# Observer: сервис новостей
news = NewsService()
# Factory: создаём уведомления
alice = NotificationFactory.create("email", "alice@example.com")
bob = NotificationFactory.create("sms", "+7 900 123-45-67")
carol = NotificationFactory.create("push", "device-xyz")
dave = NotificationFactory.create(config.default_channel, "dave@example.com")
eva = NotificationFactory.create("telegram", "eva_dev")
# Подписываем всех
for user in [alice, bob, carol, dave, eva]:
news.subscribe(user)
# Первая новость — получат все пятеро
news.publish("Python 4.0 официально анонсирован!")
# Bob отписывается
news.unsubscribe(bob)
# Вторая новость — Bob уже не получит
news.publish("PyCon 2026: программа конференции опубликована")
🎯 Итоги урока
⚙️ Singleton в проекте
AppConfig — один экземпляр на всю программу. Хранит дефолтный канал, который Factory использует при создании уведомлений.
🏭 Factory в проекте
NotificationFactory скрывает детали создания. Добавить новый канал = 1 класс + 1 строка в словаре. Остальное не трогаем.
👁 Observer в проекте
NewsService не знает о типах уведомлений. Подписчики добавляются/убираются динамически. Издатель не меняется.
🔗 Связь паттернов
Singleton → конфиг для Factory. Factory → объекты для Observer. Observer → система событий. Каждый паттерн делает свою работу.
✨ Open/Closed принцип
Добавили Telegram: 1 новый класс + 1 строка. Не менялись NewsService, AppConfig, BaseNotification. Система расширяема без правки существующего кода.
🌍 В реальных проектах
Эта архитектура лежит в основе Celery/RabbitMQ задач, Django Signals, систем уведомлений в мобильных приложениях и микросервисов.