Практика 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 — единый интерфейс аналитики
Реализуйте адаптеры для унификации интерфейсов нескольких систем аналитики.
Требования:
- Целевой интерфейс
AnalyticsService(ABC):track_event(event_name: str, properties: dict) -> strget_report(start_date: str, end_date: str) -> dict
- Существующие «чужие» классы — менять нельзя:
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}"}
- Адаптеры
GoogleAnalyticsAdapter(AnalyticsService)иYandexMetrikaAdapter(AnalyticsService)— хранят ссылку на адаптируемый объект (композиция) и приводят его методы к целевому интерфейсу. - Клиентский код: функция
track_all(services: list[AnalyticsService], event: str, props: dict) -> list[str]отправляет событие во все сервисы и возвращает список отчётов.
Проверка: клиентский код работает с обоими сервисами через единый интерфейс и не зависит от конкретных классов; исходные классы не изменены.
Задание 2. Decorator — конвейер обработки текста
Реализуйте паттерн Decorator (не синтаксис @decorator) для обработки текста.
Требования:
- Интерфейс
TextProcessor(ABC)с методомprocess(text: str) -> str. - Базовый компонент
PlainTextProcessor— возвращает текст без изменений. - Базовый класс декоратора
TextProcessorDecorator(TextProcessor):- хранит
_wrapped: TextProcessor; - делегирует
process()обёрнутому объекту, добавляя своё поведение.
- хранит
- Конкретные декораторы:
TrimDecorator— убирает пробелы по краям;UpperCaseDecorator— переводит в верхний регистр;CensorDecorator(wrapped, words: list[str])— заменяет указанные слова на***;HtmlEscapeDecorator— экранирует символы<,>,&,".
Ключевое свойство: декораторы комбинируются в произвольном порядке, образуя цепочку обёрток.
Пример:
processor = CensorDecorator( UpperCaseDecorator(TrimDecorator(PlainTextProcessor())), words=["СПАМ"],)processor.process(" Привет, это спам сообщение ")# "ПРИВЕТ, ЭТО *** СООБЩЕНИЕ"В выводе сравните этот подход с реализацией через @decorator: когда какой
уместнее.
Задание 3. Composite — дерево файловой системы
Реализуйте паттерн Composite для иерархии «часть–целое».
Требования:
- Общий интерфейс
FileSystemNode(ABC):size() -> int— размер в байтах;name— имя узла;render(indent: int = 0) -> str— текстовое представление с отступами.
- Лист
File(name: str, size: int)— хранит собственный размер. - Контейнер
Directory(name: str):- метод
add(node: FileSystemNode) -> None(можноremove); size()возвращает суммарный размер всех потомков (рекурсивно);render()выводит дерево с вложенностью.
- метод
- Клиент работает с
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 поверх нескольких подсистем интернет-магазина.
Требования:
- Подсистемы (отдельные классы со своими методами):
Inventory—check(item_id: str, qty: int) -> bool(наличие на складе);Payment—charge(amount: float) -> str(списание средств);Shipping—schedule(item_id: str, address: str) -> str(доставка);Notifier—send(message: str) -> str(уведомление клиента).
- Фасад
OrderFacade:- создаёт/принимает подсистемы;
- метод
place_order(item_id: str, qty: int, amount: float, address: str) -> dictпоследовательно: проверяет наличие → проводит оплату → планирует доставку → отправляет уведомление и возвращает сводный результат. - при отсутствии товара заказ не оформляется (возврат отказа или исключение
OrderError).
- Клиент вызывает один метод фасада, не зная о внутренних подсистемах.
Проверка: один вызов place_order(...) запускает весь сценарий; клиентский код
не обращается к подсистемам напрямую.
Задание 5*. Комбинация структурных паттернов (повышенной сложности)
Объедините паттерны в одной задаче — мини-фреймворк рендеринга UI-дерева.
Требования:
- Composite: интерфейс
Widget(ABC)с методомrender() -> str; листLabel,Button; контейнерPanel, агрегирующий дочерние виджеты. - Decorator:
BorderDecorator(widget)иPaddingDecorator(widget, n)— оборачивают любойWidget, дополняя егоrender()рамкой/отступами; их можно применять и к листу, и к контейнеру. - Adapter: существует «чужой» класс
LegacyWidgetс методомdraw() -> str— напишитеLegacyWidgetAdapter(Widget), чтобы встроить его в дерево. - 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, за дублирование кода вместо композиции.
Вопросы для самопроверки
- Чем Adapter отличается от Decorator, если оба «оборачивают» другой объект?
- Почему в задании 1 нельзя менять исходные классы
GoogleAnalyticsиYandexMetrika? Какой принцип SOLID это иллюстрирует? - Чем паттерн Decorator отличается от синтаксического декоратора Python
@…? Когда какой уместнее? - Почему порядок применения декораторов влияет на результат? Приведите пример.
- Что общего у листа (
File) и контейнера (Directory) в паттерне Composite и зачем им единый интерфейс? - Как Composite обеспечивает рекурсивный подсчёт размера дерева?
- Какую задачу решает Facade и что он не гарантирует (чего не делает)?
- Можно ли обратиться к подсистеме напрямую в обход фасада? Хорошо это или плохо?
- Почему структурные паттерны предпочитают композицию наследованию?
- К какой группе GoF относятся Adapter, Decorator, Facade и Composite и что объединяет эту группу?