$ sudo teach IT
МОДУЛЬ 11 · УРОК 2

Factory — Фабрика

Представьте: вы пишете интернет-магазин, который принимает оплату через Stripe, PayPal и банковскую карту. В зависимости от выбора пользователя нужно создавать разные объекты платёжных систем. Делать это прямо в бизнес-логике — плохая идея. Паттерн Factory как раз решает эту проблему: он берёт на себя ответственность за создание объектов.

⏱ ~40 минут 🎓 Для новичков 🐍 Python 3 🏭 Паттерны проектирования

😤 Проблема: создание объектов разбросано по всему коду

Давайте начнём с боли. Допустим, вы пишете систему оплаты. У вас есть три класса: 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 достаточен. Не усложняй без нужды.