Рассказывайте о паттерне в порядке «проблема — решение — цена — пример из жизни». Название и UML-схема — последнее, что нужно интервьюеру. В Python многие паттерны сводятся к функции или модулю, но задача, которую они решают, остаётся, и назвать нужно именно её.
Что такое паттерн и три группы GoF
Каталог GoF делится на три главы. Порождающие паттерны отвечают на вопрос, кто и как создаёт объекты; структурные — как собрать из объектов и классов более крупную структуру; поведенческие — как распределить обязанности и организовать взаимодействие. Во введении авторы выделяют четыре главных элемента паттерна — имя, проблему, решение и последствия, — и на собеседовании последствия ценятся не меньше самого решения.
«Паттерны микросервисов» — другой уровень. Saga, API Gateway, Circuit Breaker или Database per Service решают задачи распределённой системы: согласованность данных между сервисами, отказы сети, маршрутизацию запросов. Это архитектурные паттерны, и их каталог ведётся отдельно, например на microservices.io. Если вопрос звучит как «какие паттерны вы знаете», уточните уровень: объектный дизайн или архитектура системы.
| Группа | Главный вопрос | Паттерны в книге |
|---|---|---|
| Порождающие (5) | как создавать объекты | Abstract Factory, Builder, Factory Method, Prototype, Singleton |
| Структурные (7) | как собирать структуры | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy |
| Поведенческие (11) | как распределять обязанности | Observer, Strategy, Command, State, Iterator и ещё шесть |
Singleton: самый простой и самый критикуемый
Singleton гарантирует единственный экземпляр класса и глобальную точку доступа к нему. Его любят спрашивать именно из-за критики: глобальное изменяемое состояние, скрытая зависимость, которой не видно в сигнатуре функций, и тесты, влияющие друг на друга. В примере «тест» поменял env на test, и это увидит любой код, создающий Config: вывод True test. Ленивое создание в __new__ к тому же не потокобезопасно без блокировки.
В Python роль синглтона часто играет модуль: импортированный модуль кешируется в sys.modules, и повторный import возвращает тот же объект. Поэтому достаточно создать экземпляр на уровне модуля, например config = load_config() в config.py, и импортировать его. Похожую идею использует стандартная библиотека: logging.getLogger с одним и тем же именем всегда возвращает тот же логгер, отсюда вторая строка вывода True. В Java классический пример — Runtime.getRuntime(): у каждого приложения единственный экземпляр Runtime. Сильный ответ называет альтернативу: создать объект один раз в точке сборки приложения и передавать явно, как в make_report.
import logging
class Config: # классический Singleton через __new__
_instance = None
def __new__(cls):
if cls._instance is None: # не потокобезопасно без блокировки
cls._instance = super().__new__(cls)
cls._instance.values = {"env": "prod"}
return cls._instance
a, b = Config(), Config()
a.values["env"] = "test" # изменение видно всем, включая соседние тесты
print(a is b, b.values["env"])
print(logging.getLogger("app") is logging.getLogger("app"))
def make_report(config: dict) -> str: # альтернатива: зависимость передаётся явно
return f"report for {config['env']}"
print(make_report({"env": "staging"}))Factory Method, Abstract Factory и Builder
Фабрики прячут выбор конкретного класса. В GoF Factory Method — метод, который переопределяют подклассы, решая, какой объект создать; Abstract Factory — объект, создающий целое семейство согласованных объектов, например элементы интерфейса для разных стилей оформления. В Python эту роль часто выполняет функция или словарь классов: make_exporter выбирает класс по формату в одном месте, и вызывающий код не знает о CsvExporter. Вывод первой строки: id;1;2 [{"id": 1}].
Builder нужен, когда объект собирается за несколько шагов и важно проверить итог: RequestBuilder не выпустит POST без Content-Type, а успешная сборка печатает Request(url='/api/cards', method='POST', headers={'Content-Type': 'application/json'}). В Python именованные аргументы и dataclass часто делают Builder лишним, а в Java он встречается постоянно — например, HttpRequest.newBuilder() в java.net.http. Полезный вопрос к себе: что сломается, если создавать объект напрямую?
import json
from dataclasses import dataclass, field
class CsvExporter:
def export(self, rows: list[int]) -> str: return "id;" + ";".join(map(str, rows))
class JsonExporter:
def export(self, rows: list[int]) -> str: return json.dumps([{"id": r} for r in rows])
EXPORTERS = {"csv": CsvExporter, "json": JsonExporter} # выбор класса в одном месте
def make_exporter(fmt: str):
if fmt not in EXPORTERS:
raise ValueError(f"unknown format: {fmt}")
return EXPORTERS[fmt]()
@dataclass
class Request:
url: str
method: str = "GET"
headers: dict[str, str] = field(default_factory=dict)
class RequestBuilder: # пошаговая сборка и проверка в build()
def __init__(self, url: str) -> None: self._req = Request(url)
def post(self) -> "RequestBuilder":
self._req.method = "POST"
return self
def header(self, key: str, value: str) -> "RequestBuilder":
self._req.headers[key] = value
return self
def build(self) -> Request:
if self._req.method == "POST" and "Content-Type" not in self._req.headers:
raise ValueError("POST without Content-Type")
return self._req
print(make_exporter("csv").export([1, 2]), make_exporter("json").export([1]))
print(RequestBuilder("/api/cards").post().header("Content-Type", "application/json").build())Adapter, Decorator и Proxy: три обёртки с разными целями
Все три паттерна оборачивают объект, поэтому их путают, но различаются они намерением. Adapter меняет интерфейс: SmsAdapter превращает чужой send_sms(phone, body) в наш send(to, text). Decorator сохраняет интерфейс и добавляет поведение, причём обёртки можно вкладывать друг в друга: LoggingNotifier логирует отправку. Proxy сохраняет интерфейс и управляет доступом: LazyReport создаёт дорогой отчёт только при первом обращении, поэтому «loading once» печатается один раз на два вызова render.
В стандартной библиотеке functools.cmp_to_key адаптирует старую функцию сравнения к key-функции, io.BufferedReader добавляет буферизацию к сырому потоку, как BufferedInputStream в Java, а weakref.proxy возвращает заместитель объекта на слабой ссылке. Ловушка: синтаксис @decorator в Python — не то же самое, что Decorator из GoF. PEP 318 прямо признаёт, что название вызвало жалобы и главная из них — расхождение с терминологией книги GoF.
class LegacySms: # чужой API, менять нельзя
def send_sms(self, phone: str, body: str) -> str: return f"sms:{phone}:{body}"
class SmsAdapter: # Adapter: чужой интерфейс -> наш send()
def __init__(self, legacy: LegacySms) -> None: self.legacy = legacy
def send(self, to: str, text: str) -> str: return self.legacy.send_sms(phone=to, body=text)
class LoggingNotifier: # Decorator: тот же интерфейс + поведение
def __init__(self, inner) -> None: self.inner = inner
def send(self, to: str, text: str) -> str:
result = self.inner.send(to, text)
print("logged:", to)
return result
class Report:
def render(self) -> str: return "report body"
class LazyReport: # Proxy: тот же интерфейс, контроль доступа
def __init__(self, load) -> None: self._load, self._real = load, None
def render(self) -> str:
if self._real is None:
print("loading once")
self._real = self._load()
return self._real.render()
notifier = LoggingNotifier(SmsAdapter(LegacySms()))
print(notifier.send("+70000000000", "code 1234"))
report = LazyReport(Report)
print(report.render(), report.render())Strategy и Observer: поведение как параметр и подписка
Strategy делает алгоритм взаимозаменяемым параметром. В Python функции — объекты первого класса, поэтому стратегия часто сводится к аргументу: sorted с key по сроку повторения и по названию дал два разных порядка — [('SQL', 1), ('SOLID', 2), ('JWT', 3)] и [('JWT', 3), ('SOLID', 2), ('SQL', 1)]. В Java ту же роль играет Comparator. Отличие от State: стратегию выбирает клиент, а состояние переключается внутри объекта.
Observer позволяет издателю оповещать подписчиков, ничего о них не зная: EventBus вызвал оба обработчика, вывод — «welcome email -> anna» и «metrics: signup anna». На собеседовании ждут разговора о подводных камнях: порядок вызова, исключение в одном обработчике, которое в такой реализации прервёт остальные, утечки памяти от забытых подписок, выбор между синхронным вызовом и очередью. Близкий механизм есть в logging: логгер передаёт записи всем своим обработчикам.
from typing import Callable
cards = [("SQL", 1), ("JWT", 3), ("SOLID", 2)]
print(sorted(cards, key=lambda card: card[1])) # Strategy: алгоритм — это параметр
print(sorted(cards, key=lambda card: card[0]))
class EventBus: # Observer: издатель не знает подписчиков
def __init__(self) -> None:
self._subs: dict[str, list[Callable[[str], None]]] = {}
def subscribe(self, event: str, handler: Callable[[str], None]) -> None:
self._subs.setdefault(event, []).append(handler)
def publish(self, event: str, payload: str) -> None:
for handler in self._subs.get(event, []):
handler(payload)
bus = EventBus()
bus.subscribe("signup", lambda user: print("welcome email ->", user))
bus.subscribe("signup", lambda user: print("metrics: signup", user))
bus.publish("signup", "anna")Где узнать паттерны в стандартных библиотеках
Самый убедительный ответ — показать паттерн в коде, которым пользуешься каждый день. Таблица собирает примеры из документации Python и Java. Названия классов не обязаны совпадать с GoF один в один, но решаемая задача та же: адаптировать интерфейс, добавить поведение, заменить алгоритм или оповестить подписчиков.
Если просят пример из вашего проекта, подойдут middleware в веб-фреймворке как цепочка обёрток, события ORM как Observer, выбор драйвера базы по строке подключения как фабрика. Главное — назвать проблему, которую решает конкретная конструкция, а не только термин.
| Паттерн | Python | Java |
|---|---|---|
| Singleton | logging.getLogger(name) | Runtime.getRuntime() |
| Adapter | functools.cmp_to_key | Arrays.asList |
| Decorator | io.BufferedReader | BufferedInputStream |
| Proxy | weakref.proxy | java.lang.reflect.Proxy |
| Strategy | sorted(key=...) | Comparator |
| Observer | обработчики logging | Flow.Subscriber |
Как отвечать без заучивания UML
Схема ответа: проблема, которую решает паттерн, затем минимальное решение, его цена и место, где вы его встречали. Например: «Strategy нужен, когда алгоритм меняется независимо от вызывающего кода. В Python это часто просто функция-параметр, как key в sorted. Цена — ещё одна точка косвенности. В нашем сервисе так выбирался расчёт доставки». UML-диаграмму рисуйте, только если о ней просят. Типичные ошибки:
Для повторения пригодятся разбор Java-собеседования, вопросы по Python и подборка по архитектуре backend. Перескажите каждый паттерн из статьи по схеме выше без подглядывания; RecallDeck помогает возвращаться к таким темам карточками с интервальным повторением.
- пересказать определение, не назвав проблему, которую паттерн решает;
- назвать Singleton любимым паттерном и не упомянуть глобальное состояние;
- путать Decorator с Proxy или Strategy со State;
- переносить в Python Java-иерархии классов там, где хватает функции.
Коротко
Частые вопросы
Сколько паттернов нужно знать к собеседованию?
Официального минимума нет. Практичнее уверенно объяснить хрестоматийные — Singleton, фабрики, Builder, Adapter, Decorator, Proxy, Strategy, Observer — и уметь узнать остальные по описанию задачи. Глубокое понимание нескольких паттернов полезнее поверхностного знания всех двадцати трёх.
MVC — это паттерн из книги GoF?
Нет, MVC не входит в 23 паттерна. Но во введении книги ему посвящён отдельный раздел, где MVC разобран как сочетание паттернов: представление подписано на модель (Observer), представления вкладываются друг в друга (Composite), а контроллер — сменная стратегия обработки ввода (Strategy).
Чем Strategy отличается от State?
Структурно они похожи: объект делегирует работу заменяемому объекту. Но стратегию выбирает и подставляет клиент, а состояния обычно переключают друг друга сами, реагируя на события, — как заказ, который переходит из «создан» в «оплачен» и «отправлен». Вопрос проверяет понимание намерения, а не схемы.
Нужны ли паттерны в Python, если язык динамический?
Нужно понимание задач, а не обязательно классы. Многие паттерны в Python упрощаются: стратегия становится функцией, синглтон — модулем, декоратор поведения — функцией-обёрткой. Но сами задачи — выбор реализации, расширение без правки, развязка издателя и подписчиков — остаются, и интервьюер ждёт, что вы их узнаете.
Источники
Источники и редакционная политика
Сведения проверены 7 ноября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.