Лекция 12. Паттерны проектирования (GoF)
1. Введение: зачем нужны паттерны
Паттерн проектирования — это типовое, проверенное временем решение часто встречающейся задачи проектирования в определённом контексте. Паттерн — это не готовый код, который можно скопировать, а идея, описание ролей объектов и схемы их взаимодействия. Один и тот же паттерн в разных проектах выглядит по-разному, но сохраняет узнаваемую структуру.
Термин пришёл из книги «Design Patterns: Elements of Reusable Object-Oriented Software» (1994), написанной четырьмя авторами — Гаммой, Хелмом, Джонсоном и Влиссидесом. Их часто называют «Бандой четырёх» (Gang of Four, GoF), а 23 описанных ими паттерна — каталогом GoF.
Зачем они нужны:
- Общий словарь. Сказать «здесь используется Observer» короче и точнее, чем описывать схему подписки и оповещения словами.
- Готовые решения. Не нужно изобретать структуру заново — её уже продумали.
- Качество дизайна. Паттерны воплощают принципы SOLID: открытость для расширения, программирование на уровне абстракций, композицию вместо наследования.
Важное предостережение: паттерн — не самоцель. Применять паттерн стоит только тогда, когда он решает реальную проблему. Избыточное «навешивание» паттернов (overengineering) усложняет код не меньше, чем их отсутствие.
2. Классификация GoF
23 паттерна делятся на три группы по назначению.
| Группа | Что решает | Паттерны |
|---|---|---|
| Порождающие (creational) | Создание объектов, сокрытие конкретных классов | Factory Method, Abstract Factory, Builder, Prototype, Singleton |
| Структурные (structural) | Компоновка объектов и классов в крупные структуры | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |
| Поведенческие (behavioral) | Взаимодействие объектов, распределение ответственности | Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor |
Ниже разбираем ключевые паттерны каждой группы.
3. Порождающие паттерны
3.1. Factory Method (Фабричный метод)
Назначение. Определить интерфейс для создания объекта, но позволить подклассам решать, какой именно класс инстанцировать.
Когда применять: когда заранее неизвестно, объекты какого конкретного типа понадобятся, и вы хотите изолировать клиентский код от конкретных классов.
from abc import ABC, abstractmethod
class Notification(ABC): @abstractmethod def send(self, recipient: str, message: str) -> str: ...
class EmailNotification(Notification): def send(self, recipient: str, message: str) -> str: return f"Email отправлен на {recipient}: {message}"
class SMSNotification(Notification): def send(self, recipient: str, message: str) -> str: return f"SMS отправлено на {recipient}: {message}"
class NotificationFactory(ABC): @abstractmethod def create_notification(self) -> Notification: ...
def notify(self, recipient: str, message: str) -> str: notification = self.create_notification() # фабричный метод return notification.send(recipient, message)
class EmailNotificationFactory(NotificationFactory): def create_notification(self) -> Notification: return EmailNotification()
class SMSNotificationFactory(NotificationFactory): def create_notification(self) -> Notification: return SMSNotification()
factory = EmailNotificationFactory()print(factory.notify("user@example.com", "Привет!"))# Email отправлен на user@example.com: Привет!Часто фабрику выбирают по строке-каналу. Такой выбор удобно вынести в функцию или в реестр (см. раздел 6 о питоническом взгляде).
3.2. Abstract Factory (Абстрактная фабрика)
Назначение. Предоставить интерфейс для создания семейств связанных объектов без указания их конкретных классов.
Когда применять: когда система должна работать с разными «семействами» продуктов и важно, чтобы продукты из одного семейства использовались вместе (например, виджеты под Windows или под macOS).
from abc import ABC, abstractmethod
class Button(ABC): @abstractmethod def render(self) -> str: ...
class Checkbox(ABC): @abstractmethod def render(self) -> str: ...
class WindowsButton(Button): def render(self) -> str: return "[Windows Button]"
class WindowsCheckbox(Checkbox): def render(self) -> str: return "[Windows Checkbox]"
class MacButton(Button): def render(self) -> str: return "[Mac Button]"
class MacCheckbox(Checkbox): def render(self) -> str: return "[Mac Checkbox]"
class UIFactory(ABC): # абстрактная фабрика @abstractmethod def create_button(self) -> Button: ... @abstractmethod def create_checkbox(self) -> Checkbox: ...
class WindowsUIFactory(UIFactory): def create_button(self) -> Button: return WindowsButton() def create_checkbox(self) -> Checkbox: return WindowsCheckbox()
class MacUIFactory(UIFactory): def create_button(self) -> Button: return MacButton() def create_checkbox(self) -> Checkbox: return MacCheckbox()
def build_form(factory: UIFactory) -> list[str]: return [factory.create_button().render(), factory.create_checkbox().render()]
print(build_form(WindowsUIFactory())) # ['[Windows Button]', '[Windows Checkbox]']Отличие от Factory Method: фабричный метод создаёт один продукт, абстрактная фабрика — семейство согласованных продуктов.
3.3. Builder (Строитель)
Назначение. Отделить конструирование сложного объекта от его представления, чтобы один процесс построения мог создавать разные представления.
Когда применять: когда у объекта много параметров (часть необязательные) или когда сборка идёт пошагово. Часто реализуется как fluent-интерфейс (цепочка вызовов).
class Query: def __init__(self, table, columns, conditions): self.table, self.columns, self.conditions = table, columns, conditions
def to_sql(self) -> str: cols = ", ".join(self.columns) if self.columns else "*" sql = f"SELECT {cols} FROM {self.table}" if self.conditions: sql += " WHERE " + " AND ".join(self.conditions) return sql
class QueryBuilder: def __init__(self): self._table = None self._columns: list[str] = [] self._conditions: list[str] = []
def select(self, *columns: str) -> "QueryBuilder": self._columns = list(columns) return self # возврат self даёт fluent-интерфейс
def from_table(self, table: str) -> "QueryBuilder": self._table = table return self
def where(self, condition: str) -> "QueryBuilder": self._conditions.append(condition) return self
def build(self) -> Query: if self._table is None: raise ValueError("Не указана таблица (from_table)") return Query(self._table, self._columns, self._conditions)
query = (QueryBuilder() .select("name", "email") .from_table("users") .where("active = 1") .build())print(query.to_sql())# SELECT name, email FROM users WHERE active = 1Иногда добавляют Director — класс, инкапсулирующий типовые последовательности вызовов строителя (готовые «рецепты» запросов).
3.4. Singleton (Одиночка)
Назначение. Гарантировать, что у класса есть только один экземпляр, и предоставить к нему глобальную точку доступа.
Когда применять: для разделяемых ресурсов — пула соединений, конфигурации, логгера. Внимание: Singleton часто критикуют как «глобальную переменную в обёртке» — он усложняет тестирование и создаёт скрытые зависимости.
В Python есть несколько способов. Самый прозрачный — через метакласс:
class SingletonMeta(type): _instances = {}
def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]
class Config(metaclass=SingletonMeta): def __init__(self): self.settings = {"debug": True}
a = Config()b = Config()print(a is b) # TrueПитонический способ: часто Singleton не нужен вовсе. Модуль в Python сам по себе
является одиночкой — он импортируется один раз и кэшируется в sys.modules.
Достаточно завести объект на уровне модуля, и все импортирующие получат тот же
экземпляр.
4. Структурные паттерны
4.1. Adapter (Адаптер)
Назначение. Преобразовать интерфейс класса в другой интерфейс, ожидаемый клиентом. Позволяет работать вместе классам с несовместимыми интерфейсами.
Когда применять: при интеграции с чужим кодом (внешние библиотеки, legacy), который нельзя или не нужно менять.
from abc import ABC, abstractmethod
class AnalyticsService(ABC): # целевой интерфейс @abstractmethod def track_event(self, name: str, props: dict) -> str: ...
class GoogleAnalytics: # чужой класс, менять нельзя def send_hit(self, hit_type: str, params: dict) -> str: return f"GA hit: {hit_type} with {params}"
class GoogleAnalyticsAdapter(AnalyticsService): def __init__(self, ga: GoogleAnalytics): self._ga = ga
def track_event(self, name: str, props: dict) -> str: return self._ga.send_hit("event", {"event": name, **props})
service: AnalyticsService = GoogleAnalyticsAdapter(GoogleAnalytics())print(service.track_event("purchase", {"amount": 1500}))# GA hit: event with {'event': 'purchase', 'amount': 1500}4.2. Decorator (Декоратор)
Назначение. Динамически добавлять объекту новые обязанности, оборачивая его в объект-обёртку с тем же интерфейсом. Гибкая альтернатива наследованию.
Когда применять: когда нужно расширять поведение в произвольных комбинациях, не
порождая классов на каждое сочетание. (Не путать с синтаксическими декораторами
Python @… — это другое, хотя идея родственна.)
from abc import ABC, abstractmethod
class TextProcessor(ABC): @abstractmethod def process(self, text: str) -> str: ...
class PlainTextProcessor(TextProcessor): def process(self, text: str) -> str: return text
class TextProcessorDecorator(TextProcessor): def __init__(self, wrapped: TextProcessor): self._wrapped = wrapped
class UpperCaseDecorator(TextProcessorDecorator): def process(self, text: str) -> str: return self._wrapped.process(text).upper()
class TrimDecorator(TextProcessorDecorator): def process(self, text: str) -> str: return self._wrapped.process(text).strip()
# Декораторы комбинируются в любом порядкеprocessor = UpperCaseDecorator(TrimDecorator(PlainTextProcessor()))print(processor.process(" hello ")) # HELLO4.3. Facade (Фасад)
Назначение. Предоставить единый упрощённый интерфейс к сложной подсистеме.
Когда применять: когда подсистема состоит из множества классов, а клиенту нужен лишь типичный сценарий. Фасад не скрывает подсистему полностью — он лишь даёт удобный «вход».
class CPU: def execute(self) -> str: return "CPU: выполнение"
class Memory: def load(self) -> str: return "Memory: загрузка"
class Disk: def read(self) -> str: return "Disk: чтение"
class ComputerFacade: def __init__(self): self._cpu, self._mem, self._disk = CPU(), Memory(), Disk()
def start(self) -> list[str]: return [self._disk.read(), self._mem.load(), self._cpu.execute()]
print(ComputerFacade().start())# ['Disk: чтение', 'Memory: загрузка', 'CPU: выполнение']4.4. Composite (Компоновщик) — кратко
Назначение. Объединять объекты в древовидные структуры и одинаково обрабатывать как отдельные объекты («листья»), так и их группы («узлы»).
Когда применять: для иерархий «часть–целое» — файловая система, дерево UI, организационная структура. Лист и контейнер реализуют общий интерфейс, поэтому клиент не различает их.
from abc import ABC, abstractmethod
class FileSystemNode(ABC): @abstractmethod def size(self) -> int: ...
class File(FileSystemNode): def __init__(self, size: int): self._size = size def size(self) -> int: return self._size
class Directory(FileSystemNode): def __init__(self): self._children: list[FileSystemNode] = [] def add(self, node: FileSystemNode) -> None: self._children.append(node) def size(self) -> int: return sum(child.size() for child in self._children)
root = Directory()root.add(File(100))sub = Directory(); sub.add(File(50)); root.add(sub)print(root.size()) # 1505. Поведенческие паттерны
5.1. Strategy (Стратегия)
Назначение. Определить семейство взаимозаменяемых алгоритмов, инкапсулировать каждый из них и сделать их подставляемыми во время выполнения.
Когда применять: когда у задачи есть несколько способов решения (способы оплаты, алгоритмы сортировки/сжатия), и выбор делается в рантайме.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC): @abstractmethod def pay(self, amount: float) -> str: ...
class CardPayment(PaymentStrategy): def pay(self, amount: float) -> str: return f"Оплата {amount} картой"
class PayPalPayment(PaymentStrategy): def pay(self, amount: float) -> str: return f"Оплата {amount} через PayPal"
class PaymentProcessor: def __init__(self, strategy: PaymentStrategy): self._strategy = strategy
def process(self, amount: float) -> str: return self._strategy.pay(amount)
print(PaymentProcessor(CardPayment()).process(1000))print(PaymentProcessor(PayPalPayment()).process(500))5.2. Observer (Наблюдатель)
Назначение. Установить зависимость «один-ко-многим»: при изменении состояния объекта (субъекта) все его подписчики автоматически уведомляются.
Когда применять: для систем событий, GUI, реактивных обновлений — когда нужно оповещать заранее неизвестное число слушателей.
from abc import ABC, abstractmethod
class EventListener(ABC): @abstractmethod def handle(self, event: str, data: dict) -> None: ...
class EventEmitter: def __init__(self): self._listeners: dict[str, list[EventListener]] = {}
def on(self, event: str, listener: EventListener) -> None: self._listeners.setdefault(event, []).append(listener)
def emit(self, event: str, data: dict | None = None) -> None: for listener in self._listeners.get(event, []): listener.handle(event, data or {})
class Logger(EventListener): def __init__(self): self.logs: list[str] = [] def handle(self, event: str, data: dict) -> None: self.logs.append(f"{event}: {data}")
emitter = EventEmitter()log = Logger()emitter.on("user_registered", log)emitter.emit("user_registered", {"name": "Иван"})print(log.logs) # ["user_registered: {'name': 'Иван'}"]5.3. Command (Команда)
Назначение. Инкапсулировать запрос как объект, что позволяет параметризовать клиентов разными запросами, ставить запросы в очередь, логировать их и поддерживать отмену (undo).
Когда применять: для операций с историей и отменой (редакторы), очередей задач, макрокоманд, транзакций.
from abc import ABC, abstractmethod
class Command(ABC): @abstractmethod def execute(self) -> None: ... @abstractmethod def undo(self) -> None: ...
class TextDocument: def __init__(self): self.content = ""
class InsertCommand(Command): def __init__(self, doc: TextDocument, text: str): self._doc = doc self._text = text
def execute(self) -> None: self._doc.content += self._text
def undo(self) -> None: self._doc.content = self._doc.content[:-len(self._text)]
class CommandHistory: def __init__(self): self._done: list[Command] = []
def execute(self, command: Command) -> None: command.execute() self._done.append(command)
def undo(self) -> None: if self._done: self._done.pop().undo()
doc = TextDocument()history = CommandHistory()history.execute(InsertCommand(doc, "Hello"))print(doc.content) # Hellohistory.undo()print(doc.content) # (пусто)5.4. Template Method (Шаблонный метод)
Назначение. Определить скелет алгоритма в методе базового класса, оставив реализацию отдельных шагов подклассам.
Когда применять: когда несколько алгоритмов имеют одинаковую структуру, но различаются в деталях отдельных шагов. Общий порядок фиксируется в базовом классе, изменяемые шаги — абстрактные методы.
from abc import ABC, abstractmethod
class DataExporter(ABC): def export(self, data: list[dict]) -> str: # шаблонный метод header = self.make_header() body = "\n".join(self.format_row(row) for row in data) return f"{header}\n{body}"
@abstractmethod def make_header(self) -> str: ... @abstractmethod def format_row(self, row: dict) -> str: ...
class CSVExporter(DataExporter): def make_header(self) -> str: return "name,age" def format_row(self, row: dict) -> str: return f"{row['name']},{row['age']}"
print(CSVExporter().export([{"name": "Иван", "age": 30}]))# name,age# Иван,306. Питонический взгляд: паттерны и первоклассные объекты
Многие паттерны GoF возникли в языках (C++, Java), где функции не являются полноценными объектами, а классы — единственный способ упаковать поведение. В Python функции и классы — это объекты «первого класса»: их можно присваивать переменным, передавать в аргументах и возвращать. Поэтому ряд паттернов упрощается или растворяется.
-
Strategy. Вместо иерархии классов-стратегий часто достаточно передать функцию:
def process(amount: float, pay) -> str:return pay(amount)process(1000, lambda a: f"Оплата {a} картой")Класс-стратегия оправдан, когда у алгоритма есть состояние или несколько методов.
-
Command во многих случаях — это просто
callable(функция илиpartial), сложенный в список. Полный класс нужен, когда требуетсяundo. -
Factory Method. Фабрику нередко заменяет функция или реестр (registry) — словарь
имя → класс:REGISTRY: dict[str, type] = {}def register(name: str):def deco(cls):REGISTRY[name] = clsreturn clsreturn deco@register("email")class EmailNotification:...def create(name: str):return REGISTRY[name]()Такой плагин-подход (реестр + декоратор регистрации) — частая питоническая замена громоздких иерархий фабрик.
-
Singleton заменяется модулем или объектом уровня модуля.
-
Decorator для функций встроен в язык в виде синтаксиса
@decorator. -
Iterator встроен через протокол
__iter__/__next__и генераторы.
Вывод: в Python паттерн — это ориентир, а не обязательная церемония. Сначала
выбираем самое простое решение (функция, словарь, генератор), а к полному
ООП-варианту переходим, когда появляется состояние, несколько методов или
требование строгого контракта через ABC.
7. Таблица-сводка
| Паттерн | Группа | Назначение (одной фразой) |
|---|---|---|
| Factory Method | Порождающий | Подкласс решает, какой объект создать |
| Abstract Factory | Порождающий | Создание семейства согласованных объектов |
| Builder | Порождающий | Пошаговая сборка сложного объекта |
| Singleton | Порождающий | Единственный экземпляр и доступ к нему |
| Adapter | Структурный | Совместить несовместимые интерфейсы |
| Decorator | Структурный | Добавить обязанности через обёртку |
| Facade | Структурный | Упрощённый вход в сложную подсистему |
| Composite | Структурный | Единая работа с деревом «часть–целое» |
| Strategy | Поведенческий | Взаимозаменяемые алгоритмы в рантайме |
| Observer | Поведенческий | Автооповещение подписчиков об изменении |
| Command | Поведенческий | Запрос как объект (очередь, undo) |
| Template Method | Поведенческий | Скелет алгоритма + переопределяемые шаги |
Краткие итоги
- Паттерн — типовое решение задачи проектирования, общий словарь и носитель принципов SOLID, а не готовый копируемый код.
- GoF делит 23 паттерна на три группы: порождающие (создание объектов), структурные (компоновка), поведенческие (взаимодействие).
- Порождающие изолируют клиента от конкретных классов: Factory Method (один продукт), Abstract Factory (семейство), Builder (пошаговая сборка), Singleton (единственный экземпляр).
- Структурные собирают объекты в большие структуры: Adapter (совместимость), Decorator (наращивание обязанностей), Facade (упрощённый интерфейс), Composite (деревья).
- Поведенческие распределяют ответственность: Strategy, Observer, Command, Template Method.
- В Python первоклассные функции, реестры, генераторы и синтаксис
@decoratorчасто заменяют тяжёлые ООП-реализации паттернов. Применяйте паттерн осознанно, когда он действительно упрощает дизайн.
Вопросы для самопроверки
- Чем паттерн проектирования отличается от готового фрагмента кода (библиотеки)?
- На какие три группы GoF делит паттерны и по какому признаку?
- В чём принципиальная разница между Factory Method и Abstract Factory?
- Когда уместен Builder, а когда достаточно обычного конструктора с параметрами?
- Почему Singleton часто называют антипаттерном и чем его заменяют в Python?
- Чем Adapter отличается от Decorator, если оба «оборачивают» другой объект?
- Какую задачу решает Facade и что он не делает (чего не гарантирует)?
- Как паттерн Command обеспечивает отмену операций (undo)?
- Чем Strategy отличается от Template Method по способу варьирования поведения?
- Приведите три примера, где питонический подход (функция, реестр, генератор) заменяет классический паттерн GoF.