← назад к разделу

Книге «Design Patterns» банды четырёх уже больше тридцати лет, но читать её как готовый каталог рецептов сегодня не нужно — половина паттернов давно растворилась в языке и фреймворках. Зачем тогда учить? Потому что Django, SQLAlchemy, любой HTTP-фреймворк разговаривают именно этим словарём. ast.NodeVisitor, sessionmaker, SimpleLazyObject — это не случайные имена, это паттерны GoF в названиях классов. Не зная паттерн, трудно понять, почему класс устроен так, а не иначе.

Ниже — все 23 паттерна по классическим группам. Для каждого: суть в одном предложении, где он уже живёт в готовых инструментах и нужно ли его писать самому.

Порождающие паттерны

Эта группа отвечает на один вопрос: как правильно создавать объекты?

Singleton

Суть: один экземпляр на всё приложение.

Если создавать объект в каждом месте, где он нужен, получается несколько несвязанных копий с разным состоянием. Singleton решает это: объект существует в одном экземпляре, и все обращаются к одному и тому же.

В Python синглтон реализуется модулем: объект, созданный на уровне модуля, существует в одном экземпляре — модуль импортируется один раз и кешируется в sys.modules. Это лучше классического GoF-варианта с методом get_instance(): модульный синглтон импортируется явно и легко подменяется в тестах через monkeypatch.

Главное следствие: один экземпляр обслуживает все запросы параллельно, поэтому в таких объектах не должно быть изменяемого состояния — иначе возникает гонка потоков.

# pricing.py — модуль и есть синглтон
pricing_service = PricingService()


def test_calculates_price():
    service = PricingService()  # в тесте создаём свой экземпляр — просто

Prototype

Суть: новый экземпляр при каждом запросе.

Противоположность Singleton: иногда нужен свежий объект для каждой операции, а не общий на всех. В Python это буквально модуль copy: copy.copy() и copy.deepcopy() создают новый экземпляр из существующего, а поведение настраивается методами __copy__ и __deepcopy__.

Ловушка: значение по умолчанию аргумента функции вычисляется один раз при определении функции — и «новизна» теряется. Решение — создавать объект внутри вызова:

def create_report_broken(request: ReportRequest, builder: ReportBuilder = ReportBuilder()) -> ReportDto:
    ...  # ловушка: один builder на все вызовы


def create_report(request: ReportRequest) -> ReportDto:
    builder = ReportBuilder()  # каждый раз новый экземпляр
    return builder.with_request(request).build()

Factory Method

Суть: создание объекта делегируется методу, скрывающему конкретный класс.

Вместо того чтобы везде писать ConcreteClass(...), вызывающий код работает с абстракцией, а фабричный метод решает, какую реализацию создать.

В FastAPI каждая функция-зависимость в Depends(...) — это фабричный метод: вызывающий код знает интерфейс, функция решает, какую реализацию сконфигурировать. В SQLAlchemy та же роль у sessionmaker:

def make_clock(settings: Settings) -> Clock:
    if settings.profile == "integration-test":
        return FixedClock(datetime(2026, 1, 15, 10, 0, tzinfo=UTC))
    return SystemClock()

В прикладном коде классметоды-фабрики — хороший способ создавать объекты с валидацией:

class Order:

    @classmethod
    def create(cls, customer_id: CustomerId, lines: list[OrderLine]) -> "Order":
        if not lines:
            raise ValueError("Заказ должен содержать хотя бы одну позицию")
        return cls(OrderId.generate(), customer_id, lines, Status.CREATED)

Abstract Factory

Суть: фабрика, которая создаёт семейство связанных объектов.

Если Factory Method создаёт один объект, Abstract Factory создаёт целую группу объектов, которые должны «сочетаться» друг с другом.

В Python роль абстрактной фабрики играет DB-API: модуль драйвера (sqlite3, psycopg) по единому контракту выдаёт согласованные между собой Connection и Cursor, скрывая конкретную реализацию. Какой будет драйвер — решает конфигурация, не вызывающий код.

В прикладном коде Abstract Factory пишут редко: выбор реализации по настройкам покрывает обычная фабричная функция.

Builder

Суть: пошаговая сборка сложного объекта с читаемым кодом.

Конструктор с восемью позиционными параметрами — источник ошибок: легко перепутать порядок, непонятно, что за что отвечает. В Python бо́льшую часть работы Builder делают именованные аргументы и dataclass: перепутать порядок невозможно, значения по умолчанию встроены в язык.

Полноценный Builder остаётся в конструкторах запросов: select(...).where(...).order_by(...) в SQLAlchemy — та самая пошаговая сборка сложного объекта:

@dataclass(frozen=True)
class OrderSearchQuery:
    customer_id: CustomerId | None = None
    status: OrderStatus | None = None
    page: int = 0
    size: int = 20


query = OrderSearchQuery(status=OrderStatus.PAID, size=20)

Структурные паттерны

Эта группа отвечает на вопрос: как правильно строить связи между объектами?

Adapter

Суть: преобразует один интерфейс в другой, которого ожидает клиент.

Представьте две заглушки разной формы: код ожидает один интерфейс, а внешняя библиотека предлагает другой. Adapter — переходник между ними.

В Python это WSGI/ASGI: серверу (uvicorn, gunicorn) всё равно, чем написано приложение — Django, FastAPI или Flask. Он работает с единым контрактом, к которому каждый фреймворк приводит свой стиль.

В прикладном коде Adapter — основа работы с внешними зависимостями: адаптер переводит доменный интерфейс в язык конкретного SDK:

class S3DocumentStorageAdapter:

    def __init__(self, s3_client: S3Client) -> None:
        self._s3_client = s3_client

    def store(self, document: Document) -> DocumentRef:
        key = str(document.id)
        self._s3_client.put_object(Bucket="documents", Key=key, Body=document.content)
        return DocumentRef(key)

Bridge

Суть: абстракция и реализация развиваются независимо.

Классический пример: fsspec — одна абстракция «файловая система», независимые реализации под file://, s3://, https://. Код, читающий файл, не меняется при смене источника. Та же схема у logging: логгер — абстракция, хендлеры — независимые реализации вывода.

В прикладном коде Bridge в чистом виде почти не встречается — его заменяет связка «Protocol + внедрение зависимости».

Composite

Суть: группа объектов используется так же, как один объект.

Нужно отправить уведомление одновременно по email и SMS, но вызывающий код не должен знать о деталях. Composite позволяет «завернуть» несколько объектов в один, реализующий тот же интерфейс.

В стандартной библиотеке это unittest.TestSuite: набор тестов запускается так же, как один тест. Та же механика у contextlib.ExitStack — несколько контекстов выглядят как один.

class CompositeNotificationAdapter:

    def __init__(self, channels: list[NotificationPort]) -> None:
        self._channels = channels

    def order_cancelled(self, order: Order) -> None:
        for channel in self._channels:
            channel.order_cancelled(order)

Decorator

Суть: объект оборачивается в обёртку с тем же интерфейсом, добавляющую поведение.

Нужно добавить кеширование к репозиторию, но менять его класс нельзя (или не хочется). Decorator создаёт обёртку с тем же интерфейсом, которая перехватывает вызовы и добавляет нужное поведение.

В Python паттерн встроен в синтаксис: @functools.lru_cache, @functools.wraps, @retry из tenacity — обёртки с тем же интерфейсом, добавляющие поведение функции. Для объектов та же идея пишется вручную:

class CachingProductRepository:

    def __init__(self, delegate: ProductRepository, cache: Cache) -> None:
        self._delegate = delegate
        self._cache = cache

    def find_by_id(self, product_id: ProductId) -> Product | None:
        return self._cache.get_or_load(
            product_id, lambda: self._delegate.find_by_id(product_id)
        )

Facade

Суть: простой интерфейс над сложной подсистемой.

Работа с HTTP напрямую через http.client требует: открыть соединение, собрать заголовки, обработать редиректы и кодировки, закрыть всё в правильном порядке. requests.get(...) скрывает всю эту сложность за одним вызовом.

В Python фасады повсюду: requests (над urllib3), shutil (над os), subprocess.run (над Popen). В прикладном коде фасад над внешним SDK — нормальная форма адаптера: один метод может скрывать три вызова стороннего API, повторные попытки и преобразование ошибок.

class PaymentGatewayAdapter:

    def __init__(self, sdk_client: PaymentSdkClient) -> None:
        self._sdk_client = sdk_client

    def charge(self, order: Order, method: PaymentMethod) -> PaymentResult:
        request = self._sdk_client.new_request(
            amount=order.total.amount,
            currency=order.total.currency.code,
            method=method.token,
        )
        response = self._sdk_client.submit(request)
        return PaymentResult(response.transaction_id, response.status)

Flyweight

Суть: разделяемые неизменяемые объекты вместо тысяч одинаковых копий.

Если создавать по объекту на каждое слово в тексте, память закончится быстро. Flyweight разделяет объекты с одинаковым содержимым — один экземпляр на значение.

В Python это кеш маленьких int (от −5 до 256) и интернирование строк (sys.intern). Во фреймворках — внутренние кеши скомпилированных регулярных выражений (re.compile) и метаданных типов.

В прикладном коде Flyweight почти никогда не пишут руками. Его идею несут неизменяемые value objects и члены Enum: Currency.RUB один на всё приложение именно потому, что неизменяем.

Proxy

Суть: объект-заместитель контролирует доступ к реальному объекту.

Прокси перехватывает вызовы и делает что-то до или после: открывает транзакцию, проверяет права, кеширует результат.

В Python прокси строится на __getattr__: Django оборачивает request.user в SimpleLazyObject — запрос к базе уходит только при первом обращении; unittest.mock.MagicMock перехватывает любые вызовы; weakref.proxy контролирует время жизни объекта.

Отсюда и классические ловушки: прокси прозрачен не до конца — isinstance по конкретному классу и жёсткие проверки типов на заместителе ломаются.

class LazyProxy:

    def __init__(self, factory: Callable[[], Any]) -> None:
        self._factory = factory
        self._target: Any = None

    def __getattr__(self, name: str) -> Any:
        if self._target is None:
            self._target = self._factory()  # реальный объект создаётся при первом обращении
        return getattr(self._target, name)

Поведенческие паттерны

Эта группа отвечает на вопрос: как организовать взаимодействие объектов?

Chain of Responsibility

Суть: запрос идёт по цепочке обработчиков, пока кто-то его не обработает.

HTTP-запрос нужно сначала проверить на аутентификацию, потом на CSRF, потом на авторизацию, и каждый шаг может остановить обработку. Вместо одного огромного метода — цепочка независимых обработчиков.

В Python это стек middleware — Django-middleware и ASGI-middleware в FastAPI/Starlette — эталон паттерна. Та же механика у иерархии логгеров в logging: запись поднимается от логгера к логгеру, пока её кто-то не обработает.

class TraceIdMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request: Request, call_next) -> Response:
        token = trace_id_var.set(resolve_trace_id(request))
        try:
            return await call_next(request)  # передаём дальше по цепочке
        finally:
            trace_id_var.reset(token)

Command

Суть: операция упакована в объект — её можно передать, отложить, поставить в очередь.

Обычно вызов метода мгновенный и безымянный. Command превращает операцию в объект с данными — его можно передать в другой поток, отложить, залогировать, отменить.

В Python это задачи Celery: task.delay(...) упаковывает вызов в объект и отправляет в очередь. Тот же смысл у functools.partial, уходящего в ThreadPoolExecutor.

В прикладном коде Command — структурная основа разделения «что делать» и «как делать»: один объект несёт данные операции, другой её исполняет.

@dataclass(frozen=True)
class CancelOrderCommand:
    order_id: OrderId
    reason: CancelReason


class CancelOrderHandler:

    def __init__(self, order_repository: OrderRepository, session: Session) -> None:
        self._order_repository = order_repository
        self._session = session

    def handle(self, command: CancelOrderCommand) -> None:
        with self._session.begin():
            order = self._order_repository.find_by_id(command.order_id)
            order.cancel(command.reason)
            self._order_repository.save(order)

Interpreter

Суть: язык с грамматикой и интерпретатор выражений на нём.

В Python это Jinja2-шаблоны, мини-язык format-спецификаций и lookup'ы Django ORM (created_at__gte=...) — строковые выражения со своей грамматикой и интерпретатором.

В собственном коде создавать мини-языки не стоит: строковые выражения не проверяются тайп-чекером, ломаются при рефакторинге и усложняют отладку.

Iterator

Суть: последовательный доступ к элементам без раскрытия внутренней структуры.

Паттерн давно растворился в языке: протокол итератора (__iter__/__next__), цикл for, генераторы, itertools. Клиенты внешних API добавляют пагинаторы поверх той же идеи (например, paginators в boto3).

Реализовывать Iterator руками не приходится. Единственное близкое решение — отдавать из коллекций агрегата неизменяемые представления.

Mediator

Суть: объекты общаются через посредника, не зная друг о друге.

Если один сервис напрямую вызывает другой, они тесно связаны: изменение одного ломает другой. Mediator убирает прямую зависимость — объекты публикуют события, и кто хочет — подписывается.

В Django это signals: приложение публикует сигнал, обработчики подписываются, и никто ни с кем не связан напрямую. Вне Django ту же роль играет blinker. Роутер ASGI-приложения — тоже медиатор между транспортом и обработчиками.

order_cancelled = Signal()


class CancelOrderHandler:

    def handle(self, command: CancelOrderCommand) -> None:
        # ... логика отмены ...
        order_cancelled.send(sender=self.__class__, order_id=command.order_id)


@receiver(order_cancelled)
def schedule_refund(sender: type, order_id: OrderId, **kwargs: Any) -> None:
    # другой модуль, не знает об обработчике отмены
    ...

Memento

Суть: снимок состояния объекта для последующего отката.

Savepoint в транзакциях — чистый Memento: session.begin_nested() в SQLAlchemy фиксирует точку, rollback() откатывает к ней.

В прикладном коде почти никогда не нужен: откат состояния — работа транзакции базы данных, история изменений — отдельная таблица аудита или event sourcing.

Observer

Суть: подписчики получают уведомления об изменении состояния издателя.

Нужно отправить письмо при отмене заказа. Можно вызвать email_service.send(...) прямо в бизнес-логике — но тогда бизнес-логика знает об email-сервисе. Observer разделяет их: бизнес-логика публикует событие, email-сервис подписывается.

В Django это signals (post_save и кастомные сигналы). Важный нюанс: если обработчик с внешними эффектами (письмо, SMS) вешается без привязки к транзакции, уведомление может уйти по откаченной транзакции. Правильно — transaction.on_commit(...).

@receiver(order_cancelled)
def notify_customer(sender: type, order_id: OrderId, **kwargs: Any) -> None:
    transaction.on_commit(lambda: notification_port.order_cancelled(order_id))

State

Суть: поведение объекта меняется при смене его внутреннего состояния.

Заказ в статусе CREATED можно отменить. Заказ в статусе DELIVERED — нет. Логику переходов можно разложить в отдельные классы-состояния, но для большинства задач достаточно проверок в методах самого объекта:

class Order:

    def cancel(self, reason: CancelReason) -> None:
        if self.status not in (Status.PAID, Status.CREATED):
            raise IllegalOrderStateError(self.id, self.status, "cancel")
        self.status = Status.CANCELLED
        self._register_event(OrderCancelled(self.id, reason))

Классический State с отдельным классом на каждое состояние оправдан при очень большой машине состояний. Для обычного объекта с несколькими статусами хватает Enum и проверок переходов.

Strategy

Суть: семейство алгоритмов за общим интерфейсом, выбираемых в зависимости от ситуации.

Скидки для разных категорий покупателей: можно написать цепочку if с условиями, а можно передавать по функции на каждую категорию. Добавление новой категории — новая функция, а не правка условий.

В Python Strategy повсюду и часто это просто функция: sorted(key=...), json.dumps(default=...) — алгоритм передаётся первым классом. Семейство с несколькими операциями оформляется через Protocol.

DiscountPolicy = Callable[[Order], Money]


def vip_discount(order: Order) -> Money:
    if not order.customer.is_vip:
        return Money.ZERO
    return order.total * Decimal("0.10")


class DiscountService:

    def __init__(self, policies: list[DiscountPolicy]) -> None:
        self._policies = policies

    def calculate_discount(self, order: Order) -> Money:
        return sum((policy(order) for policy in self._policies), start=Money.ZERO)

Template Method

Суть: скелет алгоритма в базовом классе, изменяемые шаги — в наследниках.

Тест всегда должен пройти по одному сценарию: подготовка → сам тест → уборка, даже если тест упал. Базовый класс unittest.TestCase берёт это на себя, оставляя наследнику только содержательные шаги:

class OrderApiTest(unittest.TestCase):

    def setUp(self) -> None:
        # пишем только изменяемый шаг, скелет запуска гарантирует суперкласс
        self.client = make_test_client()

Современный вариант — передавать шаг как функцию, а не создавать наследника. Контекстные менеджеры так и делают: скелет (открыть соединение → выполнить → закрыть) постоянный, переменный шаг передаётся телом блока with.

Visitor

Суть: новая операция над структурой объектов без изменения их классов.

Есть иерархия типов PaymentMethod: Card, Sbp, Cash. Нужно считать комиссию по-разному для каждого типа, не добавляя метод fee() в каждый класс. Visitor добавляет операцию снаружи. В Python классический Visitor дожил до наших дней в ast.NodeVisitor — обходе синтаксического дерева.

В современных языках Visitor вытеснил pattern matching:

# Python 3.10+: match вместо Visitor
type PaymentMethod = Card | Sbp | Cash


def fee(method: PaymentMethod) -> Decimal:
    match method:
        case Card(amount=amount):
            return amount * Decimal("0.02")
        case Sbp():
            return Decimal("0")
        case Cash():
            return Decimal("50")

Все 23 паттерна: быстрая сводка

ПаттернГде встречается в готовых инструментахНужен ли в своём коде
SingletonМодуль и объекты уровня модуляНе писать руками — модуль уже синглтон
Prototypecopy.copy / copy.deepcopyРедко; чаще достаточно нового вызова конструктора
Factory Methodsessionmaker в SQLAlchemy, Depends в FastAPIДа — классметоды-фабрики для создания объектов с валидацией
Abstract FactoryDB-API: драйвер выдаёт согласованные Connection и CursorНе нужен — выбор реализации делает фабричная функция
BuilderQuery-конструкторы: select(...).where(...) в SQLAlchemyРедко — именованные аргументы и dataclass покрывают задачу
AdapterWSGI/ASGI между сервером и фреймворкомДа — адаптеры к внешним зависимостям
Bridgefsspec, хендлеры loggingПочти никогда — «Protocol + DI» покрывает
Compositeunittest.TestSuite, contextlib.ExitStackДа — когда нужно несколько получателей за одним интерфейсом
Decorator@lru_cache, @wraps, middleware-обёрткиДа — и для функций (@decorator), и для объектов
Facaderequests, shutil, subprocess.runДа — адаптер-фасад над чужим SDK
FlyweightКеш маленьких int, sys.intern, re.compileПочти никогда — идею несут неизменяемые value objects
ProxySimpleLazyObject в Django, weakref.proxy, mock-объектыНе писать — использовать __getattr__-делегирование и готовые обёртки
Chain of ResponsibilityASGI/Django middleware, иерархия логгеровРедко — хватает готовых цепочек фреймворка
CommandЗадачи Celery, functools.partial + пул потоковДа — разделение «что» и «как» в обработчиках операций
InterpreterJinja2, lookup'ы Django ORM (field__gte=...)Не изобретать собственных языков выражений
IteratorПротокол итератора, генераторы, itertoolsРастворился в языке
MediatorDjango signals, blinkerДа — события вместо прямых вызовов между модулями
MementoSavepoint: begin_nested() в SQLAlchemyПочти никогда — откат делает транзакция базы данных
ObserverDjango signals, transaction.on_commitДа — доменные события, постоянно
StateБиблиотека transitions для сложных случаевДа, в облегчённой форме — Enum + проверки переходов
Strategysorted(key=...), json.dumps(default=...)Да — функции первым классом вместо разрастающихся if
Template Methodunittest.TestCase (setUp/tearDown)Колбэк-вариант предпочтительнее наследования
Visitorast.NodeVisitorВытеснен pattern matching (Python 3.10+)

Из 23 паттернов в прикладном коде регулярно пишут семь–восемь: Adapter, Strategy, Observer, Command, Decorator, Composite, Factory Method и State. Ещё столько же используют каждый день в готовом виде, не замечая: Proxy, Singleton, Builder, Facade, Template Method, Chain of Responsibility. Остальные — словарь для чтения чужого кода.

Коротко

  • Паттерны GoF — не рецепты для копирования, а словарь: именно им разговаривают фреймворки в названиях классов.
  • Proxy в Python строится на __getattr__: ленивые объекты Django, mock-объекты в тестах, weakref.proxy.
  • Strategy — главный инструмент против разрастающихся if: часто это просто функция, переданная первым классом.
  • Observer — стандартный способ отделить побочные действия (письмо, метрика) от бизнес-логики.
  • Adapter — основа работы с внешними зависимостями: домен знает интерфейс, адаптер знает конкретный SDK.
  • Singleton не пишут вручную с get_instance() — модуль уже синглтон.
  • Decorator добавляет поведение без наследования — в Python это буквально синтаксис @.
  • Из 23 паттернов регулярно пишут руками ~7; остальные живут в готовых инструментах.

Что почитать дальше

  • SOLID на примерах — принципы, ради которых эти паттерны существуют.
  • GRASP на примерах — какому классу отдать ответственность, прежде чем выбирать паттерн.
  • Spring AOP — как устроен Proxy, паттерн номер один Spring.
  • DI/IoC, bean scopes — Singleton и Prototype как scope контейнера.
  • Spring Events — Observer и Mediator в работе.