Зачем порождающие паттерны
Порождающие паттерны отвечают на один вопрос: как создать объект, не привязывая клиента к конкретному классу и не превращая создание в кашу из new. Оператор new — это жёсткая связь с реализацией, и там, где вариантов больше одного, он мешает и расширению (OCP), и подмене в тестах.
На собеседовании эту группу спрашивают чаще остальных, и почти всегда просят пример из JDK или Spring. Заготовьте по одному примеру на паттерн — это отличает выученное определение от рабочего знания.
Важная рамка, которую стоит проговорить: паттерн — не цель. Это имя для решения, которое и так возникает из требований. Вводить Abstract Factory там, где хватает конструктора, — тот же случай спекулятивной общности, что и интерфейс с единственной реализацией.
Factory Method и статическая фабрика — это разные вещи
Это самая частая путаница в этой теме, и на ней хорошо отличают уровень кандидата.
Factory Method по GoF — паттерн с наследованием: базовый класс объявляет метод создания продукта, а решение о конкретном классе принимает подкласс. Клиент работает с базовым типом и не знает, что именно ему создали. Ключевой признак — полиморфизм: выбор происходит в рантайме через подставленную реализацию.
Статическая фабрика (static factory method) — идиома из Effective Java, а не паттерн GoF. Это просто статический метод вместо публичного конструктора: Integer.valueOf, List.of, Optional.of, LocalDate.of, EnumSet.noneOf. Её плюсы: у метода есть имя (два конструктора с одинаковой сигнатурой невозможны, а of и copyOf — пожалуйста), не обязана каждый раз создавать новый объект (Integer.valueOf берёт из кэша, Boolean.valueOf возвращает константу), может вернуть подтип (EnumSet отдаёт RegularEnumSet или JumboEnumSet в зависимости от размера).
Ответ уровня senior на вопрос «что такое фабрика» звучит так: уточнить, о чём речь — о паттерне Factory Method с полиморфной подстановкой или о статической фабрике Блоха, — и объяснить разницу. Примеры в JDK: Calendar.getInstance() и NumberFormat.getInstance() — фабрики, отдающие подходящую реализацию; Collection.iterator() — классический Factory Method, где каждая коллекция создаёт свой итератор.
// Factory Method (GoF): решение принимает подкласс.
abstract class Dialog {
abstract Button createButton(); // factory method
void render() { createButton().draw(); } // клиент не знает конкретный класс
}
class WebDialog extends Dialog { Button createButton() { return new HtmlButton(); } }
// Статическая фабрика (Effective Java, item 1) — не паттерн GoF.
Integer i = Integer.valueOf(127); // может вернуть объект из кэша
List<String> l = List.of("a", "b"); // может вернуть специализированный подтип
Abstract Factory
Abstract Factory создаёт семейства связанных объектов так, чтобы клиент не знал их конкретных классов и не мог случайно смешать два семейства. Ключевое слово — семейство: если объект один, это не Abstract Factory, а обычная фабрика.
Пример в платформе — javax.xml.parsers.DocumentBuilderFactory и TransformerFactory: newInstance() отдаёт реализацию, выбранную по конфигурации, а та уже создаёт согласованный набор объектов парсера. Похожая идея у java.sql.Connection: одно соединение порождает Statement, PreparedStatement и Blob одной и той же СУБД — смешать объекты от разных драйверов невозможно.
В Spring роль контейнера фабрик играет BeanFactory. Отдельно стоит знать интерфейс FactoryBean: это бин, задача которого — создавать другой бин. Классический вопрос на собеседовании — как получить сам FactoryBean, а не его продукт: через префикс амперсанда в имени бина.
Цена паттерна — жёсткость набора. Добавить новый продукт в семейство означает править интерфейс фабрики и все её реализации: Abstract Factory хорош, когда меняются семейства, и плох, когда часто меняется состав продуктов.
Builder: две разные версии одного имени
Под словом Builder на интервью тоже скрываются две разные вещи, и различить их — сильный ход.
Builder по GoF разделяет процесс конструирования и представление: есть Director, который знает последовательность шагов, и несколько билдеров, дающих разные результаты из одного и того же сценария сборки. Акцент — на пошаговом процессе.
Builder Блоха (Effective Java) решает более приземлённую задачу: телескопические конструкторы и длинные списки одинаковых по типу параметров. Вместо конструктора с восемью аргументами — цепочка именованных методов и build() в конце. Именно эту версию порождает Lombok через @Builder, и именно она в 95% случаев имеется в виду в проде.
Почему билдер особенно ценен для неизменяемых объектов: он позволяет собирать состояние по частям, а на выходе отдать полностью проинициализированный immutable-объект и проверить все инварианты разом в build(). Без билдера пришлось бы либо делать сеттеры (и потерять неизменяемость), либо тащить громоздкий конструктор.
Примеры в JDK: HttpRequest.newBuilder() из Java 11, Stream.builder(), Locale.Builder, Calendar.Builder. Внимание к частой ошибке: StringBuilder — не паттерн Builder. Это изменяемый буфер, у него нет продукта с инвариантами и нет пошагового конструирования разных представлений; совпало только название.
// Builder Блоха: именованные шаги + проверка инвариантов в build().
Order order = Order.builder()
.customer(id)
.item(sku, 2)
.deliveryAddress(address)
.build(); // здесь валидируем: адрес обязателен, позиций > 0
// JDK, Java 11+
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.org"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.build();
Prototype и почему clone() не любят
Prototype создаёт новый объект копированием существующего — это полезно, когда конструирование дорого (тяжёлая инициализация, поход в сеть) или когда нужен объект «как этот, но с одним отличием».
В Java этот паттерн исторически представлен Cloneable и Object.clone(), и это признанно неудачный дизайн. Cloneable — маркерный интерфейс без единого метода: он не объявляет clone(), а лишь меняет поведение защищённого метода Object. Сам clone() объявлен protected, поэтому его нужно переопределять и повышать видимость; он не вызывает конструктор, из-за чего final-поля и инварианты остаются в стороне; и по умолчанию копия поверхностная — вложенные изменяемые объекты остаются общими с оригиналом.
Поверхностное против глубокого копирования — отдельный частый вопрос. Shallow copy копирует значения полей, то есть ссылки; изменяемый список внутри копии окажется тем же самым объектом, что и в оригинале. Deep copy рекурсивно копирует и вложенные объекты. Глубокое копирование делают вручную, через копирующие конструкторы или через сериализацию (последнее медленно и ломается на несериализуемых полях).
Рекомендация Блоха, которую стоит озвучить: вместо Cloneable использовать копирующий конструктор или копирующую статическую фабрику — new ArrayList<>(other), List.copyOf(other), EnumSet.copyOf(other). Они работают с интерфейсом, не требуют приведения типа, не бросают CloneNotSupportedException и позволяют менять представление при копировании. Отдельно: для по-настоящему неизменяемых объектов копирование не нужно вовсе — их можно разделять.
Singleton и object pool
Singleton подробно разобран в уроке про тестирование и базовые паттерны — здесь только место в классификации и главное. Гарантирует единственный экземпляр; потокобезопасные варианты в Java: enum (защищён и от рефлексии, и от сериализации), static holder и double-checked locking с обязательным volatile. Главный минус — глобальное изменяемое состояние, из-за которого страдают тестируемость и изоляция.
Отдельно проговорите отличие от singleton scope в Spring: контейнерный синглтон — это один экземпляр на ApplicationContext, а не на JVM, и он не создаёт глобальной точки доступа, потому что зависимость приходит через внедрение. Поэтому «в Spring везде синглтоны» и «паттерн Singleton — антипаттерн» уживаются без противоречия.
Object pool переиспользует дорогие в создании объекты вместо повторного конструирования. Живые примеры — пулы соединений (HikariCP) и пулы потоков (ThreadPoolExecutor). Именно поэтому пул соединений настраивают, а не создают Connection на каждый запрос: рукопожатие с БД дороже самой выборки.
Родственная идея — Flyweight: разделять неизменяемые экземпляры вместо создания одинаковых. В JDK это кэш Integer.valueOf на диапазоне -128..127, пул строковых литералов и константы Boolean.TRUE/FALSE. Отсюда же знаменитая ловушка с == на Integer.
Как отвечать на собеседовании
На вопрос «какие знаете порождающие паттерны» не перечисляйте список — назовите и сразу привяжите к примеру из JDK. Это занимает те же тридцать секунд, но звучит принципиально иначе.
Держите наготове два разграничения, которые дают быстрый плюс: Factory Method (полиморфизм, решает подкласс) против статической фабрики Блоха (именованный статический метод, может кэшировать и возвращать подтип), и Builder GoF (Director, разные представления) против Builder Блоха (борьба с телескопическими конструкторами, то, что делает Lombok).
Про Prototype отвечайте честно: паттерн полезен, но реализация через Cloneable в Java неудачна — маркерный интерфейс, protected clone(), обход конструктора, поверхностная копия по умолчанию. Правильная замена — копирующий конструктор или копирующая фабрика.
Если спросят про паттерны в Spring, соберите их в одну карту: BeanFactory и FactoryBean — фабрики, singleton scope — управляемый контейнером экземпляр, прототипный scope — новый объект на каждый запрос зависимости, пул соединений — object pool.