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

Практика 10. Практическая работа 10. Структурные паттерны

Цель

  • Освоить структурные паттерны проектирования GoF: Adapter, Decorator, Facade, Composite.
  • Научиться компоновать объекты в более крупные структуры через композицию, а не наследование.
  • Понять разницу между паттерном Decorator и синтаксическими декораторами Python (@decorator).
  • Закрепить принципы SOLID (особенно OCP, LSP и «композиция вместо наследования») на практических задачах.

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

Структурные паттерны описывают, как из отдельных классов и объектов собирать более крупные структуры, сохраняя гибкость и эффективность.

  • Adapter (Адаптер) — преобразует интерфейс класса в другой интерфейс, ожидаемый клиентом. Позволяет работать вместе классам с несовместимыми интерфейсами. Применяется при интеграции с чужим/legacy-кодом, который нельзя или нежелательно менять.
  • Decorator (Декоратор) — динамически добавляет объекту новые обязанности, оборачивая его в объект-обёртку с тем же интерфейсом. Гибкая альтернатива наследованию: поведения комбинируются в произвольном порядке, не порождая классов на каждое сочетание. Это паттерн, не путать с @decorator.
  • Facade (Фасад) — предоставляет единый упрощённый интерфейс к сложной подсистеме из множества классов. Не скрывает подсистему полностью, а даёт удобный «вход» для типичного сценария.
  • Composite (Компоновщик) — объединяет объекты в древовидные структуры «часть–целое» и позволяет одинаково обрабатывать как отдельные объекты («листья»), так и их группы («узлы/контейнеры»).

Общая идея Adapter и Decorator — обёртка вокруг другого объекта. Разница в цели: Adapter меняет интерфейс, Decorator сохраняет интерфейс, но дополняет поведение. Теория и базовые примеры — в лекции 12 (раздел 4).


Задания

Используйте abc.ABC и @abstractmethod для фиксации контрактов. Указывайте аннотации типов. Полные решения приводить не нужно — реализуйте классы по требованиям и сопроводите краткими примерами использования.

Задание 1. Adapter — единый интерфейс аналитики

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

Требования:

  1. Целевой интерфейс AnalyticsService(ABC):
    • track_event(event_name: str, properties: dict) -> str
    • get_report(start_date: str, end_date: str) -> dict
  2. Существующие «чужие» классы — менять нельзя:
    class GoogleAnalytics:
    def send_hit(self, hit_type: str, params: dict) -> str:
    return f"GA hit: {hit_type} with {params}"
    def fetch_data(self, query: dict) -> list:
    return [{"metric": "views", "value": query.get("limit", 100)}]
    class YandexMetrika:
    def reachGoal(self, target: str, params: dict | None = None) -> str:
    return f"YM goal: {target}, params={params}"
    def get_stats(self, date_from: str, date_to: str) -> dict:
    return {"visits": 500, "period": f"{date_from}{date_to}"}
  3. Адаптеры GoogleAnalyticsAdapter(AnalyticsService) и YandexMetrikaAdapter(AnalyticsService) — хранят ссылку на адаптируемый объект (композиция) и приводят его методы к целевому интерфейсу.
  4. Клиентский код: функция track_all(services: list[AnalyticsService], event: str, props: dict) -> list[str] отправляет событие во все сервисы и возвращает список отчётов.

Проверка: клиентский код работает с обоими сервисами через единый интерфейс и не зависит от конкретных классов; исходные классы не изменены.


Задание 2. Decorator — конвейер обработки текста

Реализуйте паттерн Decorator (не синтаксис @decorator) для обработки текста.

Требования:

  1. Интерфейс TextProcessor(ABC) с методом process(text: str) -> str.
  2. Базовый компонент PlainTextProcessor — возвращает текст без изменений.
  3. Базовый класс декоратора TextProcessorDecorator(TextProcessor):
    • хранит _wrapped: TextProcessor;
    • делегирует process() обёрнутому объекту, добавляя своё поведение.
  4. Конкретные декораторы:
    • TrimDecorator — убирает пробелы по краям;
    • UpperCaseDecorator — переводит в верхний регистр;
    • CensorDecorator(wrapped, words: list[str]) — заменяет указанные слова на ***;
    • HtmlEscapeDecorator — экранирует символы <, >, &, ".

Ключевое свойство: декораторы комбинируются в произвольном порядке, образуя цепочку обёрток.

Пример:

processor = CensorDecorator(
UpperCaseDecorator(TrimDecorator(PlainTextProcessor())),
words=["СПАМ"],
)
processor.process(" Привет, это спам сообщение ")
# "ПРИВЕТ, ЭТО *** СООБЩЕНИЕ"

В выводе сравните этот подход с реализацией через @decorator: когда какой уместнее.


Задание 3. Composite — дерево файловой системы

Реализуйте паттерн Composite для иерархии «часть–целое».

Требования:

  1. Общий интерфейс FileSystemNode(ABC):
    • size() -> int — размер в байтах;
    • name — имя узла;
    • render(indent: int = 0) -> str — текстовое представление с отступами.
  2. Лист File(name: str, size: int) — хранит собственный размер.
  3. Контейнер Directory(name: str):
    • метод add(node: FileSystemNode) -> None (можно remove);
    • size() возвращает суммарный размер всех потомков (рекурсивно);
    • render() выводит дерево с вложенностью.
  4. Клиент работает с File и Directory одинаково — через FileSystemNode.

Дополнительно (по желанию): метод find(name: str) -> FileSystemNode | None для рекурсивного поиска узла по имени.

Проверка:

root = Directory("root")
root.add(File("a.txt", 100))
sub = Directory("docs"); sub.add(File("b.txt", 50))
root.add(sub)
assert root.size() == 150

Задание 4. Facade — фасад оформления заказа

Реализуйте паттерн Facade поверх нескольких подсистем интернет-магазина.

Требования:

  1. Подсистемы (отдельные классы со своими методами):
    • Inventorycheck(item_id: str, qty: int) -> bool (наличие на складе);
    • Paymentcharge(amount: float) -> str (списание средств);
    • Shippingschedule(item_id: str, address: str) -> str (доставка);
    • Notifiersend(message: str) -> str (уведомление клиента).
  2. Фасад OrderFacade:
    • создаёт/принимает подсистемы;
    • метод place_order(item_id: str, qty: int, amount: float, address: str) -> dict последовательно: проверяет наличие → проводит оплату → планирует доставку → отправляет уведомление и возвращает сводный результат.
    • при отсутствии товара заказ не оформляется (возврат отказа или исключение OrderError).
  3. Клиент вызывает один метод фасада, не зная о внутренних подсистемах.

Проверка: один вызов place_order(...) запускает весь сценарий; клиентский код не обращается к подсистемам напрямую.


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

Объедините паттерны в одной задаче — мини-фреймворк рендеринга UI-дерева.

Требования:

  1. Composite: интерфейс Widget(ABC) с методом render() -> str; лист Label, Button; контейнер Panel, агрегирующий дочерние виджеты.
  2. Decorator: BorderDecorator(widget) и PaddingDecorator(widget, n) — оборачивают любой Widget, дополняя его render() рамкой/отступами; их можно применять и к листу, и к контейнеру.
  3. Adapter: существует «чужой» класс LegacyWidget с методом draw() -> str — напишите LegacyWidgetAdapter(Widget), чтобы встроить его в дерево.
  4. Facade: класс UIRenderer с методом render_screen(root: Widget) -> str, скрывающий детали обхода дерева и сборки итоговой строки.

Проверка: дерево из панелей, кнопок, декорированных и адаптированных виджетов рендерится одним вызовом фасада; клиент работает только с интерфейсом Widget.


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

КритерийВесЧто проверяется
Задание 1 (Adapter)20%Целевой интерфейс, два адаптера, исходные классы не изменены, клиент не зависит от конкретных классов
Задание 2 (Decorator)20%Базовый компонент и базовый декоратор, ≥4 декоратора, произвольный порядок композиции
Задание 3 (Composite)20%Общий интерфейс листа и контейнера, рекурсивный size()/render(), единая обработка
Задание 4 (Facade)20%Подсистемы, один метод-сценарий, обработка отказа, клиент не трогает подсистемы
Качество кода20%Использование ABC, аннотации типов, читаемость, примеры/тесты использования

Бонус: до +15% за выполненное задание 5* (комбинация паттернов).

Снижение: за изменение «чужих» классов в задании 1, за подмену паттерна Decorator синтаксисом @decorator, за дублирование кода вместо композиции.


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

  1. Чем Adapter отличается от Decorator, если оба «оборачивают» другой объект?
  2. Почему в задании 1 нельзя менять исходные классы GoogleAnalytics и YandexMetrika? Какой принцип SOLID это иллюстрирует?
  3. Чем паттерн Decorator отличается от синтаксического декоратора Python @…? Когда какой уместнее?
  4. Почему порядок применения декораторов влияет на результат? Приведите пример.
  5. Что общего у листа (File) и контейнера (Directory) в паттерне Composite и зачем им единый интерфейс?
  6. Как Composite обеспечивает рекурсивный подсчёт размера дерева?
  7. Какую задачу решает Facade и что он не гарантирует (чего не делает)?
  8. Можно ли обратиться к подсистеме напрямую в обход фасада? Хорошо это или плохо?
  9. Почему структурные паттерны предпочитают композицию наследованию?
  10. К какой группе GoF относятся Adapter, Decorator, Facade и Composite и что объединяет эту группу?