Шаблоны проектирования¶
Справочник вопросов по классическим шаблонам проектирования (GoF) для подготовки к экзамену.
1. Шаблоны проектирования: что это и типы шаблонов¶
Шаблон проектирования — типовое, проверенное практикой решение часто встречающейся задачи проектирования в заданном контексте; это не готовый код, а описание подхода. Шаблоны GoF делятся на три типа: порождающие (creational) — отвечают за гибкое создание объектов; структурные (structural) — описывают сборку объектов и классов в более крупные структуры; поведенческие (behavioral) — распределяют обязанности и организуют взаимодействие между объектами.
Порождающие
2. Простая фабрика (Simple Factory)¶
Назначение: инкапсулировать выбор и создание конкретного объекта в одной функции/методе, чтобы клиент не зависел от конкретных классов. Когда применять: когда логика выбора реализации по параметру (например, создание нужного клиента хранилища make_storage("s3")) повторяется и её хочется собрать в одном месте. Формально не входит в каталог GoF, но служит базой для фабричного метода.
3. Фабричный метод (Factory Method)¶
Назначение: определить интерфейс для создания объекта, но делегировать выбор конкретного класса подклассам, переопределяющим метод-фабрику. Когда применять: когда базовый алгоритм фиксирован, но тип создаваемого продукта должен задаваться наследником (например, разные Logger-ы создают разные форматтеры записей).
4. Абстрактная фабрика (Abstract Factory)¶
Назначение: предоставить интерфейс для создания целого семейства связанных объектов без привязки к их конкретным классам. Когда применять: когда нужно гарантировать совместимость группы продуктов и переключать всё семейство разом (например, фабрика «клиентов облака» сразу даёт согласованные Compute, Storage, Network для AWS или GCP).
5. Строитель (Builder)¶
Назначение: пошагово конструировать сложный объект, отделяя процесс сборки от итогового представления. Когда применять: когда у объекта много опциональных параметров и комбинаций (например, конфиг HTTP-клиента или запрос с таймаутами, ретраями, заголовками), а телескопические конструкторы становятся нечитаемыми — отсюда популярный fluent-стиль b.WithTimeout(...).WithRetries(...).Build().
6. Прототип (Prototype)¶
Назначение: создавать новые объекты копированием (клонированием) существующего экземпляра-прототипа вместо вызова конструктора. Когда применять: когда инициализация объекта дорогая или конфигурация сложна, и проще склонировать уже настроенный образец, слегка изменив его (например, копия преднастроенного шаблона ресурса/манифеста).
7. Одиночка (Singleton)¶
Назначение: гарантировать, что у класса есть ровно один экземпляр, и предоставить к нему глобальную точку доступа. Когда применять: для разделяемых ресурсов в едином состоянии — пул соединений, конфигурация, логгер; в многопоточной среде требует потокобезопасной инициализации (в Go — sync.Once). Часто критикуется как скрытая глобальная зависимость, усложняющая тестирование.
Структурные
8. Адаптер (Adapter)¶
Назначение: преобразовать интерфейс существующего класса в интерфейс, ожидаемый клиентом, «обёрткой» поверх несовместимого API. Когда применять: при интеграции стороннего или legacy-кода, когда менять его нельзя, а вызывать нужно через общий интерфейс (например, обернуть SDK провайдера под свой интерфейс MetricsSink).
9. Мост (Bridge)¶
Назначение: разделить абстракцию и её реализацию на две независимые иерархии, чтобы они менялись отдельно друг от друга. Когда применять: когда есть два измерения вариативности, и комбинировать их наследованием — комбинаторный взрыв классов (например, «тип уведомления» × «канал доставки»: абстракция Notification держит ссылку на реализацию Sender).
10. Компоновщик (Composite)¶
Назначение: объединить объекты в древовидную структуру и работать с отдельным элементом и группой единообразно, через общий интерфейс. Когда применять: для иерархий «часть–целое», где клиент не должен различать лист и контейнер (например, файловая система, дерево ресурсов или вложенные группы правил).
11. Фасад (Facade)¶
Назначение: предоставить единый упрощённый интерфейс к сложной подсистеме, скрыв её внутреннее устройство. Когда применять: когда набор низкоуровневых вызовов нужно свести к понятной операции верхнего уровня (например, метод Deploy() поверх десятка шагов сборки, выкладки и проверки здоровья).
12. Приспособленец (Flyweight)¶
Назначение: экономить память, разделяя общее (внутреннее) состояние между множеством мелких объектов и вынося различающееся (внешнее) состояние наружу. Когда применять: когда объектов очень много и они дублируют одинаковые данные (например, переиспользуемые описания типов метрик/лейблов для миллионов точек данных).
13. Заместитель (Proxy)¶
Назначение: подставить вместо реального объекта объект-заместитель с тем же интерфейсом, чтобы контролировать доступ к нему. Когда применять: для отложенной (ленивой) инициализации, кэширования, контроля доступа или логирования вокруг дорогого/удалённого ресурса (например, кэширующий или защищающий прокси перед удалённым API).
Поведенческие
14. Состояние (State)¶
Назначение: позволить объекту менять поведение при изменении внутреннего состояния так, будто меняется его класс — каждое состояние выносится в отдельный объект. Когда применять: когда поведение объекта сильно зависит от состояния и иначе разрастаются ветвления if/switch (например, жизненный цикл задачи: Pending → Running → Failed/Done).
15. Шаблонный метод (Template Method)¶
Назначение: задать в базовом классе скелет алгоритма, оставив переопределение отдельных шагов подклассам. Когда применять: когда несколько вариантов отличаются лишь частью шагов общего процесса (например, базовый пайплайн extract → transform → load, где наследники переопределяют только transform).
16. Стратегия (Strategy)¶
Назначение: определить семейство взаимозаменяемых алгоритмов, инкапсулировать каждый и сделать их подменяемыми во время выполнения. Когда применять: когда нужно выбирать способ решения задачи на лету, не плодя условия (например, стратегия ретраев или балансировки, передаваемая в клиент как параметр/функция).
17. Посетитель (Visitor)¶
Назначение: вынести операцию из иерархии объектов в отдельный класс-посетитель, чтобы добавлять новые операции без изменения самих объектов. Когда применять: когда структура классов стабильна, а набор операций над ней часто расширяется (например, обход AST/дерева конфигурации разными анализаторами и валидаторами).
18. Наблюдатель (Observer)¶
Назначение: установить отношение «один-ко-многим», при котором изменение состояния субъекта автоматически уведомляет всех подписчиков. Когда применять: для реактивных уведомлений об изменениях без жёсткой связности (например, рассылка событий о смене статуса сервиса подписчикам — алертам, дашбордам, логам).
19. Хранитель (Memento)¶
Назначение: сохранять и восстанавливать внутреннее состояние объекта, не нарушая его инкапсуляции. Когда применять: когда нужны откат/undo или снимки состояния (например, сохранение состояния конфигурации перед изменением для отката при сбое).
20. Цепочка обязанностей (Chain of Responsibility)¶
Назначение: передавать запрос по цепочке обработчиков, пока один из них его не обработает, ослабляя связь отправителя и получателя. Когда применять: когда запрос может обрабатываться разными способами в зависимости от условий (например, конвейер middleware HTTP-запроса: аутентификация → лимиты → логирование → обработчик).
21. Команда (Command)¶
Назначение: инкапсулировать запрос в виде объекта, что позволяет параметризовать, ставить в очередь, логировать и отменять операции. Когда применять: когда действия нужно откладывать, ставить в очередь, повторять или отменять (например, очередь задач/джобов, где каждая команда — самодостаточный объект с методом Execute()).
22. Посредник (Mediator)¶
Назначение: ввести объект-посредник, через который общаются компоненты, чтобы они не ссылались друг на друга напрямую и снизилась связность. Когда применять: когда множество объектов взаимодействуют по принципу «все со всеми» и связи становятся неуправляемыми (например, координатор, через который компоненты системы обмениваются командами вместо прямых вызовов).