Практика 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 — фабрика уведомлений
Реализуйте систему отправки уведомлений через фабричный метод.
Требования:
- Абстрактный класс
Notification(ABC):send(recipient: str, message: str) -> str— отправка (возвращает отчёт);__repr__.
- Конкретные продукты:
EmailNotification,SMSNotification,PushNotification. - Абстрактный создатель
NotificationFactory(ABC):- абстрактный фабричный метод
create_notification() -> Notification; - конкретный метод
notify(recipient, message) -> str— создаёт продукт и вызываетsend(логика отправки не дублируется в подклассах).
- абстрактный фабричный метод
- Конкретные фабрики:
EmailNotificationFactory,SMSNotificationFactory,PushNotificationFactory. - Функция
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
Реализуйте абстрактную фабрику элементов интерфейса под две платформы.
Требования:
- Абстрактные продукты:
Button(ABC):render() -> str,click() -> str;TextInput(ABC):render() -> str,set_value(value: str) -> str;Checkbox(ABC):render() -> str,toggle() -> str.
- Конкретные продукты двух семейств:
Windows*иMac*(по три класса). - Абстрактная фабрика
UIFactory(ABC):create_button(label) -> Button;create_text_input(placeholder) -> TextInput;create_checkbox(label) -> Checkbox.
- Конкретные фабрики
WindowsUIFactory,MacUIFactory. - Клиентский код
build_form(factory: UIFactory) -> list[str]— собирает форму (кнопка, поле, чекбокс) и возвращает списокrender(). Клиент не должен упоминать конкретные классы продуктов.
Проверьте: одна и та же функция build_form с разными фабриками даёт
согласованные «виджеты» одной платформы; продукты разных семейств не смешиваются.
В комментарии поясните отличие Abstract Factory от Factory Method (семейство
продуктов против одного).
Задание 3. Builder — конструктор SQL-запросов
Реализуйте Builder для пошагового построения SQL-подобных запросов с fluent-API.
Требования:
- Класс
Queryс атрибутамиtable,columns,conditions,order_by,limit_value; методto_sql() -> strи__repr__. - Класс
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.
- Класс
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 и его питоничные альтернативы
Реализуйте единый объект конфигурации тремя способами и сравните их.
Требования:
- Через метакласс
SingletonMeta: классConfigс атрибутомsettings: dict. Два вызоваConfig()должны давать один и тот же объект (a is b). - Через
__new__: классLogger, кэширующий единственный экземпляр в атрибуте класса. Методlog(msg)накапливает сообщения в общий список. - Питоничный способ: модуль-одиночка. Заведите функцию
get_settings() -> dict, возвращающую один словарь уровня модуля (через@lru_cacheили модульную переменную). Покажите, что повторный вызов отдаёт тот же объект. - В комментарии перечислите минимум две проблемы Singleton (тестируемость,
скрытые зависимости, многопоточность) и поясните, почему в Python чаще достаточно
модуля. Образец
SingletonMetaсм. вlecture_12.md, раздел 3.4.
Проверьте: Config() is Config(); Logger() is Logger(); два вызова
get_settings() возвращают идентичный объект (is).
Задание 5*. Реестр вместо иерархии фабрик (повышенной сложности)
Замените громоздкую иерархию фабрик из задания 1 на питоничный реестр.
Требования:
- Словарь-реестр
REGISTRY: dict[str, type[Notification]]. - Декоратор регистрации
@register("email"), который кладёт класс в реестр под именем и возвращает класс без изменений. - Функция
create(channel: str, **kwargs) -> Notification— создаёт продукт по имени; при неизвестном канале —ValueErrorсо списком доступных каналов. - Покажите, что добавление нового канала (
@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_factory | 20% |
| Задание 2: два семейства, три продукта, клиент не зависит от конкретных классов | 20% |
Задание 3: fluent-интерфейс, валидация (QueryBuildError), Director с рецептами | 25% |
Задание 4: три способа Singleton, рабочая проверка is, разбор недостатков | 20% |
| Задание 5*: реестр, декоратор регистрации, расширяемость без правок | 10% |
Качество кода: типизация, имена по ролям паттерна, ABC, docstrings | 5% |
Штрафы: дублирование логики send в каждой фабрике (задание 1); смешивание
продуктов разных семейств (задание 2); build() без валидации таблицы; «реестр»,
требующий ручной правки create при добавлении класса.
Вопросы для самопроверки
- В чём принципиальная разница между Factory Method и Abstract Factory?
- Почему метод
notifyлогично размещать в базовом создателе, а не в подклассах? - Что даёт
return selfв методах строителя и как это связано с fluent-интерфейсом? - Зачем нужен Director, если строитель уже умеет собирать объект?
- Когда Builder оправдан, а когда достаточно конструктора с параметрами по умолчанию?
- Как работает Singleton через метакласс и через
__new__? В чём различие? - Почему Singleton часто называют антипаттерном и чем его заменяют в Python?
- Почему модуль в Python естественным образом ведёт себя как одиночка?
- Как реестр (
имя → класс) с декоратором регистрации реализует принцип Open/Closed? - Приведите по одному примеру, когда питоничная альтернатива (функция, реестр, модуль) предпочтительнее классической ООП-реализации порождающего паттерна.