Паттерн Фабрика в Python

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

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

Паттерн Фабрика — это такая «кухня»: он отвечает за создание объектов, а вам нужно лишь сообщить ему, какой тип объекта вы хотите.

Зачем нужен паттерн Фабрика?

В программировании нам часто требуется создавать объекты. Если мы напрямую используемnewключевое слово или конструктор класса в коде, это приведёт к:

  • Высокая связанность кода: Код создания объектов тесно связан с конкретными классами
  • Сложно поддерживать: Когда нужно изменить или добавить новые типы объектов, приходится изменять код в нескольких местах
  • Нарушает принцип открытости/закрытости: Принцип открытости для расширения и закрытости для модификации нарушается

Паттерн «Фабрика» решает эти проблемы, инкапсулируя процесс создания объектов.


Три типа паттерна «Фабрика»

Паттерн «Фабрика» в основном делится на три типа; давайте разберём их на конкретных примерах.

1. Паттерн «Простая фабрика»

Простая фабрика — это самый базовый паттерн фабрики; он использует фабричный класс для создания объектов разных типов.

Базовая структура

Пример

from abc import ABC, abstractmethod

# Интерфейс продукта
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

# Конкретный продукт
class Dog(Animal):
    def speak(self):
        return "Гав!"

class Cat(Animal):
    def speak(self):
        return "Мяу!"

class Duck(Animal):
    def speak(self):
        return "Кря!"

# Простая фабрика
class AnimalFactory:
    @staticmethod
    def create_animal(animal_type):
        if animal_type == "собака":
            return Dog()
        elif animal_type == "кошка":
            return Cat()
        elif animal_type == "duck":
            return Duck()
        else:
            raise ValueError(f"Неизвестный тип животного: {animal_type}")

# Пример использования
def test_simple_factory():
    factory = AnimalFactory()
   
    dog = factory.create_animal("dog")
    cat = factory.create_animal("cat")
    duck = factory.create_animal("duck")
   
    print(dog.speak())  # Вывод: Гав!
    print(cat.speak())  # Вывод: Мяу!
    print(duck.speak()) # Вывод: Кря!

if __name__ == "__main__":
    test_simple_factory()

Преимущества и недостатки

Преимущества:

  • Отвязывает клиента от конкретных классов продуктов
  • Разделение обязанностей, легко поддерживать

Недостатки:

  • Добавление новых продуктов требует изменения класса фабрики, что нарушает принцип открытости/закрытости
  • Класс фабрики имеет слишком много обязанностей, что не соответствует принципу единственной ответственности

2. Паттерн «Фабричный метод»

Паттерн «Фабричный метод» решает проблемы паттерна «Простая фабрика», позволяя подклассам решать, какие объекты создавать.

Базовая структура

Пример

from abc import ABC, abstractmethod

# Интерфейс продукта
class Button(ABC):
    @abstractmethod
    def render(self):
        pass
   
    @abstractmethod
    def onClick(self):
        pass

# Конкретный продукт
class WindowsButton(Button):
    def render(self):
        return "Отрисовать кнопку в стиле Windows"
   
    def onClick(self):
        return "Кнопка Windows нажата"

class MacButton(Button):
    def render(self):
        return "Отрисовать кнопку в стиле Mac"
   
    def onClick(self):
        return "Нажата кнопка Mac"

# Абстрактный класс создателя
class Dialog(ABC):
    @abstractmethod
    def createButton(self) -> Button:
        pass
   
    def render(self):
        # Вызвать фабричный метод для создания продукта
        button = self.createButton()
        result = button.render()
        return result

# Конкретный создатель
class WindowsDialog(Dialog):
    def createButton(self) -> Button:
        return WindowsButton()

class MacDialog(Dialog):
    def createButton(self) -> Button:
        return MacButton()

# Пример использования
def test_factory_method():
    # Выберите конкретную фабрику на основе конфигурации
    config = "windows"  # Можно прочитать из файла конфигурации
   
    if config == "windows":
        dialog = WindowsDialog()
    else:
        dialog = MacDialog()
   
    result = dialog.render()
    print(result)

if __name__ == "__main__":
    test_factory_method()

Рабочий процесс паттерна «Фабричный метод»

3. Паттерн «Абстрактная фабрика»

Паттерн «Абстрактная фабрика» предоставляет интерфейс для создания семейств связанных или зависимых объектов без указания их конкретных классов.

Базовая структура

Пример

from abc import ABC, abstractmethod

# Абстрактный продукт A
class Button(ABC):
    @abstractmethod
    def paint(self):
        pass

# Абстрактный продукт B
class Checkbox(ABC):
    @abstractmethod
    def paint(self):
        pass

# Конкретный продукт A1
class WindowsButton(Button):
    def paint(self):
        return "Отрисовать кнопку Windows"

# Конкретный продукт A2
class MacButton(Button):
    def paint(self):
        return "Отрисовать кнопку Mac"

# Конкретный продукт B1
class WindowsCheckbox(Checkbox):
    def paint(self):
        return "Отрисовать чекбокс Windows"

# Конкретный продукт B2
class MacCheckbox(Checkbox):
    def paint(self):
        return "Отрисовать чекбокс Mac"

# Абстрактная фабрика
class GUIFactory(ABC):
    @abstractmethod
    def createButton(self) -> Button:
        pass
   
    @abstractmethod
    def createCheckbox(self) -> Checkbox:
        pass

# Конкретная фабрика 1
class WindowsFactory(GUIFactory):
    def createButton(self) -> Button:
        return WindowsButton()
   
    def createCheckbox(self) -> Checkbox:
        return WindowsCheckbox()

# Конкретная фабрика 2
class MacFactory(GUIFactory):
    def createButton(self) -> Button:
        return MacButton()
   
    def createCheckbox(self) -> Checkbox:
        return MacCheckbox()

# Клиентский код
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory
        self.button = None
        self.checkbox = None
   
    def createUI(self):
        self.button = self.factory.createButton()
        self.checkbox = self.factory.createCheckbox()
   
    def paint(self):
        result = []
        if self.button:
            result.append(self.button.paint())
        if self.checkbox:
            result.append(self.checkbox.paint())
        return "\n".join(result)

# Пример использования
def test_abstract_factory():
    # Выбор фабрики в зависимости от типа системы
    system_type = "windows"  # Может быть автоматически определён или прочитан из конфигурации
   
    if system_type == "windows":
        factory = WindowsFactory()
    else:
        factory = MacFactory()
   
    app = Application(factory)
    app.createUI()
    print(app.paint())

if __name__ == "__main__":
    test_abstract_factory()

Сравнение трёх фабричных паттернов

Характеристика Паттерн «Простая фабрика» Паттерн «Фабричный метод» Шаблон абстрактной фабрики
Сложность Низкая Средняя Высокая
Расширяемость Плохо ОК Очень хорошо
Применимые сценарии Несколько типов объектов Одно семейство продуктов Несколько связанных семейств продуктов
Принцип открытости/закрытости Нарушает Соответствует Соответствует
Зависимость Зависит от конкретных классов Зависит от абстрактных классов Зависит от абстрактных интерфейсов

Практические сценарии применения

Сценарий 1: Фабрика подключений к базе данных

Пример

from abc import ABC, abstractmethod
import sqlite3
import mysql.connector

# Интерфейс подключения к базе данных
class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass
   
    @abstractmethod
    def execute(self, query):
        pass

# Конкретное подключение к базе данных
class SQLiteConnection(DatabaseConnection):
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
   
    def connect(self):
        self.connection = sqlite3.connect(self.db_path)
        return self.connection
   
    def execute(self, query):
        if self.connection:
            cursor = self.connection.cursor()
            cursor.execute(query)
            return cursor.fetchall()

class MySQLConnection(DatabaseConnection):
    def __init__(self, host, user, password, база данных):
        self.host = host
        self.user = user
        self.password = password
        self.database = database
        self.connection = None
   
    def connect(self):
        self.connection = mysql.connector.connect(
            host=self.host,
            user=self.user,
            password=self.password,
            database=self.database
        )
        return self.connection
   
    def execute(self, query):
        if self.connection:
            cursor = self.connection.cursor()
            cursor.execute(query)
            return cursor.fetchall()

# Фабрика баз данных
class DatabaseFactory:
    @staticmethod
    def create_connection(db_type, **kwargs):
        if db_type == "sqlite":
            return SQLiteConnection(**kwargs)
        elif db_type == "mysql":
            return MySQLConnection(**kwargs)
        else:
            raise ValueError(f"Неподдерживаемый тип базы данных: {db_type}")

# Пример использования
def test_database_factory():
    # Создать подключение SQLite
    sqlite_conn = DatabaseFactory.create_connection(
        "sqlite",
        db_path="example.db"
    )
    sqlite_conn.connect()
   
    # Создание подключения MySQL
    mysql_conn = DatabaseFactory.create_connection(
        "mysql",
        host="localhost",
        user="root",
        password="password",
        database="test"
    )
    mysql_conn.connect()
   
    print("Подключение к базе данных успешно создано!")

if __name__ == "__main__":
    test_database_factory()

Сценарий 2: Фабрика логгеров

Пример

import logging
from abc import ABC, abstractmethod
import sys

# Интерфейс Logger
class Logger(ABC):
    @abstractmethod
    def info(self, message):
        pass
   
    @abstractmethod
    def error(self, message):
        pass
   
    @abstractmethod
    def debug(self, message):
        pass

# Консольный логгер
class ConsoleLogger(Logger):
    def info(self, message):
        print(f"INFO: {message}")
   
    def error(self, message):
        print(f"ERROR: {message}", file=sys.stderr)
   
    def debug(self, message):
        print(f"DEBUG: {message}")

# Файловый логгер
class FileLogger(Logger):
    def __init__(self, filename):
        self.filename = filename
   
    def info(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"INFO: {message}\n")
   
    def error(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"ERROR: {message}\n")
   
    def debug(self, message):
        with open(self.filename, 'a') as f:
            f.write(f"DEBUG: {message}\n")

# Фабрика логгеров
class LoggerFactory:
    @staticmethod
    def get_logger(logger_type, **kwargs):
        if logger_type == "console":
            return ConsoleLogger()
        elif logger_type == "file":
            return FileLogger(**kwargs)
        else:
            raise ValueError(f"Неподдерживаемый тип логгера: {logger_type}")

# Пример использования
def test_logger_factory():
    # Создать консольный логгер
    console_logger = LoggerFactory.get_logger("console")
    console_logger.info("Это информационное сообщение")
    console_logger.error("Это сообщение об ошибке")
   
    # Создать файловый логгер
    file_logger = LoggerFactory.get_logger("file", filename="app.log")
    file_logger.info("Информация записана в файл")
    file_logger.debug("Отладочная информация")

if __name__ == "__main__":
    test_logger_factory()

Лучшие практики и соображения

1. Когда использовать паттерн Фабрика?

Ситуации, подходящие для использования паттерна Фабрика:

  • Процесс создания объектов относительно сложен
  • Необходимо создавать разные объекты в зависимости от разных условий
  • Хотите отделить создание объектов от их использования
  • Система должна поддерживать несколько типов продуктов

Ситуации, где это не подходит:

  • Процесс создания объектов прост, просто используйте конструктор напрямую
  • Типов продуктов немного, и расширение маловероятно

2. Распространённые ошибки и как их избежать

Ошибка 1: Переусложнение

Пример

# Не рекомендуется: использовать сложную фабрику для простой ситуации
class SimpleObject:
    def __init__(self, name):
        self.name = name

# Переусложнённая фабрика
class SimpleObjectFactory:
    @staticmethod
    def create_simple_object(name):
        return SimpleObject(name)

# Рекомендуется: создавать напрямую
obj = SimpleObject("test")

Ошибка 2: у фабричного класса слишком много обязанностей

Пример

# Не рекомендуется: одна фабрика выполняет слишком много задач
class GodFactory:
    def create_user(self): ...
    def create_order(self): ...
    def create_product(self): ...
    def send_email(self): ...  # Это не создание объектов!

# Рекомендуется: разделять по ответственности
class UserFactory: ...
class OrderFactory: ...
class ProductFactory: ...

3. Интеграция с внедрением зависимостей

Паттерн «Фабрика» часто используется вместе с внедрением зависимостей (DI):

Пример

from abc import ABC, abstractmethod

# Интерфейс сервиса
class NotificationService(ABC):
    @abstractmethod
    def send(self, message):
        pass

# Конкретный сервис
class EmailService(NotificationService):
    def send(self, message):
        return f"Отправить email: {message}"

class SMSService(NotificationService):
    def send(self, message):
        return f"Отправить SMS: {message}"

# Фабрика
class NotificationFactory:
    @staticmethod
    def create_service(service_type):
        if service_type == "email":
            return EmailService()
        elif service_type == "sms":
            return SMSService()
        else:
            raise ValueError(f"Неизвестный тип сервиса: {service_type}")

# Класс, использующий внедрение зависимостей
class OrderProcessor:
    def __init__(self, notification_service: NotificationService):
        self.notification_service = notification_service
   
    def process_order(self, order):
        # Обработка логики заказа
        result = self.notification_service.send("Обработка заказа завершена")
        return result

# Использование
def main():
    # Создать сервис через фабрику
    notification_service = NotificationFactory.create_service("email")
   
    # Внедрить зависимость
    processor = OrderProcessor(notification_service)
    result = processor.process_order({"id": 1})
    print(result)

if __name__ == "__main__":
    main()

Практические задачи

Упражнение 1: Реализуйте фабрику фигур

Создайте фабрику фигур, поддерживающую создание Circle, Rectangle и Triangle. Каждая фигура должна иметь методы для вычисления площади и периметра.

Требования:

  • Используйте паттерн «Фабричный метод»
  • Каждый класс фигуры реализуетcalculate_area()иcalculate_perimeter()methods
  • Приведите примеры использования

Упражнение 2: Расширение фабрики баз данных

На основе предыдущего примера фабрики баз данных добавьте поддержку базы данных PostgreSQL.

Требования:

  • СоздайтеPostgreSQLConnectionКласс
  • Измените фабрику, чтобы поддержать новый тип базы данных
  • Убедитесь, что не нарушите существующий код

Упражнение 3: Фабрика, управляемая конфигурацией

Создайте систему, которая может динамически выбирать фабрику на основе файлов конфигурации.

Требования:

  • Читать конфигурацию из файлов JSON или YAML
  • Создавать соответствующие объекты на основе конфигурации
  • Поддерживать горячую перезагрузку конфигурации

Итог

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

  1. Снижение связанности: Клиенту не нужно знать детали создания конкретных продуктов.
  2. Повышение сопровождаемости: Логика создания управляется централизованно, что упрощает её изменение и расширение.
  3. Повышение гибкости: Новые типы продуктов можно легко добавлять.
  4. Повышение повторного использования кода: Логику создания можно повторно использовать в нескольких местах.

Не забудьте выбрать подходящий тип фабричного шаблона:

  • Простая фабрика: Подходит для сценариев с небольшим количеством типов продуктов и незначительными изменениями.
  • Фабричный метод: Подходит для сценариев, где необходимо расширять семейства продуктов.
  • Абстрактная фабрика: Подходит для сложных сценариев, требующих создания связанных семейств продуктов.

Правильно используя фабричный шаблон, вы можете писать более гибкий, сопровождаемый и расширяемый код на Python!

Другие расширения