Перейти к содержимому

Практика 9. Практическая работа 9. Порождающие паттерны

Цель

  • Освоить порождающие паттерны GoF: Factory Method, Abstract Factory, Singleton, Builder.
  • Научиться изолировать клиентский код от конкретных классов и логики инстанцирования.
  • Отработать fluent-интерфейс (цепочку вызовов) и роль Director в Builder.
  • Понять, когда паттерн оправдан, а когда уместнее «питоническая» альтернатива (функция, реестр, объект уровня модуля).
  • Закрепить принципы SOLID, прежде всего Open/Closed и программирование на уровне абстракций.

Краткая теория

  • Factory Method определяет интерфейс создания объекта, но решение о конкретном классе откладывается на подкласс. Создаёт один продукт.
  • Abstract Factory создаёт семейство связанных продуктов так, чтобы они использовались согласованно (например, виджеты одной платформы). Клиент работает только с абстрактными интерфейсами фабрики и продуктов.
  • Singleton гарантирует единственный экземпляр и глобальную точку доступа. Его часто критикуют как «глобальную переменную в обёртке»: он усложняет тесты и прячет зависимости. В Python модуль сам по себе одиночка (кэшируется в sys.modules).
  • Builder отделяет пошаговое конструирование сложного объекта от его представления. Удобен при множестве (часто необязательных) параметров. Обычно — fluent-интерфейс (return self); Director инкапсулирует типовые «рецепты» сборки.
  • Питонический взгляд. Первоклассные функции, словарь-реестр (имя → класс), декоратор регистрации и объект уровня модуля нередко заменяют тяжёлые иерархии фабрик и Singleton. Паттерн — ориентир, а не обязательная церемония. Подробные примеры и обсуждение — в lecture_12.md (разделы 3 и 6).

Задания

Задание 1. Factory Method — фабрика уведомлений

Реализуйте систему отправки уведомлений через фабричный метод.

Требования:

  1. Абстрактный класс Notification(ABC):
    • send(recipient: str, message: str) -> str — отправка (возвращает отчёт);
    • __repr__.
  2. Конкретные продукты: EmailNotification, SMSNotification, PushNotification.
  3. Абстрактный создатель NotificationFactory(ABC):
    • абстрактный фабричный метод create_notification() -> Notification;
    • конкретный метод notify(recipient, message) -> str — создаёт продукт и вызывает send (логика отправки не дублируется в подклассах).
  4. Конкретные фабрики: EmailNotificationFactory, SMSNotificationFactory, PushNotificationFactory.
  5. Функция get_factory(channel: str) -> NotificationFactory по строке ("email", "sms", "push") возвращает фабрику; при неизвестном канале — ValueError. Базовый каркас Notification/NotificationFactory см. в lecture_12.md, раздел 3.1.

Проверьте: get_factory("email").notify("user@example.com", "Привет!") содержит адрес и текст; get_factory("telegram") бросает ValueError.


Задание 2. Abstract Factory — кроссплатформенный UI

Реализуйте абстрактную фабрику элементов интерфейса под две платформы.

Требования:

  1. Абстрактные продукты:
    • Button(ABC): render() -> str, click() -> str;
    • TextInput(ABC): render() -> str, set_value(value: str) -> str;
    • Checkbox(ABC): render() -> str, toggle() -> str.
  2. Конкретные продукты двух семейств: Windows* и Mac* (по три класса).
  3. Абстрактная фабрика UIFactory(ABC):
    • create_button(label) -> Button;
    • create_text_input(placeholder) -> TextInput;
    • create_checkbox(label) -> Checkbox.
  4. Конкретные фабрики WindowsUIFactory, MacUIFactory.
  5. Клиентский код build_form(factory: UIFactory) -> list[str] — собирает форму (кнопка, поле, чекбокс) и возвращает список render(). Клиент не должен упоминать конкретные классы продуктов.

Проверьте: одна и та же функция build_form с разными фабриками даёт согласованные «виджеты» одной платформы; продукты разных семейств не смешиваются. В комментарии поясните отличие Abstract Factory от Factory Method (семейство продуктов против одного).


Задание 3. Builder — конструктор SQL-запросов

Реализуйте Builder для пошагового построения SQL-подобных запросов с fluent-API.

Требования:

  1. Класс Query с атрибутами table, columns, conditions, order_by, limit_value; метод to_sql() -> str и __repr__.
  2. Класс QueryBuilder (каждый метод возвращает self):
    • select(*columns) -> QueryBuilder (по умолчанию *);
    • from_table(table) -> QueryBuilder;
    • where(condition) -> QueryBuilder — можно вызывать многократно, условия объединяются через AND;
    • order(column, direction="ASC") -> QueryBuilder;
    • limit(n) -> QueryBuilder;
    • build() -> Query — сборка; без from_table() бросает QueryBuildError.
  3. Класс QueryDirector(builder) с готовыми «рецептами»:
    • build_find_active_users() -> Query;
    • build_recent_orders(limit: int) -> Query.

Скелет:

class QueryBuilder:
def __init__(self) -> None:
self._table: str | None = None
self._columns: list[str] = []
self._conditions: list[str] = []
...
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self # fluent-интерфейс
def build(self) -> "Query":
if self._table is None:
raise QueryBuildError("Не указана таблица (from_table)")
...

Проверьте: цепочка select("name","email").from_table("users").where("active = 1").where("age > 18").order("name").limit(10).build() даёт SELECT name, email FROM users WHERE active = 1 AND age > 18 ORDER BY name ASC LIMIT 10; QueryBuilder().select("id").build() бросает QueryBuildError.


Задание 4. Singleton и его питоничные альтернативы

Реализуйте единый объект конфигурации тремя способами и сравните их.

Требования:

  1. Через метакласс SingletonMeta: класс Config с атрибутом settings: dict. Два вызова Config() должны давать один и тот же объект (a is b).
  2. Через __new__: класс Logger, кэширующий единственный экземпляр в атрибуте класса. Метод log(msg) накапливает сообщения в общий список.
  3. Питоничный способ: модуль-одиночка. Заведите функцию get_settings() -> dict, возвращающую один словарь уровня модуля (через @lru_cache или модульную переменную). Покажите, что повторный вызов отдаёт тот же объект.
  4. В комментарии перечислите минимум две проблемы Singleton (тестируемость, скрытые зависимости, многопоточность) и поясните, почему в Python чаще достаточно модуля. Образец SingletonMeta см. в lecture_12.md, раздел 3.4.

Проверьте: Config() is Config(); Logger() is Logger(); два вызова get_settings() возвращают идентичный объект (is).


Задание 5*. Реестр вместо иерархии фабрик (повышенной сложности)

Замените громоздкую иерархию фабрик из задания 1 на питоничный реестр.

Требования:

  1. Словарь-реестр REGISTRY: dict[str, type[Notification]].
  2. Декоратор регистрации @register("email"), который кладёт класс в реестр под именем и возвращает класс без изменений.
  3. Функция create(channel: str, **kwargs) -> Notification — создаёт продукт по имени; при неизвестном канале — ValueError со списком доступных каналов.
  4. Покажите, что добавление нового канала (@register("telegram")) не требует правки create и существующих классов (Open/Closed Principle).

Скелет:

REGISTRY: dict[str, type[Notification]] = {}
def register(name: str):
def deco(cls: type[Notification]) -> type[Notification]:
REGISTRY[name] = cls
return cls
return deco
@register("email")
class EmailNotification(Notification):
...

Сравните в коротком комментарии (3–5 строк) подход «иерархия фабрик» (задание 1) и «реестр + декоратор»: читаемость, объём кода, расширяемость, когда какой уместен.


Критерии оценки

КритерийВес
Задание 1: корректная иерархия, фабричный метод без дублирования, get_factory20%
Задание 2: два семейства, три продукта, клиент не зависит от конкретных классов20%
Задание 3: fluent-интерфейс, валидация (QueryBuildError), Director с рецептами25%
Задание 4: три способа Singleton, рабочая проверка is, разбор недостатков20%
Задание 5*: реестр, декоратор регистрации, расширяемость без правок10%
Качество кода: типизация, имена по ролям паттерна, ABC, docstrings5%

Штрафы: дублирование логики send в каждой фабрике (задание 1); смешивание продуктов разных семейств (задание 2); build() без валидации таблицы; «реестр», требующий ручной правки create при добавлении класса.


Вопросы для самопроверки

  1. В чём принципиальная разница между Factory Method и Abstract Factory?
  2. Почему метод notify логично размещать в базовом создателе, а не в подклассах?
  3. Что даёт return self в методах строителя и как это связано с fluent-интерфейсом?
  4. Зачем нужен Director, если строитель уже умеет собирать объект?
  5. Когда Builder оправдан, а когда достаточно конструктора с параметрами по умолчанию?
  6. Как работает Singleton через метакласс и через __new__? В чём различие?
  7. Почему Singleton часто называют антипаттерном и чем его заменяют в Python?
  8. Почему модуль в Python естественным образом ведёт себя как одиночка?
  9. Как реестр (имя → класс) с декоратором регистрации реализует принцип Open/Closed?
  10. Приведите по одному примеру, когда питоничная альтернатива (функция, реестр, модуль) предпочтительнее классической ООП-реализации порождающего паттерна.