Паттерны проектирования
Порождающие, структурные и поведенческие паттерны на идиомах C# — делегаты вместо Strategy, события вместо Observer, DI-контейнер вместо ручного Singleton.
13 вопросов
JuniorТеорияОчень частоЧто такое паттерн проектирования и каковы три категории GoF?
Что такое паттерн проектирования и каковы три категории GoF?
Паттерн проектирования — именованный переиспользуемый шаблон решения повторяющейся задачи: его адаптируют, а не импортируют. Банда четырёх делит их на порождающие (создание объектов, Factory), структурные (композиция, Adapter) и поведенческие (взаимодействие, Strategy).
Типичные ошибки
- ✗Считать паттерн готовым кодом для импорта, а не шаблоном, который адаптируют под контекст
- ✗Путать категории — относить
Factoryк структурным вместо порождающих - ✗Тянуться за паттерном там, где проще обычный код, добавляя косвенность без выгоды
Уточняющие вопросы
- →Назовите один поведенческий паттерн и скажите, какую ответственность он уносит из вызывающего кода.
- →Почему паттерн — это шаблон, а не переиспользуемый библиотечный код?
MiddleТеорияОчень частоЧем различаются порождающие паттерны Factory Method и Abstract Factory?
Чем различаются порождающие паттерны Factory Method и Abstract Factory?
Factory Method — один переопределяемый метод, создающий один продукт: конкретный тип выбирает производный класс. Abstract Factory — интерфейс с несколькими методами создания, порождающий семейство связанных продуктов и держащий его согласованным.
Типичные ошибки
- ✗Называть любой
staticхелперCreateабстрактной фабрикой - ✗Упускать, что
Abstract Factoryнужен ради согласованности семейства продуктов, а не просто чтобы спрятатьnew - ✗Считать, что выбор между ними зависит от того, абстрактный класс продукт или интерфейс
Уточняющие вопросы
- →Чего стоит порождающий паттерн
Abstract Factory, когда в семейство добавляют новый продукт? - →Как контейнер внедрения зависимостей заменяет большинство рукописных фабрик?
JuniorТеорияЧастоЧто делает паттерн Decorator и чем он отличается от наследования?
Что делает паттерн Decorator и чем он отличается от наследования?
Decorator оборачивает объект с тем же интерфейсом, делегирует ему вызовы и добавляет поведение вокруг них. В отличие от наследования композиция идёт во время выполнения: декораторы складываются в любом порядке, а обёрнутый объект об этом не знает.
Типичные ошибки
- ✗Путать декоратор с адаптером — декоратор сохраняет интерфейс, адаптер его меняет
- ✗Реализовывать дополнительное поведение наследованием, зашивая его на этапе компиляции
- ✗Забыть делегировать вызов обёрнутому экземпляру, из-за чего исходное поведение теряется
Уточняющие вопросы
- →Почему декоратор обязан реализовать тот же интерфейс, что и обёрнутый объект?
- →Что меняется, если поменять местами два декоратора в стеке?
MiddleТеорияЧастоЧем структурные паттерны Adapter и Facade различаются по назначению?
Чем структурные паттерны Adapter и Facade различаются по назначению?
Adapter преобразует существующий интерфейс в тот, которого клиент уже ожидает, обычно оборачивая один несовместимый тип. Facade придумывает новый упрощённый интерфейс поверх подсистемы. Adapter — про совместимость, facade — про упрощение.
Типичные ошибки
- ✗Называть адаптером любой класс-обёртку, независимо от того, меняется ли интерфейс
- ✗Строить facade, который просто пересылает вызовы одному типу, — это адаптер
- ✗Путать оба с декоратором, который сохраняет интерфейс и добавляет поведение
Уточняющие вопросы
- →Где в многослойном решении обычно живёт структурный паттерн
Adapter? - →Когда facade превращается в god object и как его разделить?
MiddleТеорияЧастоКак event в C# нативно реализует поведенческий паттерн Observer?
Как event в C# нативно реализует поведенческий паттерн Observer?
event — это multicast-делегат, у которого += и -= и есть подписка и отписка: издатель владеет списком вызова и поднимает его, а подписчики — обычные обработчики. Язык даёт обе роли сам, без IObserver и списка подписчиков.
Типичные ошибки
- ✗Забыть
-=, из-за чего издатель держит подписчиков живыми и течёт памятью - ✗Считать, что
eventподдерживает лишь один обработчик за раз - ✗Полагать, что обработчики выполняются асинхронно, а не в потоке, который поднял событие
Уточняющие вопросы
- →Почему неудалённый обработчик держит объект-подписчик живым?
- →Когда стоит взять
IObservable<T>вместо обычногоevent?
MiddleТеорияЧастоЧем конвейер middleware в ASP.NET Core похож на структурный паттерн Decorator?
Чем конвейер middleware в ASP.NET Core похож на структурный паттерн Decorator?
Каждый middleware оборачивает остаток конвейера: он получает next, может выполнить код до вызова, оборвать цепочку или выполнить код после. Это цепочка декораторов над одним RequestDelegate: порядок регистрации — порядок обёртывания.
Типичные ошибки
- ✗Регистрировать middleware в неверном порядке, ожидая, что фреймворк сам разберётся
- ✗Забыть
await next(context), молча обрывая остаток конвейера - ✗Путать обёртывание с адаптером — middleware сохраняет сигнатуру
RequestDelegate
Уточняющие вопросы
- →Что происходит с ответом, если middleware пишет в него после
await next? - →Почему завершающий middleware конвейера никогда не вызывает
next?
MiddleТеорияЧастоЧем рискован рукописный порождающий паттерн Singleton против singleton в DI-контейнере?
Чем рискован рукописный порождающий паттерн Singleton против singleton в DI-контейнере?
Рукописный Singleton — глобальное изменяемое состояние: его трудно подменить в тестах, время жизни привязано к static полю, а ленивая инициализация требует Lazy<T> или static конструктора. Singleton в контейнере подменяется и освобождается.
Типичные ошибки
- ✗Считать, что
staticсвойствоInstanceлегко подменить или сбросить между тестами - ✗Писать double-checked locking вручную, забывая
volatileилиLazy<T> - ✗Прятать зависимость в
staticвызове вместо объявления её в конструкторе
Уточняющие вопросы
- →Чем опасна захваченная зависимость, когда singleton принимает scoped-сервис?
- →Когда
staticLazy<T>всё ещё лучше регистрации в контейнере?
JuniorТеорияИногдаКак синтаксис object initializer в C# соотносится с порождающим паттерном Builder?
Как синтаксис object initializer в C# соотносится с порождающим паттерном Builder?
Оба задают состояние после конструирования, но object initializer — лишь сахар над new без аргументов и присваиваниями свойств: он ничего не проверяет. Builder — отдельный объект, который валидирует и возвращает готовый экземпляр из Build().
Типичные ошибки
- ✗Считать, что object initializer проверяет обязательные свойства или обеспечивает инварианты
- ✗Думать, что присваивания инициализатора выполняются до конструктора, а не после него
- ✗Считать, что
init-only сеттеры снимают нужду в строителе, проверяющем сочетания значений
Уточняющие вопросы
- →Как модификатор
requiredменяет то, что может гарантировать object initializer? - →Когда порождающий паттерн
Builderлучше конструктора со многими необязательными параметрами?
MiddleТеорияИногдаКак библиотека внутрипроцессных сообщений MediatR реализует поведенческий паттерн Mediator?
Как библиотека внутрипроцессных сообщений MediatR реализует поведенческий паттерн Mediator?
Отправитель посылает объект-запрос и не называет обработчика: IMediator находит IRequestHandler<TRequest, TResponse> в контейнере и вызывает его. Отправитель и обработчик связаны лишь типом запроса, а IPipelineBehavior оборачивает вызов.
Типичные ошибки
- ✗Считать
MediatRочередью или шиной сообщений, а не внутрипроцессным диспетчером - ✗Путать request, у которого ровно один обработчик, с notification, у которого их много
- ✗Добавлять
MediatRради одной лишь косвенности, пряча за ним обычные вызовы сервисов
Уточняющие вопросы
- →Чем
IPipelineBehaviorотличается от компонента middleware в ASP.NET Core? - →Когда пропускание всего через mediator делает граф вызовов труднее для чтения?
MiddleТеорияИногдаКак реализовать поведенческий паттерн Strategy через Func<T> вместо иерархии классов?
Как реализовать поведенческий паттерн Strategy через Func<T> вместо иерархии классов?
Интерфейс стратегии заменяется делегатом: класс принимает Func<Order, decimal> и вызывает его, а каждая стратегия — лямбда или группа методов. Теряются именованный тип и состояние на стратегию, зато исчезают интерфейс и класс на каждый алгоритм.
Типичные ошибки
- ✗Считать, что делегат не заменит интерфейс стратегии, раз у него нет именованного типа
- ✗Держать интерфейс с одним методом и по классу на алгоритм там, где хватило бы
Func<T> - ✗Упускать, что форма с делегатом отдаёт состояние на стратегию и обнаружимый именованный тип
Уточняющие вопросы
- →Когда стратегия, которой нужно собственное состояние, заставляет вернуться к интерфейсу?
- →Как выбрать нужный делегат на старте приложения из конфигурации?
SeniorТеорияИногдаКакую задачу решает разделение ответственности команд и запросов (CQRS) и чего оно стоит?
Какую задачу решает разделение ответственности команд и запросов (CQRS) и чего оно стоит?
CQRS разделяет модель записи и модель чтения: сущности с инвариантами для команд, денормализованные проекции для запросов. Цена — две модели и, при отдельном хранилище чтения, конечная согласованность: клиент может не увидеть собственную запись.
Типичные ошибки
- ✗Путать
CQRSс разделением команд и запросов по методам внутри одного класса-сервиса - ✗Считать, что
CQRSобязательно требует event sourcing и без него неприменим - ✗Игнорировать конечную согласованность, а потом удивляться, что клиент не видит свою запись
Уточняющие вопросы
- →Как скрыть от интерфейса задержку, из-за которой клиент не видит собственную запись?
- →Когда
CQRSизбыточен для обычного сервиса с созданием, чтением, обновлением и удалением?
SeniorДизайнИногдаПлатёжный сервис публикует POST /payments поверх канала с доставкой «хотя бы один раз»: клиент повторяет запрос при таймаутах и сетевых сбоях, поэтому один и тот же логический запрос может прийти два-три раза, иногда одновременно на разные инстансы за балансировщиком. Списать деньги дважды недопустимо. Спроектируйте endpoint так, чтобы повтор запроса давал тот же эффект, что и однократная отправка. Раскройте: как клиент помечает повтор как тот же самый запрос; что и где вы храните, чтобы распознать дубликат; как не дать проверке дубликата и списанию гоняться между инстансами; что вы возвращаете на дубликат, который ещё выполняется, и на уже завершённый; сколько должна жить запись дедупликации. Назовите отказы, которые ваш дизайн всё ещё допускает.
Платёжный сервис публикует POST /payments поверх канала с доставкой «хотя бы один раз»: клиент повторяет запрос при таймаутах и сетевых сбоях, поэтому один и тот же логический запрос может прийти два-три раза, иногда одновременно на разные инстансы за балансировщиком. Списать деньги дважды недопустимо. Спроектируйте endpoint так, чтобы повтор запроса давал тот же эффект, что и однократная отправка. Раскройте: как клиент помечает повтор как тот же самый запрос; что и где вы храните, чтобы распознать дубликат; как не дать проверке дубликата и списанию гоняться между инстансами; что вы возвращаете на дубликат, который ещё выполняется, и на уже завершённый; сколько должна жить запись дедупликации. Назовите отказы, которые ваш дизайн всё ещё допускает.
Клиент присылает ключ идемпотентности. Сервер вставляет его в хранилище дедупликации под уникальным ограничением до списания, поэтому конкурентный дубликат проигрывает вставку. Завершённый ответ переигрывается при повторе, дубликат в процессе получает конфликт, а записи переживают повторы.
Типичные ошибки
- ✗Дедуплицировать по хешу тела запроса вместо ключа идемпотентности от клиента
- ✗Разносить проверку и запись ключа на два шага, оставляя окно гонки между инстансами
- ✗Удалять запись дедупликации сразу после ответа, из-за чего поздний повтор списывает деньги ещё раз
Уточняющие вопросы
- →Что должен вернуть endpoint, когда тот же ключ приходит с другим телом запроса?
- →Как сделать запись дедупликации и списание атомарными, если они лежат в разных хранилищах?
SeniorТеорияРедкоКогда паттерн критериев запроса Specification оправдывает себя с Entity Framework Core?
Когда паттерн критериев запроса Specification оправдывает себя с Entity Framework Core?
Когда один и тот же бизнес-фильтр нужен в нескольких местах. Specification — именованное Expression<Func<T, bool>>, которое можно юнит-тестировать и комбинировать через And/Or, а Entity Framework Core переводит его в SQL. Он перестаёт окупаться, пряча SQL.
Типичные ошибки
- ✗Объявлять specification как
Func<T, bool>, из-за чего вся таблица фильтруется на стороне клиента - ✗Строить слой спецификаций так глубоко, что итоговый SQL становится не виден
- ✗Считать, что деревья выражений нельзя комбинировать, и дублировать фильтр вместо этого
Уточняющие вопросы
- →Почему
Expression<Func<T, bool>>переводится в SQL, аFunc<T, bool>— нет? - →Как объединить две спецификации, не потеряв дерево выражений?