Перейти к содержанию
Языки

Паттерны проектирования на собеседовании: какие спрашивают и как отвечать

Паттерн проектирования — именованное типовое решение повторяющейся задачи проектирования, а не библиотека и не готовый фрагмент кода. Канон — книга «банды четырёх» (Gamma, Helm, Johnson, Vlissides) 1994 года с 23 паттернами. На собеседовании важнее не каталог целиком, а понимание: какую проблему решает паттерн, чего он стоит и где уже встречается в знакомом коде. Ниже — три группы GoF и хрестоматийные паттерны от Singleton до Observer с примерами на Python 3.12, которые мы запустили, и ссылками на стандартные библиотеки Python и Java.

Автор: Опубликовано

8 мин чтенияРедакционный разборОбновлено
  • паттерны проектирования
  • GoF
  • Python
  • Java
  • собеседование
Главная мысль

Рассказывайте о паттерне в порядке «проблема — решение — цена — пример из жизни». Название и UML-схема — последнее, что нужно интервьюеру. В Python многие паттерны сводятся к функции или модулю, но задача, которую они решают, остаётся, и назвать нужно именно её.

Что такое паттерн и три группы GoF

Каталог GoF делится на три главы. Порождающие паттерны отвечают на вопрос, кто и как создаёт объекты; структурные — как собрать из объектов и классов более крупную структуру; поведенческие — как распределить обязанности и организовать взаимодействие. Во введении авторы выделяют четыре главных элемента паттерна — имя, проблему, решение и последствия, — и на собеседовании последствия ценятся не меньше самого решения.

«Паттерны микросервисов» — другой уровень. Saga, API Gateway, Circuit Breaker или Database per Service решают задачи распределённой системы: согласованность данных между сервисами, отказы сети, маршрутизацию запросов. Это архитектурные паттерны, и их каталог ведётся отдельно, например на microservices.io. Если вопрос звучит как «какие паттерны вы знаете», уточните уровень: объектный дизайн или архитектура системы.

Три группы паттернов в книге GoF
ГруппаГлавный вопросПаттерны в книге
Порождающие (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, выбор драйвера базы по строке подключения как фабрика. Главное — назвать проблему, которую решает конкретная конструкция, а не только термин.

Паттерны GoF в стандартных библиотеках Python и Java
ПаттернPythonJava
Singletonlogging.getLogger(name)Runtime.getRuntime()
Adapterfunctools.cmp_to_keyArrays.asList
Decoratorio.BufferedReaderBufferedInputStream
Proxyweakref.proxyjava.lang.reflect.Proxy
Strategysorted(key=...)Comparator
Observerобработчики loggingFlow.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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку