Привет! Меня зовут Бромбин Андрей. В этой статье мы рассмотрим порождающие паттерны ООП. Обсудим, что такое хороший дизайн и почему не стоит начинать всё с нуля каждый раз, когда перед нами новая задача. Также разберёмся, где эти паттерны действительно помогают и какую пользу несут — всё это с наглядными примерами на Java, приближёнными к реальным.
Всем нам хочется делать больше и тратить на это меньше времени. Браться за новые задачи смелее и выполнять их эффективнее. В этом нам и помогают паттерны: они дают рабочую схему для типовых кейсов, чтобы не выдумывать решение каждый раз с чистого листа.

О шаблонах проектирования книг и статей, как песен о любви, потому что в разработке — эта тема волнует каждого начинающего и не очень специалиста. Помните, как пели Чиж и Ко: «а не спеть ли мне песню о любви?» — давайте и я попробую.
Краткий экскурс
Как и в любой хорошей статье, дадим определение тому, с чем имеем дело. В книге «Банды четырёх» есть исчерпывающее описание:
Любой паттерн описывает задачу, которая снова и снова возникает в нашей работе, а также её решение, причём таким образом, что это решение можно потом использовать миллион раз, и при этом никакие две реализации не будут полностью одинаковыми.

Проще говоря, паттерн проектирования — это проверенный способ организовать код, когда проблема — типовая. Это не рецепт решения задачи, а шаблон, который задаёт структуру и роли, улучшает понимание и ускоряет разработку.
Три семейства
Порождающие — отвечают за гибкое создание объектов. Управляют моментом и способом появления экземпляров, скрывают детали, подготавливают корректное состояние, уменьшают связанность.
Структурные — организуют классы и объекты в удобные композиции. Позволяют прятать сложность за простым фасадом, наращивать поведение обёртками и объединять элементы в структуры.
Поведенческие — описывают эффективное взаимодействие и распределение обязанностей. Регулируют, кто инициирует действие и кто на него реагирует, а также где хранится состояние и как эволюционируют сценарии.
По уровню связей
Мы часто говорим: «объект — экземпляр класса». Класс описывает форму, объекты — жизнь во времени. Отсюда ещё одно полезное деление:
Уровень классов — статические связи, фиксируемые на этапе компиляции. Здесь работают наследование и переопределение: общий каркас задаётся базовым классом, вариации — в наследниках.
Уровень объектов — динами��еские связи, которые меняются во время выполнения. Поведение строится через композицию и делегирование: компоненты можно подменять, комбинировать и конфигурировать на лету.
Порождающие паттерны
Суть порождающих шаблонов — абстрагировать и сделать гибче «рождение» объектов: скрыть подробности создания и компоновки экземпляров без ограничения кода лишними зависимостями.
FactoryMethod
Порождающий паттерн, который подразумевает объявление фабричного (переопределяемого) метода в базовом «создателе», позволяя наследникам решать, какой конкретный класс будет создан. Клиент работает с абстракциями (создателя и продукта), а выбор конкретной реализации ложится на плечи подклассов, переопределяющих фабричный метод.
Представьте, вы говорите: «Доставьте письмо». Каждая компания сама решает, кого отправить: почта — курьера на самокате, экспресс — легковушку, грузовая — фургон. Клиент не уточняет детали, выбор делает подкласс-создатель.

Рассмотрим на примере сервиса уведомлений:
FactoryMethod
package creational.factorymethod;
// Контракт отправки уведомления
@FunctionalInterface
interface Notifier { void send(String message); }
// Фабрика/Создатель с фабричным (переопределяемым) методом
abstract class AlertService {
// ФАБРИЧНЫЙ МЕТОД: подклассы ниже решают,
// какой Notifier создать для выполнения alert
protected abstract Notifier createNotifier();
// Клиентский сценарий, который должен зависеть
// не от конкретной реализации, а от абстракции
public void alert(String msg) { createNotifier().send(msg); }
}
// Конкретные (не абстрактные) наследники фабрики.
// Переопределяют абстрактный фабричный метод.
// Каждый возвращает свою реализацию.
final class SmsAlerts extends AlertService {
@Override protected Notifier createNotifier() {
return m -> System.out.println("SMS: " + m);
}
}
final class EmailAlerts extends AlertService {
@Override protected Notifier createNotifier() {
return m -> System.out.println("EMAIL: " + m);
}
}
// Демонстрация
public class Main {
public static void main(String[] args) {
// выбор конкретной фабрики в зависимости от условий
AlertService svc = (args.length > 0 && args[0].equalsIgnoreCase("sms"))
? new SmsAlerts()
: new EmailAlerts();
svc.alert("hello from Factory Method");
}
}
Резюмируем. В роли создателя выступает AlertService
с фабричным (абстрактным/переопределяемым) методом createNotifier()
, а конкретные наследники решают, какой Notifier
создать — тем самым инкапсулируя логику выбора реализации. Клиент работает с общим интерфейсом alert()
и зависит только от абстракции. Теперь, чтобы расширить систему и добавить, например, пуш-уведомления, достаточно создать нового наследника-создателя.
Сценарий применимости
Если есть несколько близких типов одного продукта с разными зависимостями и настройками, и выбор конкретного типа привязан к подклассу «создателя» — применяем.
Иначе говоря, мы убираем жёсткие зависимости клиента от конкретных реализаций, опираемся на полиморфизм и соблюдаем принцип открытости/закрытости.
Сценарий неприменимости
Если реализация одна и больше в обозримом будущем не планируется, когда параллельных иерархий классов становится много. Использование шаблона «на всякий случай» — усложнит код без выгоды, поэтому без явной причины, как и любой шаблон, не применяем.
Часто при начальном проектировании используется Фабричный метод и в случае необходимости он эволюционирует в Абстрактную фабрику.
AbstractFactory
Порождающий паттерн, который создаёт семейство согласованных объектов через одну абстракцию. Клиент работает через интерфейс и не зависит от конкретных реализаций.
Представьте, вы обращаетесь в службу доставки на Северном полюсе — она сама выбирает совместимую связку транспорта: «корабль + собачья упряжка» или «вертолёт + парашютный курьер». Один выбор — и вся логистика настроена.

Рассмотрим применение паттерна на примере системы с набором согласованных задач — отправки и получения сообщений из очередей. Думаю, не нужно объяснять, почему писать сообщения в очередь Kafka и читать из RabbitMQ в большинстве случаев странно, особенно если нет веской на то причины. Это именно наш случай — такой выбор для нас не совместим. Отталкиваясь от этого, описываем контракт:
Абстракция семейства контрактов: MessagingFactory
// Абстракция семейства
public interface MessagingFactory {
MessageProducer createProducer(); // создание отправителя
MessageConsumer createConsumer(); // создание получателя
}
// Контракты продуктов
public interface MessageProducer {
void send(String topic, String payload); // отправка сообщения
}
public interface MessageConsumer {
void subscribe(String topic); // подписка для получения сообщений
}
Фабрика семейства Kafka с согласованными друг с другом реализациями:
KafkaFactory реализующая интерфейс MessagingFactory
// Конкретная (не абстрактная) фабрика для семейства "Kafka"
// Возвращает парные реализации продьюсера и консюмера
public final class KafkaFactory implements MessagingFactory {
@Override public MessageProducer createProducer() {
return new KafkaProducer();
}
@Override public MessageConsumer createConsumer() {
return new KafkaConsumer();
}
}
// Реализация протокола отправки сообщения семейства Kafka
final class KafkaProducer implements MessageProducer {
@Override
public void send(String topic, String payload) {
System.out.println("Kafka send -> " + topic + ": " + payload);
}
}
В итоге мы получаем два семейства, проделав те же шаги для RabbitMQ:

Теперь напишем сервис, инкапсулирующий работу с фабрикой:
Клиент NotificationService
// Клиент НЕ знает конкретных реализаций
final class NotificationService {
private final MessageProducer producer;
private final MessageConsumer consumer;
// Внедряем семейство через фабрику:
// один выбор фабрики — согласованный набор реализаций
NotificationService(MessagingFactory f) {
this.producer = f.createProducer();
this.consumer = f.createConsumer();
}
// интерфейсы для клиента
void listenNotify() { consumer.subscribe("alerts"); }
void sendNotifyUser(String userId, String text) {
producer.send("alerts", userId + ": " + text);
}
}
Осталось продемонстрировать принцип работы в классе Main
:
Пример использования на клиенте
// Точка входа: клиент выбирает семейство,
// а не каждую конкретную реализацию по отдельности
public class Main {
public static void main(String[] args) {
// Семейство по аргументу/конфигу/профилю окружения
MessagingFactory factory = (args.length > 0 &&
args[0].equalsIgnoreCase("rabbit"))
? new RabbitFactory() : new KafkaFactory();
// Одна фабрика — согласованный комплект реализаций
// Клиент работает через абстракции, код не зависит от Kafka/Rabbit
NotificationService service = new NotificationService(factory);
service.listenNotify();
service.sendNotifyUser("1234", "Abstract Factory в деле");
/*
Пример вывода:
[Kafka] subscribed to alerts
[Kafka] send -> alerts : 1234: Abstract Factory в деле
*/
}
}
Сценарий применимости
Если есть несколько вариантов не одного, а набора контрактов, которые должны меняться комплектом, так как связаны друг с другом и нужна гарантия совместимости. Такой подход снижает связанность клиента с конкретными реализациями и позволяет расширять систему добавлением новой фабрики, не затрагивая при этом клиентский код.
Чем отличается от Factory Method?
Factory Method: выбирает одну реализацию под контракт, например,
SmsNotifier
.Abstract Factory: возвращает набор совместимых реализаций нескольких контрактов, например,
kafkaProducer()
+kafkaConsumer()
.
Другими словами, если вам нужен только один объект, используйте Factory Method.
Если нужен комплект совместимых объектов, то Abstract Factory.
Сценарий неприменимости
Нет семейства, спроектированного для совместного использования (один протокол или реализации можно свободно смешивать) и не планируется в обозримом будущем, контракты часто меняются, каждому клиенту нужна своя уникальная точка выбора — больше подходит паттерн Strategy, который разберём в дальнейшем.
Ограничения
Каждая конкретная фабрика должна предоставлять полный набор компонентов семейства, так как в противном случае теряется гарантия согласованности. Мы не можем просто отказаться от, например, consumer'a
, переопределяя только producer'a
. Помимо этого, паттерн усложняет код, так как для его применения нужно создать множество дополнительных классов. Это следует учитывать при проектировании системы.
Singleton
Порождающий паттерн, который гарантирует единственный экземпляр типа и единую точку доступа к нему.
Представим себе диспетчерскую башню аэропорта. Одна точка координации, к которой обращаются все самолёты. На каждый самолёт строить по башне дорого и неэффективно.

Рассмотрим два примера, где первый без использования паттерна: загружаем большой конфигурационный файл из удалённого хранилища, то есть операция инициализации объекта является дорогостоящей.
Пример без использования шаблона
package creational.singleton.bad;
// Имитируем "дорогую" загрузку удалённой конфигурации
final class RemoteConfig {
RemoteConfig() {
System.out.println("Грузим документ размером в 100 Гб" +
" из облака со скоростью 10 Мб/сек");
Thread.sleep(3000); // очень долго
}
}
final class MarketService {
private final RemoteConfig cfg = new RemoteConfig();
}
final class BillingService {
private final RemoteConfig cfg = new RemoteConfig();
}
public class MainBad {
public static void main(String[] args) {
new MarketService(); // Loading...
new BillingService(); // Loading... второй раз, а зря
}
}
Теперь рассмотрим ту же ситуацию, но на этот раз воспользуемся паттерном Singleton. Чтобы его применить, нужно скрыть конструктор по умолчанию и предоставить публичный статический метод, как глобальную точку доступа, который и будет контролировать жизненный цикл объекта.
Пример с использованием шаблона
final class RemoteConfig {
// приватный конструктор
private RemoteConfig() {
System.out.println("Грузим документ размером в 100 Гб" +
" из облака со скоростью 10 Мб/сек");
try { Thread.sleep(3000); } catch () {} // очень долго
}
private static class Holder {
static final RemoteConfig I = new RemoteConfig();
}
// глобальная точка доступа
public static RemoteConfig get() { return Holder.I; }
}
final class MarketService {
private final RemoteConfig cfg = RemoteConfig.get();
}
final class BillingService {
private final RemoteConfig cfg = RemoteConfig.get();
}
public class MainGood {
public static void main(String[] args) {
new MarketService(); // Loading...
new BillingService(); // чиллаут.
}
}
Экземпляр создаётся лениво внутри статического вложенного класса Holder
. Все потребители берут один и тот же объект через статическую единую и единственную точку get()
, поэтому «дорогая» загрузка выполняется ровно один раз.
Singleton в Spring Framework
// по умолчанию scope = singleton
@Component // один бин на ApplicationContext
class FeatureFlags {
boolean isEnabled(String key) { return true; }
}
@Service
class BillingService {
private final FeatureFlags flags;
// внедряется один и тот же бин
BillingService(FeatureFlags flags) { this.flags = flags; }
boolean discountsOn() { return flags.isEnabled("discounts"); }
}
В Spring «одиночка» — это scope контейнера по умолчанию: любой @Component
создаётся один раз на весь ApplicationContext
и затем, внедряется куда нужно.
Сценарий применимости
Если нужен один общий экземпляр в рамках процесса, инициализировать повторно дорого, не устраивает податливость глобальной переменной к изменениям, но хочется сохранить простой доступ для всех потребителей — применяем.
Сценарий неприменимости
Если нужны разные экземпляры по контексту, инициализация дешёвая или «на всякий случай» — не применяем. Обязательно обращаем внимание на риски ниже.
Минусов у паттерна много: глобальное состояние ухудшает тестируемость и скрывает зависимости, мутабельность может привести к гонкам и с этим нужно считаться. Кроме того, следить за жизненным циклом, а ещё паттерн нарушает принцип единственной ответственности — управление жизненным циклом + бизнес-логика. В DI-мире singleton-scope
нормален, если иммутабелен/stateless и легко заменяется мок-бином.
Builder
Порождающий паттерн, который выносит создание сложного объекта в отдельный «сборщик». Клиент задаёт параметры по шагам через простой интерфейс, а строитель проводит валидацию, соблюдает порядок заполнения, может подставлять бизнес значения. Итог — читаемая сборка и корректный объект без «телескопических» конструкторов.
Представим себе обычный бургер в кафе по шагам: булка — котлета — сыр — соус. Гость нажимает «кнопки» в приложении, кухня отвечает за порядок и съедобность.
Рассмотрим «ванильный» пример формирования заказа. Объект собирается пошагово через билдер: «бизнес-поля» задаёт клиент, а служебные заполняются автоматически внутри сборки. Например, id
и createdAt
выставляются внутри конструктора при build()
, так что клиенту не нужно о них заботиться — меньше ошибок и зависимостей.
Реализация Builder для сущности Order
final class Order {
private final String id; // генерируется внутри
private final Instant createdAt; // заполняется внутри
private final String customerName; // задаёт пользователь
private final String comment; // опционально
private Order(Builder b) {
this.id = UUID.randomUUID().toString();
this.createdAt = Instant.now();
this.customerName = Objects.requireNonNull(b.customerName,
"customer name is required");
this.comment = b.comment;
}
public static Builder builder() { return new Builder(); }
static final class Builder {
private String customerName;
private String comment;
Builder customerName(String v) {
this.customerName = v; return this;
}
Builder comment(String v) {
this.comment = v; return this;
}
Order build() { return new Order(this); }
}
@Override public String toString() {
return "Order{id='%s', createdAt=%s, customerName='%s', comment='%s'}"
.formatted(id, createdAt, customerName, comment);
}
}
// Пример использования на клиенте
public class Main {
public static void main(String[] args) {
Order o = Order.builder()
.customerName("andrey.brombin")
.comment("Подписаться на блог")
.build();
/*
Пример возможного вывода println(o):
Order{
id='3a7c2e7e-b0c6-4b13-9a4c-9d1a2f6f8b2a',
createdAt=2025-10-13T03:00:00Z,
customerName='andrey.brombin',
comment='Подписаться на блог'}
*/
}
}
Для «частичных изменений» в объектах обычно делают with-методы (copy-on-write) или toBuilder()
c предзаполненными полями, не раскрывая служебные поля:
Методы для частичного изменения сущности Order
// Метод для изменения одного поля, возвращающий копию
public Order withComment(String newComment) {
return new Order(this.id, this.createdAt, this.customerName, newComment);
}
// Метод изменения сразу нескольких атрибутов
public Builder toBuilder() {
return new Builder()
.withExistingMeta(this.id, this.createdAt)
.customerName(this.customerName)
.comment(this.comment);
}
static final class Builder {
private String existingId; // проставляется toBuilder()
private Instant existingCreatedAt; // проставляется toBuilder()
private String customerName;
private String comment;
// служебный метод для переноса метаданных
Builder withExistingMeta(String id, Instant createdAt) {
this.existingId = id;
this.existingCreatedAt = createdAt;
return this;
}
public Builder customerName(String v) {
this.customerName = v; return this;
}
public Builder comment(String v) {
this.comment = v; return this;
}
public Order build() {
return new Order(this);
}
}
Скорее всего, вы уже сталкивались с паттерном Builder: в Spring — классический билдер HTTP-ответа, в Lombok — аннотация @Builder
, чтобы не писать сборщик вручную. Паттерн на виду и встречается часто:
Lombok и HTTP response билдеры
// Spring реализация HTTP response
@GetMapping("/orders/{id}")
public ResponseEntity<Order> get(@PathVariable String id) {
Order o = /* ...load... */;
return ResponseEntity
.ok()
.header("X-Request-Id", id)
.body(o);
}
// Lombok вариант
@Value
@Builder(toBuilder = true)
public class Order {
@NonNull String id;
@NonNull String customer;
@Builder.Default LocalDate date = LocalDate.now();
String comment;
@Builder.Default boolean express = false;
}
// Использование
Order o = Order.builder()
.id("1234")
.customer("andrey.brombin")
.express(true)
.comment("Поставить лайк")
.date(LocalDate.parse("2025-10-15"))
.build();
Сценарий применимости
Если много параметров, некоторые из них опциональны, возможных комбинаций — море, а нужна читаемая сборка с «говорящими» именами шагов, а не «телескопический» конструктор, если важна проверка инвариантов перед созданием и модель будет эволюционировать в обозримом будущем — применяем.
Сценарий неприменимости
Если мало параметров, нет инвариантов, логика заполнения примитивна, нет реальной комбинации опций — не применяем.
Prototype
Порождающий паттерн, подразумевающий создание «эталонного» объекта (прототипа), работая с которым мы не прогоняем заново все шаги инициализации, а получаем копированием новые объекты с точечными правками. Исходный объект остаётся неизменным.
Представим ситуацию: нам нужно отправить в разные инстанции почти одинаковое письмо. Для этого возьмём шаблон письма, сделаем копию и поменяем пару строк, например, подразделение и адрес, после чего отправляем, будучи уверенными, что ничего не забыли.

В качестве примера, будем копировать документы:
Контракт Prototype<Document>
// Контракт: "умею копироваться"
interface Prototype<T> { T copy(); }
// Простой документ: хотим быстро получать похожие версии
final class Document implements Prototype<Document> {
private final String title;
private final String text;
Document(String title, String text) {
this.title = title;
this.text = text;
}
@Override
public Document copy() {
// Обычная копия: то же самое, новые ссылки
return new Document(this.title, this.text);
}
// Копия, где поменяли только заголовок
public Document copyWithTitle(String t) {
return new Document(t, this.text);
}
// Копия, где поменяли только текст
public Document copyWithText(String tx) {
return new Document(this.title, tx);
}
}
В этом примере можно увидеть схожесть с изменением объекта с помощью toBuilder()
. Напомню, он преобразовывает текущий объект без изменения исходного прототипа в предзаполненный билдер, а после мы изменяем нужные поля и получаем копию.
Если в составе объекта есть мутабельные структуры (коллекции, массивы, вложенные объекты), простая копия ссылок приведёт к «общему состоянию» между копиями. В таком случае нужно делать глубокое копирование:
Глубокое копирование
import java.util.ArrayList;
import java.util.List;
final class RichDoc implements Prototype<RichDoc> {
private final String title;
private final List<String> tags; // мутабельная коллекция!
RichDoc(String title, List<String> tags) {
this.title = title;
// защитная копия в конструкторе
this.tags = new ArrayList<>(tags);
}
@Override
public RichDoc copy() {
// глубокая копия коллекции (элементы String иммутабельны)
return new RichDoc(this.title, new ArrayList<>(this.tags));
}
public RichDoc copyWithTitle(String t) { return new RichDoc(t, new ArrayList<>(this.tags)); }
public RichDoc copyWithTags(List<String> ts) { return new RichDoc(this.title, new ArrayList<>(ts)); }
}
Сценарий применимости
Если создание обходится дорого или требует некоторого множества этапов. Когда нужны вариации одного базового состояния. Важно, чтобы прототип оставался неизменяемым, правкам поддавались только копии.
Сценарий неприменимости
В том случае, когда создание объекта дешёвое и не требует множества операций, следовательно, копирование ничего не упрощает. Когда нужен совсем другой объект (не вариация текущего состояния), тогда лучше использовать конструктор или фабрика. Если состав сложный (вложенные объекты, коллекции, граф ссылок), а глубокая копия трудозатратна и рискованна, а значит, велик шанс «утечек» общего состояния.
Заключение первой части
В этой статье мы разобрались с порождающими шаблонами. Узнали, когда стоит использовать паттерн, а когда лучше не усложнять код. Во всех примерах преследуется одна цель — сделать создание объектов абстрактным для клиента, а для читателя кода — прозрачным и контролируемым. Структурные и поведенческие паттерны рассмотрим в следующей части статьи.
Присоединяйтесь к моему Telegram-каналу. Я публикую в нём полезные материалы, ресурсы для подготовки к собеседованию, а также в канале можно найти дорожную карту Java-разработчика. Если у вас появились вопросы, идеи или замечания, делитесь ими в комментариях. Вместе мы сделаем наш путь ещё интереснее и полезнее.
Предлагаю также присоединится к прочтению материалов о Дорожной карте Java-разработчика. До встречи в будущих статьях!
© 2025 ООО «МТ ФИНАНС»