Паттерны проектирования
Распространённые паттерны проектирования в Java — singleton, factory, builder и внедрение зависимостей.
11 вопросов
MiddleТеорияОчень частоПочему в Java стоит предпочитать композицию наследованию?
Почему в Java стоит предпочитать композицию наследованию?
Наследование — «белый ящик»: подкласс опирается на внутреннее устройство суперкласса, поэтому изменение сверху может незаметно его сломать (проблема хрупкого базового класса), а связь фиксируется на компиляции. Композиция — «чёрный ящик»: объект держит сотрудника и делегирует через его интерфейс, поэтому поведение подменяется в рантайме, заглушается в тесте и свободно сочетается. Наследование оставьте для настоящего «является» над стабильным базовым классом.
Типичные ошибки
- ✗Наследоваться от класса только ради пары его методов, когда хватило бы делегирования
- ✗Считать подкласс защищённым от изменений суперкласса, раз он компилируется
- ✗Моделировать любую связь как «является» и получать по подклассу на каждую комбинацию возможностей
Уточняющие вопросы
- →В чём именно состоит проблема хрупкого базового класса и как делегирование её избегает?
- →Когда наследование всё же уместнее, чем хранение сотрудника внутри объекта?
MiddleТеорияОчень частоЧто гарантирует создающий паттерн singleton, и как сделать его потокобезопасным?
Что гарантирует создающий паттерн singleton, и как сделать его потокобезопасным?
Создающий паттерн singleton гарантирует, что у класса ровно один экземпляр, и даёт глобальную точку доступа к нему: private конструктор блокирует внешний new, а static поле с аксессором отдают этот единственный экземпляр. Сделать его потокобезопасным без гонок можно через enum (JVM сериализует его инициализацию), вложенный static-holder класс (ленивый, загружается при первом доступе) или double-checked locking на volatile поле.
Типичные ошибки
- ✗Ленивая инициализация без синхронизации — два потока создают по экземпляру в гонке
- ✗Забыть
volatileв double-checked locking, открывая частично сконструированный объект - ✗Игнорировать, что сериализация или рефлексия могут нарушить гарантию единственного экземпляра
Уточняющие вопросы
- →Почему
enum-singleton защищён ещё и от сериализации и атак через рефлексию? - →Что именно предотвращает
volatileв double-checked locking?
MiddleТеорияЧастоЧто решает создающий паттерн builder, и когда к нему обращаются?
Что решает создающий паттерн builder, и когда к нему обращаются?
Создающий паттерн builder конструирует сложный объект шаг за шагом через текучий API — цепочку setter-подобных вызовов, завершаемую build(), — вместо одного огромного конструктора. Он устраняет телескопические конструкторы, где перегрузки с длинными списками одинаковых по типу параметров нечитаемы. К нему обращаются, когда у объекта много необязательных параметров или для неизменяемого объекта: builder собирает поля, затем build() отдаёт результат.
Типичные ошибки
- ✗Оставлять построенный объект изменяемым, теряя выгоду неизменяемости паттерна
- ✗Применять builder для тривиального объекта из двух полей, где конструктор яснее
- ✗Путать builder (создающий) с decorator, который оборачивает для добавления поведения
Уточняющие вопросы
- →Как builder помогает разграничить обязательные и необязательные поля?
- →Почему паттерн builder естественно сочетается с неизменяемыми объектами?
MiddleТеорияЧастоКакую проблему решает создающий паттерн factory, и как он развязывает клиент?
Какую проблему решает создающий паттерн factory, и как он развязывает клиент?
Создающий паттерн factory делегирует создание объекта выделенному методу или подклассу, который возвращает интерфейс или абстрактный тип, вместо вызова клиентом new на конкретном классе. Клиент просит у фабрики абстрактный тип и не знает, какую реализацию получит, поэтому замена или добавление реализаций затрагивает одно место. Он также централизует логику конструирования в этом одном методе.
Типичные ошибки
- ✗Возвращать из фабрики конкретный тип, из-за чего клиент по-прежнему зависит от реализаций
- ✗Путать factory (создающий) с поведенческим паттерном о взаимодействии объектов
- ✗Рассеивать вызовы
newпо коду вместо централизации их в фабрике
Уточняющие вопросы
- →Чем factory method отличается от abstract factory?
- →Как возврат интерфейса поддерживает принцип инверсии зависимостей?
MiddleКодЧастоРеализуйте поведенческий паттерн Strategy через функциональный интерфейс
Реализуйте поведенческий паттерн Strategy через функциональный интерфейс
Дайте DiscountRule ровно один абстрактный метод — long apply(long amountCents), — тогда стратегией становится любая лямбда или ссылка на метод. Checkout принимает правило в конструкторе, держит его в final поле и делегирует: total() возвращает Math.max(0, rule.apply(amountCents)). Новое правило — просто новая лямбда в месте вызова, и Checkout не меняется.
Типичные ошибки
- ✗Добавлять в интерфейс второй абстрактный метод, из-за чего он перестаёт годиться для лямбды
- ✗Заводить по подклассу
Checkoutна каждое правило вместо хранения правила полем - ✗Делать
switchпоenumвнутриtotal(), из-за чего каждое новое правило правитCheckout
Уточняющие вопросы
- →Почему второй абстрактный метод не даёт использовать
DiscountRuleкак лямбду? - →Чем паттерн
Strategyотличается здесь от паттернаTemplate Method?
SeniorТеорияЧастоЧто такое внедрение зависимостей, и как оно связано с инверсией управления?
Что такое внедрение зависимостей, и как оно связано с инверсией управления?
Внедрение зависимостей поставляет коллабораторов объекта снаружи — через его конструктор или setter — вместо того чтобы объект сам создавал их через new. Это инвертирует управление: объект больше не решает, какую конкретную зависимость создать, поэтому зависит от абстракций, а внешний сборщик (часто Spring) связывает граф. Выигрыш — более слабая связанность и лёгкое тестирование: вы внедряете mock и меняете реализации без правки потребителя.
Типичные ошибки
- ✗Отождествлять DI с фреймворком, тогда как это просто передача зависимостей снаружи
- ✗Вызывать
newдля зависимостей внутри класса, что сохраняет управление и сводит идею на нет - ✗Зависеть от конкретного типа вместо абстракции, блокируя подмену в тестах
Уточняющие вопросы
- →Каковы компромиссы между constructor injection и setter injection?
- →Как IoC-контейнер вроде Spring разрешает и связывает граф зависимостей?
JuniorТеорияИногдаЧто такое паттерн проектирования, и каковы три категории GoF?
Что такое паттерн проектирования, и каковы три категории GoF?
Паттерн проектирования — это переиспользуемое именованное решение повторяющейся проблемы проектирования: проверенный шаблон, который адаптируют под свой код, а не готовая библиотека. Банда четырёх делит их на создающие паттерны (создание объектов, напр. singleton, factory), структурные паттерны (как объекты компонуются, напр. adapter, decorator) и поведенческие паттерны (как объекты взаимодействуют и делят ответственность, напр. observer, strategy).
Типичные ошибки
- ✗Считать паттерн готовым кодом для импорта, а не шаблоном, адаптируемым под контекст
- ✗Путать три категории — относить factory к структурным вместо создающих
- ✗Применять паттерны там, где проще обычный код, добавляя лишнюю косвенность
Уточняющие вопросы
- →Приведите один поведенческий паттерн и объясните, какую ответственность он распределяет.
- →Почему паттерн — это шаблон, а не переиспользуемый библиотечный код?
MiddleТеорияИногдаКак структурный паттерн Decorator формирует классы java.io?
Как структурный паттерн Decorator формирует классы java.io?
Декоратор оборачивает объект в другой объект того же интерфейса, добавляет поведение и делегирует остальное тому, кого держит, — поэтому возможности накапливаются в рантайме, а не плодят подкласс на каждую комбинацию. java.io построен так: в new BufferedReader(new InputStreamReader(System.in)) каждая обёртка хранит обёрнутый поток полем и переадресует ему read(), и ступени сочетаются в любом порядке.
Типичные ошибки
- ✗Думать, что
BufferedReaderнаследуется от обёрнутого reader, а не хранит его полем - ✗Путать декоратор с адаптером — декоратор сохраняет интерфейс, адаптер его меняет
- ✗Считать порядок вложенности неважным, хотя именно он задаёт порядок работы ступеней
Уточняющие вопросы
- →Почему закрытие самой внешней обёртки
java.ioзакрывает и все потоки под ней? - →Чем декоратор отличается от структурного паттерна
Adapter?
MiddleТеорияИногдаКак поведенческий паттерн Observer проявляется в слушателях Java?
Как поведенческий паттерн Observer проявляется в слушателях Java?
Субъект держит список зарегистрированных слушателей и проталкивает событие каждому при изменении своего состояния, поэтому знает только интерфейс слушателя, но не конкретные классы, а наблюдателей можно добавлять и убирать в рантайме. addActionListener из Swing, PropertyChangeSupport и ApplicationListener из Spring устроены так же; субъект держит наблюдателей композицией, а старый класс java.util.Observable объявлен устаревшим в Java 9 за навязанное наследование.
Типичные ошибки
- ✗Заставлять субъект зависеть от конкретных классов слушателей, а не от интерфейса слушателя
- ✗Забывать отписать слушателя, из-за чего список субъекта держит наблюдателя живым и утекает
- ✗Хвататься за устаревший
java.util.Observableвместо интерфейса слушателя
Уточняющие вопросы
- →Как безопасно уведомить слушателей, если один из них отписывается прямо во время обхода списка?
- →Почему класс
java.util.Observableобъявлен устаревшим в пользу интерфейсов-слушателей?
MiddleТеорияИногдаЧто закрепляет поведенческий паттерн Template Method в иерархии?
Что закрепляет поведенческий паттерн Template Method в иерархии?
Метод базового класса закрепляет скелет алгоритма — порядок шагов — и делегирует изменяемые шаги abstract или переопределяемым хукам, которые заполняет подкласс; скелет обычно final, чтобы никто не переставил шаги. Классический пример — HttpServlet.service(), раздающий вызовы в doGet: базовый класс владеет неизменным ходом и вызывает вниз, а подкласс поставляет детали.
Типичные ошибки
- ✗Оставлять метод-скелет переопределяемым, из-за чего подкласс переставляет или пропускает шаги
- ✗Делать каждый хук
abstract, хотя у необязательного шага должно быть пустое тело по умолчанию - ✗Путать его со
Strategy— этот меняет поведение наследованием, аStrategy— композицией
Уточняющие вопросы
- →Почему метод-скелет в базовом классе обычно объявляют
final? - →Чем паттерн
Template Methodотличается от паттернаStrategy?
SeniorТеорияИногдаЧем различаются создающие паттерны Factory Method и Abstract Factory?
Чем различаются создающие паттерны Factory Method и Abstract Factory?
Factory Method — это один переопределяемый метод, создающий один продукт: конкретный класс выбирает подкласс создателя, поэтому вариация идёт через наследование и фиксируется при выборе подкласса. Abstract Factory — объект с несколькими методами создания, выпускающий целое семейство взаимно согласованных продуктов; клиент держит его композицией, поэтому всё семейство подменяется в рантайме.
Типичные ошибки
- ✗Называть
Abstract Factoryлюбойstaticпомощник, прячущийnew, — это статический factory-метод - ✗Упускать, что
Abstract Factoryсуществует ради взаимной согласованности семейства продуктов - ✗Считать, что оба меняются наследованием, хотя клиент держит
Abstract Factoryкомпозицией и подменяет его в рантайме
Уточняющие вопросы
- →Что ломается в
Abstract Factory, когда в семейство нужно добавить новый продукт? - →Чем статический factory-метод отличается от паттерна
Factory Method?