Найти в Дзене

Эффективное ООП на Python: Разбираем Singleton, Factory и Observer на примерах

Привет, коллеги! Когда проект на Python перерастает размеры "скрипта для себя", код неизбежно начинает пухнуть. Появляется путаница, одни и те же проблемы приходится решать по несколько раз, и правка одного модуля ломает три других. В мире программирования для этого придумали паттерны проектирования. Это не готовый код, а шаблоны мышления — проверенные способы решать типовые задачи так, чтобы код оставался гибким и понятным . Особенность Python в том, что многие паттерны здесь реализуются проще и элегантнее, чем в строгих Java или C#. Сегодня разберем три самых популярных и полезных паттерна с реальными примерами кода.
Представьте, что вы строите дом. Можно лепить стены из глины, а можно использовать типовые решения: фундамент, несущие балки, перекрытия. Паттерны — это такие же стандартизированные "архитектурные решения", только для кода . Паттерны делятся на три большие группы : Мы возьмем по одному хиту из каждой категории.
Тип: Порождающий.
Суть: Гарантирует, что у класса есть
Оглавление

Привет, коллеги!

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

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

Особенность Python в том, что многие паттерны здесь реализуются проще и элегантнее, чем в строгих Java или C#. Сегодня разберем три самых популярных и полезных паттерна с реальными примерами кода.

Что такое паттерны проектирования (и зачем они нужны)

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

Паттерны делятся на три большие группы :

  1. Порождающие (как создавать объекты)
  2. Структурные (как строить связи между классами)
  3. Поведенческие (как объектам общаться между собой)

Мы возьмем по одному хиту из каждой категории.

-2

1. Паттерн Singleton (Одиночка)

Тип: Порождающий.
Суть: Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа .

Где пригодится:

  • Подключение к базе данных (чтобы не открывать 100500 соединений)
  • Класс с настройками приложения (логично иметь один объект конфига)
  • Логгер (пишем всё в один файл из одного места)

Как это работает в Python

В Python есть лайфхак: модули сами по себе являются синглтонами. Но если нужен именно класс — используем метаклассы.

Пример: Менеджер подключения к БД

class SingletonMeta(type):
"""Метакласс для реализации паттерна Одиночка"""
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# Если экземпляра еще нет - создаем
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]

class DatabaseConnection(metaclass=SingletonMeta):
"""Наш "единственный" класс для работы с БД"""
def __init__(self):
self.connection = self.connect_to_db()
print("Создаю новое подключение к БД")

def connect_to_db(self):
# Здесь реальная логика подключения
return "Connection object"

# Проверяем
db1 = DatabaseConnection()
db2 = DatabaseConnection()

print(db1 is db2) # Выведет: True (это один и тот же объект!)
print(id(db1), id(db2)) # ID объектов совпадают

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

-3

2. Паттерн Factory (Фабрика)

Тип: Порождающий.
Суть: Создает объекты, не привязывая вызывающий код к конкретным классам. Вместо if-elif-else по всему проекту, мы делегируем создание объектов фабрике .

Где пригодится:

  • Приложение должно работать с разными типами уведомлений (email, SMS, push)
  • Система платежей (разные платежные шлюзы)
  • Парсеры файлов (в зависимости от расширения)

Простая фабрика на практике

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

from abc import ABC, abstractmethod

# 1. Создаем абстрактный класс (интерфейс) для всех уведомлений
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass

# 2. Реализуем конкретные виды уведомлений
class EmailNotification(Notification):
def send(self, message: str):
print(f"📧 Отправляем email: {message}")
# Здесь логика отправки email

class SMSNotification(Notification):
def send(self, message: str):
print(f"📱 Отправляем SMS: {message}")
# Здесь логика отправки SMS

class PushNotification(Notification):
def send(self, message: str):
print(f"🔔 Отправляем Push-уведомление: {message}")

# 3. Создаем Фабрику
class NotificationFactory:
@staticmethod
def create_notification(notification_type: str) -> Notification:
"""Фабричный метод, который возвращает нужный объект"""
notifications = {
"email": EmailNotification(),
"sms": SMSNotification(),
"push": PushNotification()
}
notification = notifications.get(notification_type.lower())
if not notification:
raise ValueError(f"Неизвестный тип уведомления: {notification_type}")
return notification

# 4. Используем
if __name__ == "__main__":
# Вызывающий код ничего не знает про классы EmailNotification и т.д.
# Он просто просит фабрику дать ему уведомление по имени
notif = NotificationFactory.create_notification("email")
notif.send("Ваш заказ принят!")

notif2 = NotificationFactory.create_notification("sms")
notif2.send("Курьер выехал")

Что мы выиграли?
Если завтра появится Telegram-уведомление, мы просто добавим класс TelegramNotification и пропишем его в словаре фабрики. Код, который отправляет уведомления (main), вообще не изменится! Это соблюдение принципа открытости/закрытости (OCP).

-4

3. Паттерн Observer (Наблюдатель)

Тип: Поведенческий.
Суть: Создает механизм подписки, позволяющий одним объектам (издателям) следить за изменениями других объектов (подписчиков) . Классика событийно-ориентированного программирования.

Где пригодится:

  • Системы мониторинга (датчик температуры обновляет график, логи и включает сигналку)
  • Обработка событий в GUI (кнопка нажата — все слушатели узнали)
  • Торговые роботы (изменение цены вызывает цепочку действий)

Реализация "издатель-подписчик"

Смоделируем датчик температуры, на который подписаны Логгер и Сигнализация.

import time
import random

# 1. Базовый класс "Издатель" (Subject)
class Subject:
"""Тот, за кем следят (наблюдаемый объект)"""
def __init__(self):
self._observers = [] # Список подписчиков

def attach(self, observer):
"""Подписать наблюдателя"""
if observer not in self._observers:
self._observers.append(observer)

def detach(self, observer):
"""Отписать наблюдателя"""
try:
self._observers.remove(observer)
except ValueError:
pass

def notify(self, data):
"""Уведомить всех подписчиков о новых данных"""
for observer in self._observers:
observer.update(data)

# 2. Конкретный издатель (Датчик температуры)
class TemperatureSensor(Subject):
def run_simulation(self):
"""Симуляция работы датчика"""
while True:
temp = random.randint(15, 35) # Эмулируем случайную температуру
print(f"\n[Датчик] Новая температура: {temp}°C")
self.notify(temp) # Оповещаем всех подписчиков!
time.sleep(3) # Новые данные каждые 3 секунды

# 3. Интерфейс для Наблюдателей (не обязателен, но желателен)
class Observer(ABC):
@abstractmethod
def update(self, data):
pass

# 4. Конкретные Наблюдатели (подписчики)
class Logger(Observer):
def update(self, temp):
print(f"[Логгер] Записываем в лог: {temp}°C")

class Alarm(Observer):
def update(self, temp):
if temp > 30:
print(f"[Сигнализация] 🚨 ВНИМАНИЕ! Перегрев: {temp}°C!")

class Display(Observer):
def update(self, temp):
print(f"[Дисплей] Показываем на экране: {temp}°C")

# 5. Запуск
if __name__ == "__main__":
sensor = TemperatureSensor()

# Создаем наблюдателей
logger = Logger()
alarm = Alarm()
display = Display()

# Подписываем их на датчик
sensor.attach(logger)
sensor.attach(alarm)
sensor.attach(display)

# Запускаем мониторинг (в реальном проекте тут был бы основной цикл)
try:
sensor.run_simulation()
except KeyboardInterrupt:
print("\nМониторинг остановлен")

Теперь наш датчик температуры ничего не знает о том, что с данными будут делать. Можно добавить новый класс TelegramBotNotifier, подписать его — и система заработает без изменения кода датчика. Это и есть слабая связанность (low coupling).

Почему это стоит использовать?

  1. Переиспользование. Вы не изобретаете велосипед. Тысячи умных людей до вас уже придумали, как решать проблему .
  2. Коммуникация. Сказать "тут мы применили Фабрику" — это сказать очень много. Другой разработчик сразу поймет архитектуру .
  3. Надежность. Паттерны проверены временем. Они учитывают граничные случаи, о которых вы можете не подумать.

Итоги

Мы разобрали три кита, на которых держится множество приложений:

  • Singleton — для уникальных объектов (конфиги, подключения).
  • Factory — для гибкого создания семейств объектов.
  • Observer — для реактивных систем и обработки событий.

Это база, с которой стоит начать погружение в мир паттернов. А если хотите копнуть вглубь — обратите внимание на книгу Гарри Персиваля "Паттерны разработки на Python", где разбираются более сложные штуки вроде чистой архитектуры и DDD .

А вы используете паттерны в своих проектах? Или считаете, что Python и без них хорош? Делитесь мнением в комментариях — поспорим! 👇