Factory — Фабрика
Представьте: вы пишете интернет-магазин, который принимает оплату через Stripe, PayPal и банковскую карту. В зависимости от выбора пользователя нужно создавать разные объекты платёжных систем. Делать это прямо в бизнес-логике — плохая идея. Паттерн Factory как раз решает эту проблему: он берёт на себя ответственность за создание объектов.
😤 Проблема: создание объектов разбросано по всему коду
Давайте начнём с боли. Допустим, вы пишете систему оплаты. У вас есть три класса: StripePayment, PayPalPayment и CardPayment. Когда пользователь выбирает способ оплаты, вам нужно создать нужный объект.
Самый очевидный способ — написать if-elif прямо там, где нужен объект:
class StripePayment:
def pay(self, amount):
print(f"Оплата через Stripe: {amount} руб.")
class PayPalPayment:
def pay(self, amount):
print(f"Оплата через PayPal: {amount} руб.")
class CardPayment:
def pay(self, amount):
print(f"Оплата картой: {amount} руб.")
# Вот как это выглядит в бизнес-логике:
def checkout(cart, payment_method):
# 🔴 ПЛОХО: создание объектов прямо в бизнес-логике
if payment_method == "stripe":
payment = StripePayment()
elif payment_method == "paypal":
payment = PayPalPayment()
elif payment_method == "card":
payment = CardPayment()
else:
raise ValueError(f"Неизвестный метод оплаты: {payment_method}")
total = sum(item.price for item in cart)
payment.pay(total)
print("Заказ оформлен!")
checkout(cart, "stripe")
🔥 В чём проблема?
- Функция checkout знает о конкретных классах — StripePayment, PayPalPayment, CardPayment. Это лишняя ответственность.
- Добавить новый метод оплаты = менять бизнес-логику. Добавили CryptoPayment? Нужно лезть в checkout и дописывать elif.
- Такой if-elif повторится в каждом месте, где создаётся объект оплаты. Три страницы, три одинаковых блока — кошмар поддержки.
Идея паттерна Factory проста: вынести создание объектов в отдельное место. Пусть за это отвечает специальная функция или метод — «фабрика». Всё остальное работает только с готовым объектом, не зная как он был создан.
🏭 Простая фабричная функция (Factory Function)
Самый доступный способ — написать обычную функцию, которая принимает параметр и возвращает нужный объект. Это называется Simple Factory (или просто фабричная функция). Технически это не «настоящий» GoF-паттерн, но это первый шаг к пониманию идеи.
Вот как выглядит решение той же задачи с фабричной функцией:
class StripePayment:
def pay(self, amount):
print(f"Оплата через Stripe: {amount} руб.")
class PayPalPayment:
def pay(self, amount):
print(f"Оплата через PayPal: {amount} руб.")
class CardPayment:
def pay(self, amount):
print(f"Оплата картой: {amount} руб.")
# ✅ ФАБРИЧНАЯ ФУНКЦИЯ — всё создание объектов здесь
def create_payment(method: str):
"""
Принимает строку с названием метода оплаты,
возвращает готовый объект нужного класса.
"""
payments = {
"stripe": StripePayment,
"paypal": PayPalPayment,
"card": CardPayment,
}
payment_class = payments.get(method)
if payment_class is None:
raise ValueError(f"Неизвестный метод оплаты: {method}")
return payment_class() # создаём и возвращаем объект
# Бизнес-логика больше НЕ знает о конкретных классах!
def checkout(cart, payment_method):
payment = create_payment(payment_method) # просто вызываем фабрику
total = sum(item.price for item in cart)
payment.pay(total)
print("Заказ оформлен!")
# Использование
p1 = create_payment("stripe")
p2 = create_payment("paypal")
p3 = create_payment("card")
p1.pay(1500) # Оплата через Stripe: 1500 руб.
p2.pay(2000) # Оплата через PayPal: 2000 руб.
🔍 Что изменилось?
- Функция
checkoutтеперь не знает оStripePayment,PayPalPaymentиCardPayment— она работает только с результатом фабрики. - Весь if-elif заменён на словарь — добавить новый тип гораздо проще.
- Фабрика — единственное место, где упоминаются конкретные классы. Остальной код про них ничего не знает.
Обратите внимание на хитрость со словарём. Мы написали:
payments = {
"stripe": StripePayment, # НЕ StripePayment() — класс без скобок!
...
}
payment_class = payments.get(method)
return payment_class() # скобки здесь — создаём объект
💡 Ключевая идея: в Python классы — это тоже объекты! Их можно хранить в словарях, списках, переменных и вызывать позже. Это делает паттерн Factory очень удобным для реализации.
🤝 Почему это работает: общий интерфейс
Заметили, что код работает с объектом payment через метод pay() — независимо от того, какой класс был создан? Это возможно потому, что все три класса имеют одинаковый интерфейс — один и тот же набор методов.
В Python интерфейс можно явно выразить через базовый класс или просто договориться «все классы должны иметь метод pay». Правильный способ — использовать базовый класс:
from abc import ABC, abstractmethod
class BasePayment(ABC):
"""Базовый класс — описывает ИНТЕРФЕЙС платёжной системы."""
@abstractmethod
def pay(self, amount: float) -> None:
"""Провести оплату на указанную сумму."""
pass
@abstractmethod
def refund(self, amount: float) -> None:
"""Вернуть деньги."""
pass
class StripePayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[Stripe] Списываем {amount} руб. с карты.")
def refund(self, amount: float) -> None:
print(f"[Stripe] Возвращаем {amount} руб. на карту.")
class PayPalPayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[PayPal] Списываем {amount} руб. с аккаунта.")
def refund(self, amount: float) -> None:
print(f"[PayPal] Возвращаем {amount} руб. на аккаунт.")
class CardPayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[Card] Списываем {amount} руб. с карты.")
def refund(self, amount: float) -> None:
print(f"[Card] Возвращаем {amount} руб. на карту.")
def create_payment(method: str) -> BasePayment:
payments = {
"stripe": StripePayment,
"paypal": PayPalPayment,
"card": CardPayment,
}
cls = payments.get(method)
if cls is None:
raise ValueError(f"Неизвестный метод оплаты: '{method}'")
return cls()
✅ Теперь всё честно: функция возвращает объект типа BasePayment. Вызывающий код работает только через методы pay() и refund() — он вообще не обязан знать, Stripe это, PayPal или что-то ещё. Это и называется «клиент работает с интерфейсом, а не с конкретными классами».
🔩 Factory Method — фабрика как метод класса
Factory Method — это уже «настоящий» паттерн из книги «Банды четырёх». Идея: вместо отдельной функции, логика создания объекта выносится в метод класса. Обычно это @staticmethod или @classmethod.
Зачем это нужно? Иногда логика создания объекта сложнее простого «посмотри в словарь». Например, нужно проверить права доступа, подготовить конфигурацию или выбрать класс на основе нескольких условий — всё это удобно инкапсулировать в метод самого класса.
from abc import ABC, abstractmethod
class BasePayment(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
@abstractmethod
def refund(self, amount: float) -> None:
pass
# ✅ FACTORY METHOD — статический метод, создающий нужный объект-наследник
@staticmethod
def create(method: str) -> "BasePayment":
"""
Фабричный метод: принимает строку, возвращает
готовый объект нужного класса.
"""
registry = {
"stripe": StripePayment,
"paypal": PayPalPayment,
"card": CardPayment,
}
cls = registry.get(method)
if cls is None:
raise ValueError(f"Неизвестный метод оплаты: '{method}'")
return cls()
class StripePayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[Stripe] Списываем {amount} руб.")
def refund(self, amount: float) -> None:
print(f"[Stripe] Возврат {amount} руб.")
class PayPalPayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[PayPal] Списываем {amount} руб.")
def refund(self, amount: float) -> None:
print(f"[PayPal] Возврат {amount} руб.")
class CardPayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[Card] Списываем {amount} руб.")
def refund(self, amount: float) -> None:
print(f"[Card] Возврат {amount} руб.")
# Использование: клиент вызывает метод базового класса
payment = BasePayment.create("stripe")
payment.pay(3500) # [Stripe] Списываем 3500 руб.
payment.refund(500) # [Stripe] Возврат 500 руб.
# Тип объекта — StripePayment, но работаем через интерфейс BasePayment
print(type(payment)) # <class '__main__.StripePayment'>
print(isinstance(payment, BasePayment)) # True
@staticmethod
Метод не получает ни self, ни cls. Он просто принадлежит классу «по месту прописки», но не использует его состояние. Хорошо подходит для утилитарных фабрик.
@classmethod
Метод получает cls — ссылку на сам класс. Полезен, если фабрика при наследовании должна создавать объекты именно того класса, через который вызывается.
Вот пример с @classmethod — особенно удобен, когда у фабрики есть подклассы:
class BasePayment(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
@classmethod
def from_config(cls, config: dict) -> "BasePayment":
"""
Фабричный метод через @classmethod.
Создаёт объект на основе словаря конфигурации.
"""
method = config.get("method", "card")
currency = config.get("currency", "RUB")
print(f"Создаём платёж: {method}, валюта {currency}")
return cls.create(method)
@staticmethod
def create(method: str) -> "BasePayment":
# ... та же логика
pass
# Теперь можно передать словарь конфига:
config = {"method": "paypal", "currency": "USD"}
payment = BasePayment.from_config(config)
payment.pay(100)
✨ Главная сила фабрики: добавить новый тип — легко
Представьте, что бизнес решил добавить оплату криптовалютой. Что нужно изменить в коде?
Без фабрики — пришлось бы найти все if-elif во всём проекте и дописать новую ветку в каждом месте.
С фабрикой — меняем только одно место:
# Шаг 1: создаём новый класс
class CryptoPayment(BasePayment):
def pay(self, amount: float) -> None:
print(f"[Crypto] Отправляем {amount} руб. в биткоинах 🪙")
def refund(self, amount: float) -> None:
print(f"[Crypto] Возврат крипты на сумму {amount} руб.")
# Шаг 2: регистрируем в фабрике — один словарь, одна строчка
@staticmethod
def create(method: str) -> "BasePayment":
registry = {
"stripe": StripePayment,
"paypal": PayPalPayment,
"card": CardPayment,
"crypto": CryptoPayment, # <-- добавили одну строчку!
}
cls = registry.get(method)
if cls is None:
raise ValueError(f"Неизвестный метод оплаты: '{method}'")
return cls()
# Шаг 3: остальной код НЕ МЕНЯЕТСЯ вообще!
payment = BasePayment.create("crypto")
payment.pay(5000) # [Crypto] Отправляем 5000 руб. в биткоинах 🪙
🏆 Это называется «открыто/закрыто» (Open/Closed Principle)
Код открыт для расширения (можно добавить CryptoPayment) и закрыт для изменения (весь остальной код — checkout, бизнес-логика — не трогаем). Это один из ключевых принципов SOLID, и Factory помогает его соблюдать.
Сравните с миром без фабрики: добавить новый тип оплаты = потенциально сломать что-то в трёх разных местах. С фабрикой — написал класс, прописал в словарь, готово.
⚖️ if-elif в коде vs Фабрика: сравнение
Давайте наглядно сравним два подхода бок о бок:
❌ if-elif прямо в коде
- Бизнес-логика знает о конкретных классах
- Добавление нового типа = изменение нескольких функций
- Код дублируется в каждом месте создания
- Сложно тестировать — нужно менять зависимости
- Легко сломать что-то при правке
✅ Фабрика
- Клиентский код знает только об интерфейсе
- Добавление нового типа = только одно место
- Создание объектов централизовано
- Легко подменить реализацию для тестов
- Изменения изолированы — риск меньше
⚠️ Когда фабрика избыточна? Если у вас всего 2-3 класса и они редко меняются — простой if-elif вполне нормально. Паттерны — это инструменты, а не обязательные правила. Фабрика окупается, когда типов много или они часто добавляются.
🏗️ Abstract Factory — фабрика фабрик (знакомство)
Есть ещё более сложный вариант — Abstract Factory. Если Factory создаёт объекты одного семейства (разные платёжные системы), то Abstract Factory создаёт целые наборы связанных объектов.
Представьте: вы делаете интерфейс для Windows и macOS. На каждой платформе нужны Кнопка, Чекбокс и Поле ввода. Причём они должны быть согласованы — нельзя использовать кнопку от Windows и чекбокс от macOS.
# Abstract Factory: каждая фабрика создаёт ЦЕЛЫЙ НАБОР объектов
class GUIFactory(ABC):
@abstractmethod
def create_button(self): pass
@abstractmethod
def create_checkbox(self): pass
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacOSFactory(GUIFactory):
def create_button(self):
return MacOSButton()
def create_checkbox(self):
return MacOSCheckbox()
# Приложение получает фабрику нужной платформы и работает через неё
def build_ui(factory: GUIFactory):
btn = factory.create_button()
chk = factory.create_checkbox()
btn.render()
chk.render()
build_ui(WindowsFactory()) # всё Windows-стиля
build_ui(MacOSFactory()) # всё macOS-стиля
📌 Запомните формулу: Factory = создаёт один объект из нескольких вариантов. Abstract Factory = создаёт несколько объектов, но все они из одного «семейства». По сути, Abstract Factory — это несколько Factory, объединённых общим интерфейсом.
💻 Полный пример: фабрика платёжных систем
Соберём всё вместе — финальная, «боевая» версия кода с базовым классом, тремя реализациями и фабричным методом:
from abc import ABC, abstractmethod
class BasePayment(ABC):
"""Интерфейс платёжной системы."""
@abstractmethod
def pay(self, amount: float) -> bool:
"""Провести платёж. Возвращает True при успехе."""
pass
@abstractmethod
def refund(self, amount: float) -> bool:
"""Вернуть деньги. Возвращает True при успехе."""
pass
def __str__(self) -> str:
return self.__class__.__name__
# FACTORY METHOD
@staticmethod
def create(method: str) -> "BasePayment":
registry = {
"stripe": StripePayment,
"paypal": PayPalPayment,
"card": CardPayment,
}
payment_class = registry.get(method.lower())
if payment_class is None:
available = ", ".join(registry.keys())
raise ValueError(
f"Неизвестный метод '{method}'. Доступны: {available}"
)
return payment_class()
class StripePayment(BasePayment):
def pay(self, amount: float) -> bool:
print(f"[Stripe] ✓ Списано {amount:.2f} руб. с карты.")
return True
def refund(self, amount: float) -> bool:
print(f"[Stripe] ↩ Возврат {amount:.2f} руб. на карту.")
return True
class PayPalPayment(BasePayment):
def pay(self, amount: float) -> bool:
print(f"[PayPal] ✓ Списано {amount:.2f} руб. с аккаунта.")
return True
def refund(self, amount: float) -> bool:
print(f"[PayPal] ↩ Возврат {amount:.2f} руб. на аккаунт.")
return True
class CardPayment(BasePayment):
def pay(self, amount: float) -> bool:
print(f"[Card] ✓ Оплата {amount:.2f} руб. картой.")
return True
def refund(self, amount: float) -> bool:
print(f"[Card] ↩ Возврат {amount:.2f} руб. на карту.")
return True
# --- Бизнес-логика — не знает о конкретных классах ---
def process_order(order_amount: float, method: str) -> None:
"""Оформить заказ: создать платёж через фабрику и провести оплату."""
payment = BasePayment.create(method) # фабрика
print(f"Используем: {payment}")
success = payment.pay(order_amount)
if success:
print("✅ Заказ успешно оплачен!\n")
else:
print("❌ Оплата не прошла.\n")
# Тестируем
process_order(1499.99, "stripe")
process_order(2300.00, "paypal")
process_order(750.50, "card")
# Добавим новый тип без изменения process_order:
class CryptoPayment(BasePayment):
def pay(self, amount: float) -> bool:
print(f"[Crypto] 🪙 Отправлено {amount:.2f} руб. в крипте.")
return True
def refund(self, amount: float) -> bool:
print(f"[Crypto] 🪙 Возврат {amount:.2f} руб. крипты.")
return True
# Регистрируем (в реальном проекте это делается в самом методе create):
BasePayment.create.__func__ # просто демонстрация — в реальности обновляем словарь registry
✅ Функция process_order не знает ни о Stripe, ни о PayPal, ни о Card. Она работает только через интерфейс BasePayment. Добавить новый способ оплаты можно, не трогая эту функцию вообще.
🎯 Итоги урока
🏭 Зачем нужна фабрика
Centralizes object creation. Клиентский код работает с интерфейсом — не знает, какой конкретно класс создан.
⚙️ Simple Factory
Обычная функция create_payment(method). Принимает параметр — возвращает нужный объект. Словарь вместо if-elif.
🔩 Factory Method
Та же логика, но оформлена как @staticmethod или @classmethod базового класса. Удобно для наследования.
🏗️ Abstract Factory
Фабрика фабрик — создаёт целые семейства связанных объектов (кнопки + чекбоксы для Windows или macOS).
✨ Главный выигрыш
Добавить новый тип = написать новый класс + одна строка в фабрике. Остальной код не трогаем вообще.
⚠️ Когда не нужно
2-3 фиксированных класса, которые не меняются — простой if-elif достаточен. Не усложняй без нужды.