Паттерны проектирования
SOLID, паттерны GoF, RAII, DRY/KISS/YAGNI, Singleton, Factory, Observer, Visitor, State и PIMPL.
31 вопросов
JuniorКодОчень частоРеализуйте паттерн Singleton (потокобезопасный, по Майерсу).
Реализуйте паттерн Singleton (потокобезопасный, по Майерсу).
Singleton Майерса использует локальную static-переменную: C++11 гарантирует, что magic statics инициализируются ровно один раз потокобезопасно. Удалите копирование/перемещение для запрета лишних экземпляров.
Открыть задачу →Типичные ошибки
- ✗Возвращать указатель вместо ссылки из
instance()— вызывающие могут случайно удалить указатель или хранить nullptr; возвращайте ссылку - ✗Реализовывать Singleton хранением статического указателя и вызовом
new— это подход до C++11; он требует мьютекса и менее чист, чем static Майерса - ✗Не удалять операторы копирования/перемещения — без
= deleteвызывающий может случайно скопировать синглтон в локальную переменную, создав второй экземпляр
Уточняющие вопросы
- →Как сбросить Singleton в юнит-тестах без открытого метода
reset()? - →Что такое 'проблема мёртвой ссылки' с Singleton и как её решает Phoenix Singleton?
JuniorДизайнОчень частоКласс должен варьировать один аспект поведения — например, как он сортирует или как считает цену заказа — и этот выбор нужно делать в рантайме, не переписывая сам класс. Какой поведенческий паттерн проектирования описывает эту задачу, в чём его суть и как он идиоматично выражается в современном C++?
Класс должен варьировать один аспект поведения — например, как он сортирует или как считает цену заказа — и этот выбор нужно делать в рантайме, не переписывая сам класс. Какой поведенческий паттерн проектирования описывает эту задачу, в чём его суть и как он идиоматично выражается в современном C++?
Strategy инкапсулирует взаимозаменяемый алгоритм за единым интерфейсом, чтобы хост менял поведение в рантайме. Классика: virtual execute(); в современном C++: std::function или шаблоны.
Типичные ошибки
- ✗Использовать виртуальный вызов, когда стратегия не меняется в рантайме — оплата зря
- ✗Хранить сырой указатель на функцию, если нужно захватить состояние — используйте
std::functionили callable-объект - ✗Жить бизнес-логике в хосте вместо стратегии — паттерн теряет смысл
Уточняющие вопросы
- →Когда стоимость
std::functionважна (SOO, indirect call)? - →Как стратегия как параметр шаблона позволяет inline?
MiddleТеорияОчень частоПреимущества композиции над наследованием.
Преимущества композиции над наследованием.
Композиция ('has-a') лучше наследования ('is-a') слабой связанностью, отсутствием хрупкого базового класса, удобным мокированием, плоскими иерархиями. Наследование корректно лишь при выполнении LSP.
Типичные ошибки
- ✗Переиспользовать код через наследование вместо композиции — переиспользование кода не является веским основанием для наследования; используйте композицию + делегирование
- ✗Компоновать всё и никогда не наследоваться — некоторые абстракции (полиморфные иерархии) действительно требуют наследования; принцип 'предпочитай', а не 'всегда'
- ✗Использовать private-наследование как 'композицию с доступом к внутренностям' — обычно это запах дизайна; предпочтительна композиция с членом-переменной
Уточняющие вопросы
- →Как паттерн Mixin использует множественное наследование для переиспользования кода без проблемы ромба?
- →В каком сценарии private-наследование даёт преимущество перед обычным членом?
MiddleТеорияОчень частоРазница между Abstract Factory и Factory Method.
Разница между Abstract Factory и Factory Method.
Factory Method: подкласс переопределяет виртуальный create() для одного продукта; на наследовании. Abstract Factory: фабрика создаёт семейство связанных продуктов; на композиции.
Типичные ошибки
- ✗Называть любую создающую объекты функцию 'Factory Pattern' — паттерн подразумевает полиморфизм; обычная
makeWidget()— просто фабричная функция, а не GoF-паттерн - ✗Использовать Abstract Factory когда варьируется только один продукт — вводит лишнюю абстракцию; Factory Method проще
- ✗Делать фабричные методы
static— статические методы не переопределяются (нет виртуальной диспетчеризации), что нивелирует смысл Factory Method
Уточняющие вопросы
- →Как реализовать типобезопасный реестр объектов (самоregistrирующиеся фабрики) в C++?
- →Что такое идиома виртуального конструктора в C++ и как она соотносится с Factory Method?
MiddleТеорияОчень частоЧто такое паттерн Observer?
Что такое паттерн Observer?
Observer описывает зависимость один ко многим: при изменении состояния субъект уведомляет всех зарегистрированных наблюдателей. Применяется для событий, GUI, signal/slot.
Типичные ошибки
- ✗Хранить сырые указатели на наблюдателей без управления временем жизни — уничтоженный до отписки наблюдатель оставляет субъект с висячим указателем; используйте
weak_ptr - ✗Уведомлять наблюдателей удерживая блокировку субъекта — колбэк наблюдателя может вызывать субъект, вызывая дедлок или реентрантную модификацию
- ✗Забывать о thread safety — добавление/удаление наблюдателей и вызов
notify()из разных потоков требует синхронизации списка наблюдателей
Уточняющие вопросы
- →Как механизм signal/slot Qt реализует Observer без общего базового класса?
- →Как реализовать потокобезопасную шину событий с помощью Observer в C++?
MiddleТеорияОчень частоЧто такое SOLID? Объясните каждый принцип.
Что такое SOLID? Объясните каждый принцип.
S — один повод для изменения. O — открыт для расширения, закрыт для модификации. L — подтипы пригодны вместо базового класса. I — разделяйте толстые интерфейсы. D — зависьте от абстракций.
Типичные ошибки
- ✗Воспринимать SOLID как абсолютные правила — LSP и ISP иногда конфликтуют с производительностью; документируйте компромисс при намеренном отступлении
- ✗Разбивать класс по SRP когда обязанности всегда меняются вместе — если два concern всегда развиваются вместе, они принадлежат одному классу
- ✗Трактовать OCP как 'никогда не изменять существующий код' — принцип применяется на границе модуля/интерфейса; рефакторинг внутренностей допустим
Уточняющие вопросы
- →Покажите пример на C++, где нарушение LSP вызывает runtime-баг.
- →Как принцип инверсии зависимостей соотносится с фреймворками dependency injection?
JuniorТеорияЧастоОпишите Singleton, Strategy, Template-Method, Decorator.
Опишите Singleton, Strategy, Template-Method, Decorator.
Singleton: один экземпляр через static-локаль Майерса. Strategy: взаимозаменяемые алгоритмы. Template Method: базовый класс задаёт скелет, virtual-шаги в производных. Decorator: добавляет поведение.
Типичные ошибки
- ✗Использовать Singleton, когда нужен просто один экземпляр при запуске — многим сервисам не нужна гарантированная глобальная уникальность; предпочтительна dependency injection, которую проще тестировать
- ✗Вызывать virtual-шаги Template Method из конструктора базового класса — vtable ещё не установлена, диспетчеризация в производный не работает; вызывайте их только из обычных методов
- ✗Цеплять Decorator-ы без учёта времени жизни объекта — если внутренний объект уничтожается, пока декораторы держат сырые ссылки, они становятся висячими; используйте shared_ptr
Уточняющие вопросы
- →Чем идиома NVI отличается от подхода с чисто виртуальным интерфейсом?
- →Как реализовать составной фильтрующий конвейер с Decorator в C++?
JuniorТеорияЧастоЧто такое паттерны проектирования и зачем они используются?
Что такое паттерны проектирования и зачем они используются?
Паттерны проектирования — именованные, повторно используемые решения повторяющихся задач: шаблоны структурирования классов и связей, а не копируемые фрагменты кода.
Типичные ошибки
- ✗Избыточное применение паттернов — не каждая задача требует паттерна; лишние уровни абстракции ухудшают читаемость и производительность
- ✗Путать паттерны с алгоритмами — алгоритм сортировки решает конкретную вычислительную задачу; паттерн описывает структурное отношение между классами
- ✗Рассматривать паттерны как обязательную архитектуру — начинайте с простейшего рабочего кода и вводите паттерн только тогда, когда проблема, которую он решает, действительно появляется
Уточняющие вопросы
- →Какие паттерны проектирования использует сама стандартная библиотека C++?
- →Что такое анти-паттерны? Приведите примеры в C++.
JuniorДизайнЧастоПоведенческий паттерн Iterator даёт единообразный способ обходить коллекцию, не раскрывая, как она хранит элементы. Объясните, как эту идею реализует C++ STL — как контейнеры предоставляют обход и как алгоритмы его потребляют — и чем это отличается от классической объектно-ориентированной формы паттерна, построенной на иерархии классов.
Поведенческий паттерн Iterator даёт единообразный способ обходить коллекцию, не раскрывая, как она хранит элементы. Объясните, как эту идею реализует C++ STL — как контейнеры предоставляют обход и как алгоритмы его потребляют — и чем это отличается от классической объектно-ориентированной формы паттерна, построенной на иерархии классов.
STL реализует Iterator как обобщённый концепт, а не иерархию: контейнеры дают begin()/end(), возвращающие объекты концепта итератора. Алгоритмы работают с парами независимо от контейнера.
Типичные ошибки
- ✗Реализовать контейнер без итераторов — несовместимо с range-for и STL-алгоритмами
- ✗Итератор, не удовлетворяющий заявленной категории (forward iterator, проваливающий multipass)
- ✗Смешивать итераторы от разных контейнеров — UB
Уточняющие вопросы
- →Как sentinel в C++20 ослабляет требование совпадения типов begin и end?
- →Почему есть
std::list::sortвместо простоstd::sort?
MiddleДизайнЧастоУ вас есть класс, поведение которого нужно, но интерфейс которого не совпадает с тем, что ожидает клиент — например, сторонний логгер с другим набором методов, чем вызывает ваш код. Объясните, что такое структурный паттерн Adapter и как он решает эту задачу, и затем сопоставьте его со структурным паттерном Decorator, сосредоточившись на том, что каждый меняет в обёрнутом объекте: интерфейс, поведение или и то, и другое.
У вас есть класс, поведение которого нужно, но интерфейс которого не совпадает с тем, что ожидает клиент — например, сторонний логгер с другим набором методов, чем вызывает ваш код. Объясните, что такое структурный паттерн Adapter и как он решает эту задачу, и затем сопоставьте его со структурным паттерном Decorator, сосредоточившись на том, что каждый меняет в обёрнутом объекте: интерфейс, поведение или и то, и другое.
Adapter оборачивает объект, чтобы он подходил под другой интерфейс клиента: то же поведение, иной интерфейс. Decorator добавляет поведение вокруг того же интерфейса.
Типичные ошибки
- ✗Путать Adapter и Facade — Facade упрощает подсистему; Adapter перенацеливает один объект
- ✗Публично наследоваться от adaptee — наружу торчит весь его интерфейс, контракт адаптера сломан
- ✗Забывать прокидывать const-корректность в адаптированных методах
Уточняющие вопросы
- →Когда object adapter, а когда class adapter (private inheritance)?
- →Как
std::functionработает как адаптер?
MiddleДизайнЧастоКонструирование объекта со множеством полей — часть из которых опциональны — через один конструктор приводит к длинным, легко путаемым по порядку спискам аргументов. Что такое порождающий паттерн Builder, как он отделяет процесс конструирования от готового объекта и когда стоит выбрать его вместо конструктора со множеством аргументов?
Конструирование объекта со множеством полей — часть из которых опциональны — через один конструктор приводит к длинным, легко путаемым по порядку спискам аргументов. Что такое порождающий паттерн Builder, как он отделяет процесс конструирования от готового объекта и когда стоит выбрать его вместо конструктора со множеством аргументов?
Builder отделяет конструирование от представления: текучий builder накапливает настройки, затем build() создаёт целевой объект. Применяйте для множества параметров и опциональных полей.
Типичные ошибки
- ✗Добавлять builder для структуры из 2 полей — over-engineering
- ✗Возвращать builder по значению из каждого setter — копии; возвращайте по ссылке (
Builder&) - ✗Забывать валидацию cross-field инвариантов в
build()— наружу выходят невалидные объекты
Уточняющие вопросы
- →Чем Builder отличается от named-parameter идиомы (designated initialisers в C++20)?
- →Когда делать Builder другом целевого класса?
MiddleТеорияЧастоЧто такое порождающие, структурные и поведенческие паттерны? Приведите примеры.
Что такое порождающие, структурные и поведенческие паттерны? Приведите примеры.
Порождающие управляют созданием (Singleton, Factory). Структурные компонуют объекты (Adapter, Decorator). Поведенческие описывают взаимодействия (Observer, Strategy, Iterator).
Типичные ошибки
- ✗Запоминать все 23 GoF паттерна без понимания задачи, которую каждый решает — на интервью проверяют распознавание проблем, а не названий паттернов
- ✗Путать Factory Method с Abstract Factory — Factory Method использует наследование (подкласс переопределяет); Abstract Factory использует композицию (объект содержит фабрику)
- ✗Использовать Proxy и Decorator как взаимозаменяемые — Proxy контролирует доступ к тому же интерфейсу; Decorator добавляет новую функциональность интерфейса
Уточняющие вопросы
- →Как паттерн Iterator реализован в C++ через begin/end и range-for?
- →Где паттерн Command встречается в GUI-фреймворках и системах undo/redo?
MiddleДизайнЧастоРедактору нужны undo/redo, постановка операций в очередь на потом и запись последовательностей как макросов — при этом код, инициирующий действие, должен оставаться независимым от кода, выполняющего его. Что такое поведенческий паттерн Command и какие задачи позволяет решить превращение запроса в полноценный объект, недоступные обычному вызову функции?
Редактору нужны undo/redo, постановка операций в очередь на потом и запись последовательностей как макросов — при этом код, инициирующий действие, должен оставаться независимым от кода, выполняющего его. Что такое поведенческий паттерн Command и какие задачи позволяет решить превращение запроса в полноценный объект, недоступные обычному вызову функции?
Command инкапсулирует запрос как объект, развязывая отправителя и получателя. У команды есть execute() (часто undo()). Применяется для undo/redo, макросов, асинхронных очередей.
Типичные ошибки
- ✗Не сохранить достаточно состояния для undo — операция теряет данные
- ✗Захватывать ссылки в
std::function-команды — dangling, когда очередь выполняется позже - ✗Путать command и event — события описывают что произошло, команды — что сделать
Уточняющие вопросы
- →Как реализовать транзакцию с rollback через команды?
- →Чем Command отличается от Memento для undo?
MiddleДизайнЧастоВы хотите добавить объекту поведение — например, буферизацию или сжатие вокруг потока данных — в рантайме, накладывая несколько таких добавлений в любой комбинации, не плодя подкласс на каждую комбинацию. Что такое структурный паттерн Decorator и как реализовать его в современном C++ так, чтобы декорированный объект оставался применим везде, где работал исходный?
Вы хотите добавить объекту поведение — например, буферизацию или сжатие вокруг потока данных — в рантайме, накладывая несколько таких добавлений в любой комбинации, не плодя подкласс на каждую комбинацию. Что такое структурный паттерн Decorator и как реализовать его в современном C++ так, чтобы декорированный объект оставался применим везде, где работал исходный?
Decorator динамически добавляет поведение, оборачивая компонент с общим интерфейсом. Конкретные декораторы переопределяют методы вокруг делегированного вызова.
Типичные ошибки
- ✗Забыть виртуальный деструктор на
Component— утечка при delete через base - ✗Стэкировать декораторы, не продумав владение — кто кого удаляет
- ✗Выбирать decorator вместо наследования для compile-time поведения — лишняя indirection в рантайме
Уточняющие вопросы
- →Чем static-decorator на CRTP отличается по производительности?
- →Когда вложенность декораторов превращается в кошмар отладки?
MiddleТеорияЧастоЧто такое Dependency Injection? Пример.
Что такое Dependency Injection? Пример.
DI — техника, при которой объект получает зависимости снаружи, а не создаёт их. Формы: конструктор (предпочтительно), сеттер, интерфейс. Плюсы: тестируемость, развязанность.
Типичные ошибки
- ✗Путать DI с DI-фреймворком — DI это принцип проектирования; передача ссылки в конструктор — это DI без какого-либо фреймворка
- ✗Использовать service locator вместо DI — service locator скрывает зависимости (вызывающие обращаются к глобальному реестру); DI делает их явными в сигнатуре конструктора
- ✗Внедрять конкретные типы — нивелирует смысл; всегда внедряйте через абстрактный интерфейс или параметр шаблона, чтобы зависимость можно было подменить
Уточняющие вопросы
- →Как реализовать DI в C++ без виртуальной диспетчеризации (инъекция политик через шаблоны)?
- →Что такое IoC-контейнер и какие C++ библиотеки его предоставляют?
MiddleТеорияЧастоЧто такое PIMPL? Преимущества и недостатки.
Что такое PIMPL? Преимущества и недостатки.
PIMPL прячет приватные члены в Impl в .cpp; заголовок открывает только unique_ptr<Impl>. Плюсы: быстрая компиляция, стабильность ABI. Минусы: heap-аллокация, индирекция, ручное Правило пяти.
Типичные ошибки
- ✗Дефолтить деструктор в заголовке вместо
.cpp, гдеImplполный —~unique_ptr<Impl>()тогда срабатывает при неполномImpl, это static_assert/жёсткая ошибка - ✗Использовать PIMPL для небольших типов-значений — выделение на куче нивелирует преимущества производительности; PIMPL для больших классов с множеством зависимостей
- ✗Забывать реализовать копирующий конструктор —
unique_ptrне копируемый; необходимо явно глубоко копироватьImpl, если нужна семантика копирования
Уточняющие вопросы
- →Как PIMPL влияет на бинарную совместимость (стабильность ABI) при поставке C++ библиотеки?
- →В чём разница между PIMPL и паттерном Bridge?
MiddleТеорияЧастоКаковы недостатки Singleton? Когда он уместен?
Каковы недостатки Singleton? Когда он уместен?
Недостатки: скрытое глобальное состояние, сложность тестирования, тесная связанность, потокобезопасная ленивая инициализация, порядок уничтожения. Уместен лишь при реальном ограничении на один экземпляр (устройство, лог) без подмены.
Типичные ошибки
- ✗Хранить бизнес-логику в Singleton вместо явной передачи — делает поток непрозрачным и не допускает параллелизм с несколькими экземплярами
- ✗Двойная проверка блокировки с сырым указателем и неатомарной проверкой — классический UB до C++11; используйте Meyer's static или
std::call_once - ✗Не учитывать порядок уничтожения когда синглтоны зависят друг от друга — деструктор, обращающийся к уже уничтоженному синглтону, является UB
Уточняющие вопросы
- →Как Singleton Майерса гарантирует потокобезопасную инициализацию в C++11?
- →Что такое паттерн Monostate и как он сравнивается с Singleton?
MiddleТеорияЧастоЧто такое конечный автомат? Паттерн State.
Что такое конечный автомат? Паттерн State.
FSM — конечное множество состояний, событийные переходы и действия входа/выхода. Паттерн State моделирует его классами с общим интерфейсом; контекст хранит указатель и заменяет его при переходе.
Типичные ошибки
- ✗Хранить логику переходов внутри контекста — контекст превращается в гигантский switch; переносите логику переходов каждого состояния в класс состояния
- ✗Не обрабатывать явно недопустимые переходы — событие в неожиданном состоянии должно игнорироваться с логом, или вызывать исключение/assert; молчаливая ошибка скрывает баги
- ✗Выделять объекты состояний на куче при каждом переходе — состояния часто не имеют состояния; используйте одиночные экземпляры состояний (статический flyweight) для избежания выделений
Уточняющие вопросы
- →Что такое иерархический конечный автомат (HSM) и когда он нужен?
- →Чем
boost::smlотличается от рукописного FSM с точки зрения накладных расходов?
MiddleДизайнЧастоИ поведенческий паттерн Template Method, и Strategy позволяют держать общий алгоритм фиксированным, варьируя отдельные шаги. Объясните, что такое Template Method, и затем сопоставьте его со Strategy — в частности, как каждый из них даёт варьировать изменяемые части и на какой языковой механизм C++ каждый опирается.
И поведенческий паттерн Template Method, и Strategy позволяют держать общий алгоритм фиксированным, варьируя отдельные шаги. Объясните, что такое Template Method, и затем сопоставьте его со Strategy — в частности, как каждый из них даёт варьировать изменяемые части и на какой языковой механизм C++ каждый опирается.
Template Method фиксирует скелет алгоритма в базовом классе, отдавая шаги виртуальным методам производных классов. Strategy выносит весь алгоритм наружу через композицию. Template Method опирается на наследование.
Типичные ошибки
- ✗Делать сам шаблонный метод виртуальным — не нужно; виртуальны только шаги
- ✗Вызывать виртуальные методы в конструкторе базового класса — диспетчеризация в производный не работает
- ✗Применять Template Method, когда производный класс один — преждевременная абстракция
Уточняющие вопросы
- →Почему виртуальная диспетчеризация ломается в конструкторе базового класса?
- →Как CRTP реализует Template Method без рантайм-стоимости?
MiddleДизайнИногдаВходящий запрос должен пройти через ряд этапов обработки — например, конвейер HTTP-middleware — где каждый этап может обработать его, преобразовать или передать дальше, а отправитель не должен знать, какой этап в итоге им займётся. Что такое поведенческий паттерн Chain of Responsibility и как его идиоматично реализовать в C++?
Входящий запрос должен пройти через ряд этапов обработки — например, конвейер HTTP-middleware — где каждый этап может обработать его, преобразовать или передать дальше, а отправитель не должен знать, какой этап в итоге им займётся. Что такое поведенческий паттерн Chain of Responsibility и как его идиоматично реализовать в C++?
Chain of Responsibility передаёт запрос по цепочке обработчиков; каждый обрабатывает или пересылает дальше. Используется для middleware, фильтрации событий, цепочек логирования.
Типичные ошибки
- ✗Не предусмотреть финальный обработчик — запрос молча игнорируется
- ✗Циклы в цепочке — бесконечная передача
- ✗Мутировать запрос так, что сломаются предположения нижестоящих
Уточняющие вопросы
- →Как HTTP middleware (boost.beast / cpp-httplib) реализует этот паттерн?
- →Сравните Chain of Responsibility с switch по типам сообщений.
MiddleДизайнИногдаПрограмма держит миллионы мелких объектов, разделяющих много одинаковых, неизменных данных — скажем, глифы в раскладке текста или спрайты, переиспользующие одну текстуру — и дублирование состояния раздувает память. Что такое структурный паттерн Flyweight, как он разделяет состояние объекта, чтобы такое совместное использование было безопасным, и где он встречается в реальном C++?
Программа держит миллионы мелких объектов, разделяющих много одинаковых, неизменных данных — скажем, глифы в раскладке текста или спрайты, переиспользующие одну текстуру — и дублирование состояния раздувает память. Что такое структурный паттерн Flyweight, как он разделяет состояние объекта, чтобы такое совместное использование было безопасным, и где он встречается в реальном C++?
Flyweight уменьшает память, разделяя неизменяемое внутреннее состояние между объектами; внешнее состояние передаётся вызывающим. Примеры: интернирование строк, кеши глифов, общая текстура.
Типичные ошибки
- ✗Разделять мутабельное состояние — гонки данных / aliasing-баги
- ✗Применять Flyweight там, где накладные расходы на
shared_ptrбольше сэкономленных байт - ✗Забывать, что пул flyweight'ов должен пережить всех ссылающихся
Уточняющие вопросы
- →Как реализовать интернирование строк в многопоточной программе?
- →Сравните flyweight и COW (copy-on-write) строки.
MiddleДизайнИногдаВ диалоге, где множество виджетов должны реагировать друг на друга — включать, выключать, обновлять один другого — связывание каждого компонента с каждым порождает клубок из N×N прямых ссылок, который тяжело менять. Что такое поведенческий паттерн Mediator и как он перестраивает эти связи «многие-ко-многим», чтобы снизить связанность?
В диалоге, где множество виджетов должны реагировать друг на друга — включать, выключать, обновлять один другого — связывание каждого компонента с каждым порождает клубок из N×N прямых ссылок, который тяжело менять. Что такое поведенческий паттерн Mediator и как он перестраивает эти связи «многие-ко-многим», чтобы снизить связанность?
Mediator централизует коммуникацию компонентов: вместо N×N прямых ссылок каждый знает только посредника. Применяется в UI, чат-серверах, авиадиспетчеризации.
Типичные ошибки
- ✗Класть бизнес-логику компонентов в посредник — он должен оркестровать, а не реализовывать
- ✗Циклическое владение: посредник владеет компонентами, компоненты — посредником, без weak
- ✗Путать Mediator и Observer — Mediator это двунаправленная N-N оркестрация; Observer — one-to-many уведомление
Уточняющие вопросы
- →Как избежать превращения посредника в god-object?
- →Сравните Mediator с шиной событий.
MiddleДизайнИногдаИногда нужен новый объект — копия существующего, но вызывающий держит лишь указатель на базовый класс и не знает конкретный тип, а построение с нуля обошлось бы дорого. Что такое порождающий паттерн Prototype, какую задачу он решает и как он обычно реализуется в C++, чтобы копирование нужного конкретного типа работало через указатель на базовый класс?
Иногда нужен новый объект — копия существующего, но вызывающий держит лишь указатель на базовый класс и не знает конкретный тип, а построение с нуля обошлось бы дорого. Что такое порождающий паттерн Prototype, какую задачу он решает и как он обычно реализуется в C++, чтобы копирование нужного конкретного типа работало через указатель на базовый класс?
Prototype создаёт объекты клонированием существующего, а не конструированием с нуля — полезно при дорогой конфигурации. Обычно виртуальный clone(), возвращающий std::unique_ptr<Base>.
Типичные ошибки
- ✗Возвращать сырой указатель из
clone()— клиент должен помнить delete; используйтеunique_ptr - ✗Забыть переопределить
clone()в производном классе — slicing при копии - ✗Реализовать clone как
make_unique<Derived>(*this)в базовом классе — не скомпилируется для абстрактной; нужно override в каждом типе
Уточняющие вопросы
- →Как CRTP сокращает boilerplate
clone()? - →Почему Prototype полезен при дорогих конструкторах (парсинг конфига)?
MiddleКодИногдаНапишите кроссплатформенную программу, гарантирующую запуск только одного экземпляра.
Напишите кроссплатформенную программу, гарантирующую запуск только одного экземпляра.
Два канонических подхода: на POSIX — эксклюзивный flock(LOCK_EX | LOCK_NB) на PID-файле; на Windows — именованный мьютекс через CreateMutex с проверкой ERROR_ALREADY_EXISTS. Оба освобождают блокировку при выходе.
Типичные ошибки
- ✗Читать PID из файла и проверять, жив ли этот процесс — содержит гонку TOCTOU; используйте файловую блокировку, которая атомарна
- ✗Не записывать текущий PID в файл блокировки — затрудняет отладку; нельзя определить, какой экземпляр держит блокировку
- ✗Забывать закрывать fd перед exec() в модели fork/exec — блокировка будет освобождена; используйте O_CLOEXEC или fcntl(F_SETFD, FD_CLOEXEC)
Уточняющие вопросы
- →Как передать сигнал уже запущенному экземпляру для вывода его окна на передний план в Windows?
- →Что происходит с файловой блокировкой, если процесс убивается SIGKILL?
MiddleТеорияИногдаЧто такое паттерн Visitor и когда его использовать?
Что такое паттерн Visitor и когда его использовать?
Visitor отделяет алгоритм от структуры объектов: accept(v) вызывает v.visit(*this) — двойная диспетчеризация без dynamic_cast. Применяйте при стабильной иерархии и меняющихся операциях.
Типичные ошибки
- ✗Применять Visitor к часто меняющейся иерархии — новый тип элемента требует правки каждого visitor; если элементы меняются чаще операций, предпочтительна виртуальная диспетчеризация
- ✗Забывать добавить новый элемент в каждый существующий visitor — компилятор не обнаружит отсутствующую перегрузку, если базовый Visitor не объявляет её как pure virtual
- ✗Использовать Visitor там, где
dynamic_castбыл бы понятнее — Visitor оправдан для N операций × M типов; для 1-2 операцийdynamic_castпроще
Уточняющие вопросы
- →Как
std::visitсstd::variantдостигает исчерпывающей проверки во время компиляции? - →Что такое двойная диспетчеризация и почему C++ не поддерживает её нативно?
SeniorДизайнИногдаДолгоживущий god-объект управляет всем своим поведением через один гигантский switch по полю текущего режима — каждый метод ветвится по одному и тому же enum, и добавление режима означает правки повсюду. Нужно перестроить его так, чтобы поведение каждого режима было изолировано и новый режим можно было добавить, не трогая остальные, соблюдая ограничения: (1) изменение выпускается малыми шагами — после каждого шага код компилируется, проходит тесты и пригоден к релизу; (2) никакой big-bang ветки, неделями остающейся невливаемой; (3) существующее поведение должно быть сохранено и подтверждено до начала любой перестройки; (4) ни на одном промежуточном шаге система не должна оставаться сломанной или непокрытой тестами. Опишите миграцию, которую вы проведёте, и порядок шагов.
Долгоживущий god-объект управляет всем своим поведением через один гигантский switch по полю текущего режима — каждый метод ветвится по одному и тому же enum, и добавление режима означает правки повсюду. Нужно перестроить его так, чтобы поведение каждого режима было изолировано и новый режим можно было добавить, не трогая остальные, соблюдая ограничения: (1) изменение выпускается малыми шагами — после каждого шага код компилируется, проходит тесты и пригоден к релизу; (2) никакой big-bang ветки, неделями остающейся невливаемой; (3) существующее поведение должно быть сохранено и подтверждено до начала любой перестройки; (4) ни на одном промежуточном шаге система не должна оставаться сломанной или непокрытой тестами. Опишите миграцию, которую вы проведёте, и порядок шагов.
Сначала зафиксируйте поведение характеризующими тестами. Затем введите интерфейс State и извлекайте по одному классу состояния за раз, делая так, чтобы старый switch делегировал ему этот case, пока остальные case не тронуты — каждое извлечение компилируется, проходит тесты и выпускается. Когда каждый case стал классом состояния, замените switch сменой указателя в контексте. Переходы переносятся в классы состояний в последнюю очередь.
Типичные ошибки
- ✗Делать big-bang переписывание всего switch, оставляя ветку невыпускаемой и непроверяемой неделями
- ✗Рефакторить до написания характеризующих тестов, из-за чего регрессия поведения молча проскакивает
- ✗Переносить логику переходов в состояния до извлечения всех case, смешивая два рефакторинга и затрудняя проверку каждого шага
Уточняющие вопросы
- →Почему переходы должны переноситься в классы состояний только после извлечения каждого case?
- →Чем характеризующие тесты отличаются от юнит-тестов для готового дизайна?
SeniorДебаггингИногдаПочему наивная реализация GoF Observer становится опасной для времени жизни и многопоточности?
Почему наивная реализация GoF Observer становится опасной для времени жизни и многопоточности?
Субъект хранит сырые указатели на наблюдателей, которыми не владеет, поэтому уничтоженный, но не отписанный наблюдатель превращается в висячий вызов. Реентрантный notify() — наблюдатель подписывается или уничтожает себя внутри своего колбэка — инвалидирует итератор посреди цикла. Решение: weak_ptr на наблюдателей, копия-снимок списка перед обходом и отложенные add/remove.
Типичные ошибки
- ✗Считать, что наблюдатель всегда живёт дольше субъекта — в реальном коде наблюдатели уничтожаются первыми и оставляют висячий указатель в списке
- ✗Обходить живой контейнер наблюдателей напрямую, из-за чего наблюдатель, отписавшийся внутри своего колбэка, инвалидирует итератор
- ✗Удерживать мьютекс субъекта на весь цикл
notify(), из-за чего колбэк наблюдателя, вызывающий субъект, приводит к самодедлоку
Уточняющие вопросы
- →Как копия-снимок списка наблюдателей взаимодействует с наблюдателем, отписывающимся во время notify()?
- →Почему
weak_ptrпредпочтительнее явного контракта 'отписка в деструкторе'?
SeniorДизайнИногдаСпроектируйте систему плагинов для хост-приложения на C++: плагины поставляются отдельными разделяемыми библиотеками (.so/.dll), обнаруживаются и загружаются в рантайме, а не линкуются при сборке, а реестр в хосте отслеживает загруженные. Жёсткое ограничение — бинарная совместимость: плагин, собранный другим компилятором или другой версией компилятора, чем хост, всё равно должен загружаться и корректно работать, а хост должен уметь создавать и уничтожать объекты плагина через эту границу, не полагаясь на C++ name mangling и хрупкую раскладку объектов. Опишите архитектуру: как хост и плагин договариваются о стабильном контракте, как плагин загружается и его объекты создаются и уничтожаются, и что никогда не должно пересекать границу.
Спроектируйте систему плагинов для хост-приложения на C++: плагины поставляются отдельными разделяемыми библиотеками (.so/.dll), обнаруживаются и загружаются в рантайме, а не линкуются при сборке, а реестр в хосте отслеживает загруженные. Жёсткое ограничение — бинарная совместимость: плагин, собранный другим компилятором или другой версией компилятора, чем хост, всё равно должен загружаться и корректно работать, а хост должен уметь создавать и уничтожать объекты плагина через эту границу, не полагаясь на C++ name mangling и хрупкую раскладку объектов. Опишите архитектуру: как хост и плагин договариваются о стабильном контракте, как плагин загружается и его объекты создаются и уничтожаются, и что никогда не должно пересекать границу.
Система плагинов C++ использует стабильную C ABI фабрику (extern C create_plugin), абстрактный IPlugin, динамическую загрузку через dlopen/LoadLibrary и реестр в хосте. Заголовок IPlugin должен оставаться бинарно-стабильным.
Типичные ошибки
- ✗Экспортировать C++ классы напрямую без C-функции-фабрики — искажённые имена различаются между компиляторами и даже версиями компиляторов; всегда используйте extern "C" на границе
- ✗Не выгружать плагины в обратном порядке — если плагин B зависит от плагина A и A выгружается первым, vtable B указывает на уничтоженный код
- ✗Передавать STL-контейнеры через границу плагина — раскладка
std::string/std::vectorможет различаться при разных рантаймах или флагах; используйте C-типы или указатели
Уточняющие вопросы
- →Как работает согласование версий между хостом и плагином при эволюции интерфейса IPlugin?
- →Что такое COM (Component Object Model) и как он решает проблемы C++ ABI для плагинов на Windows?
SeniorДизайнИногдаВам достаётся кодовая база, где почти каждый конкретный класс спрятан за собственным абстрактным интерфейсом с ровно одной реализацией — чисто виртуальные заголовки, лишняя косвенность и лишнее имя для навигации на каждый, — что вам объясняют как «следование SOLID». Разберитесь, даёт ли это что-нибудь на деле: объясните, как вы отличите настоящую точку вариативности от спекулятивной абстракции, решите, какие из этих интерфейсов стоит оставить, а какие убрать, и опишите безопасное изменение для тех, что стоит убрать. Чётко назовите критерий, по которому принимаете решение оставить-или-убрать, и то, когда абстракцию стоило бы вернуть позже.
Вам достаётся кодовая база, где почти каждый конкретный класс спрятан за собственным абстрактным интерфейсом с ровно одной реализацией — чисто виртуальные заголовки, лишняя косвенность и лишнее имя для навигации на каждый, — что вам объясняют как «следование SOLID». Разберитесь, даёт ли это что-нибудь на деле: объясните, как вы отличите настоящую точку вариативности от спекулятивной абстракции, решите, какие из этих интерфейсов стоит оставить, а какие убрать, и опишите безопасное изменение для тех, что стоит убрать. Чётко назовите критерий, по которому принимаете решение оставить-или-убрать, и то, когда абстракцию стоило бы вернуть позже.
Интерфейс с единственной реализацией — спекулятивная абстракция: он добавляет заголовок, виртуальный вызов и лишнее имя для навигации, но не даёт гибкости — DIP и OCP предназначены для настоящих точек вариативности, а не для каждого класса. Диагностика: посчитать число реализаций и реальных тестовых заглушек. Сворачивайте, встраивая интерфейс в его единственную реализацию и завися от конкретного типа; возвращайте абстракцию только когда вторая реализация действительно появится.
Типичные ошибки
- ✗Трактовать DIP как 'каждая зависимость должна быть интерфейсом', а не 'зависеть от абстракций в настоящих точках вариативности'
- ✗Держать интерфейс с одной реализацией 'для тестов', когда конкретный класс уже легко тестируется или фейк не даёт пользы
- ✗Называть встраивание интерфейса нарушением OCP, хотя OCP никогда не требовал абстракции без второй реализации
Уточняющие вопросы
- →Какая метрика лучше всего отличает спекулятивную абстракцию от настоящей точки вариативности?
- →Как сохранить тестируемость класса после сворачивания его интерфейса с одной реализацией?
SeniorТеорияИногдаКаковы компромиссы по ABI, размеру бинарника и инлайнингу между Strategy на этапе компиляции и в рантайме?
Каковы компромиссы по ABI, размеру бинарника и инлайнингу между Strategy на этапе компиляции и в рантайме?
Strategy на этапе компиляции — диспетчеризация через CRTP или std::variant — позволяет оптимизатору инлайнить алгоритм и девиртуализировать, но каждая стратегия — отдельный тип, поэтому раздувает бинарник за счёт инстанцирования шаблонов и зашивает выбор в ABI. Strategy в рантайме — виртуальный интерфейс — сохраняет один стабильный тип и компактный бинарник, позволяет менять поведение и плагины через границу ABI, но платит косвенным вызовом, который оптимизатор обычно не может заинлайнить.
Типичные ошибки
- ✗Верить, что оптимизатор инлайнит сквозь виртуальный вызов по умолчанию — девиртуализация требует известного конкретного типа
- ✗Игнорировать раздувание от инстанцирования шаблонов — каждый тип стратегии умножает использующий его код
- ✗Выбирать Strategy на этапе компиляции через границу ABI, где выбор стратегии должен меняться без перекомпиляции вызывающих
Уточняющие вопросы
- →Когда диспетчеризация через
std::variantвыигрывает и у CRTP, и у виртуального интерфейса? - →Как раздувание от инстанцирования шаблонов связано с давлением на кеш инструкций на горячем пути?
SeniorПроизводительностьИногдаКогда глубокий стек обёрток Decorator или Adapter вредит производительности и отладке?
Когда глубокий стек обёрток Decorator или Adapter вредит производительности и отладке?
Каждая рантайм-обёртка добавляет виртуальный вызов, недружелюбный к кешу переход по указателю и неинлайнируемую границу, поэтому стек глубиной 6 превращает один логический вызов в шесть косвенных на горячем пути. Отладка страдает: стек вызовов раздувается почти одинаковыми кадрами, владение неясно. Сворачивайте стек через CRTP/статическую композицию или уплощайте слои при фиксированном на компиляции поведении.
Типичные ошибки
- ✗Считать, что оптимизатор девиртуализирует рантайм-цепочку декораторов — через указатель на базовый класс обычно не может
- ✗Игнорировать эффекты кеша — каждая обёртка отдельно выделена в куче, поэтому цепочка разбросана по памяти
- ✗Добавлять обёртки для поведения, которое никогда не меняется в рантайме, оплачивая косвенность за решение, фиксированное на этапе компиляции
Уточняющие вопросы
- →Как статический декоратор на CRTP устраняет виртуальный вызов на каждый слой?
- →Как профилировать, чтобы подтвердить, что узкое место — стек обёрток, а не полезная нагрузка?