Книге «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 | Модуль и объекты уровня модуля | Не писать руками — модуль уже синглтон |
| Prototype | copy.copy / copy.deepcopy | Редко; чаще достаточно нового вызова конструктора |
| Factory Method | sessionmaker в SQLAlchemy, Depends в FastAPI | Да — классметоды-фабрики для создания объектов с валидацией |
| Abstract Factory | DB-API: драйвер выдаёт согласованные Connection и Cursor | Не нужен — выбор реализации делает фабричная функция |
| Builder | Query-конструкторы: select(...).where(...) в SQLAlchemy | Редко — именованные аргументы и dataclass покрывают задачу |
| Adapter | WSGI/ASGI между сервером и фреймворком | Да — адаптеры к внешним зависимостям |
| Bridge | fsspec, хендлеры logging | Почти никогда — «Protocol + DI» покрывает |
| Composite | unittest.TestSuite, contextlib.ExitStack | Да — когда нужно несколько получателей за одним интерфейсом |
| Decorator | @lru_cache, @wraps, middleware-обёртки | Да — и для функций (@decorator), и для объектов |
| Facade | requests, shutil, subprocess.run | Да — адаптер-фасад над чужим SDK |
| Flyweight | Кеш маленьких int, sys.intern, re.compile | Почти никогда — идею несут неизменяемые value objects |
| Proxy | SimpleLazyObject в Django, weakref.proxy, mock-объекты | Не писать — использовать __getattr__-делегирование и готовые обёртки |
| Chain of Responsibility | ASGI/Django middleware, иерархия логгеров | Редко — хватает готовых цепочек фреймворка |
| Command | Задачи Celery, functools.partial + пул потоков | Да — разделение «что» и «как» в обработчиках операций |
| Interpreter | Jinja2, lookup'ы Django ORM (field__gte=...) | Не изобретать собственных языков выражений |
| Iterator | Протокол итератора, генераторы, itertools | Растворился в языке |
| Mediator | Django signals, blinker | Да — события вместо прямых вызовов между модулями |
| Memento | Savepoint: begin_nested() в SQLAlchemy | Почти никогда — откат делает транзакция базы данных |
| Observer | Django signals, transaction.on_commit | Да — доменные события, постоянно |
| State | Библиотека transitions для сложных случаев | Да, в облегчённой форме — Enum + проверки переходов |
| Strategy | sorted(key=...), json.dumps(default=...) | Да — функции первым классом вместо разрастающихся if |
| Template Method | unittest.TestCase (setUp/tearDown) | Колбэк-вариант предпочтительнее наследования |
| Visitor | ast.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 в работе.