Перейти к основному содержимому

Порождающие паттерны

Разработчику Архитектору Аналитику
Теория данных (раздел 3)

Play ITЗагрузка интерактивного демо…


Порождающие паттерны проектирования — это группа шаблонов, направленных на решение задач, связанных с созданием объектов. Их основная цель — обеспечить гибкость и контроль над процессом инстанцирования, делая код более устойчивым к изменениям, легче тестируемым и проще поддерживаемым. Вместо прямого вызова конструкторов или жёсткой привязки к конкретным типам, порождающие паттерны переносят ответственность за создание объектов в отдельные компоненты, которые могут быть заменены, расширены или адаптированы без вмешательства в основную логику приложения.

Эти паттерны особенно полезны в крупных системах, где требования к архитектуре, расширяемости и тестируемости высоки. Они позволяют изолировать логику создания от логики использования, что упрощает модификацию поведения системы при изменении условий — например, при переходе от одного типа хранилища данных к другому, при добавлении новых видов обработчиков запросов или при необходимости внедрения зависимостей в тестовой среде.


Порождающие паттерны - создание объектов без жёсткой связи

Создание объектов — одна из самых частых операций в программировании. На первый взгляд, вызов конструктора выглядит простым и прозрачным. Однако в реальных проектах эта операция быстро становится источником сложности. Прямое инстанцирование конкретных классов приводит к жёсткой связанности между компонентами. Если один модуль создаёт экземпляр другого, он зависит от его имени, сигнатур конструктора и внутренней структуры. Любое изменение в этих деталях требует правки в каждом месте, где происходит создание.

Порождающие паттерны решают эту проблему, вводя абстракции между клиентским кодом и механизмом создания. Клиент больше не знает, какой именно класс будет создан, и не управляет деталями инициализации. Он взаимодействует с интерфейсом или фабрикой, которая скрывает реализацию. Это позволяет легко подменять реализации, добавлять новые типы без изменения существующего кода и централизовать логику создания, что упрощает управление жизненным циклом объектов.

Кроме того, порождающие паттерны помогают соблюдать принцип единственной ответственности — класс, отвечающий за бизнес-логику, не должен одновременно заботиться о том, как создаются его зависимости. Отделение этих обязанностей делает код чище, логичнее и проще для понимания.


Три подхода к созданию объектов (уровень "фабрики")

В учебниках и на ревью слово "фабрика" часто смешивают три разные идеи. В каталоге GoF только две из них — официальные паттерны.

ПодходСутьВ каталоге GoF?Когда выбирать
Простая фабрика (Simple Factory)Отдельный класс/метод с switch или map: по ключу возвращает ProductНет (идиома / приём)Один тип продукта, мало вариантов, быстрый старт
Фабричный методПодкласс Creator переопределяет createProduct()ДаИерархия создателей, расширение через наследование
Абстрактная фабрикаИнтерфейс фабрики создаёт семейство согласованных продуктовДаПлатформа, тема UI, набор Connection + Command для одной СУБД

Клиент порождающего кода должен зависеть от абстракции продукта, а не от new ConcreteType() в бизнес-методе — это следствие инверсии зависимостей и тема принципов перед паттернами.

Простая фабрика и DI

В .NET и Spring "фабрика" часто заменяется регистрацией в контейнере — см. Фабрика в C#. Идея та же: клиент не выбирает конкретный класс вручную.


Основные порождающие паттерны

В каноническом справочнике "Банды четырёх" (GoF) описаны пять ключевых порождающих паттернов — Фабричный метод, Абстрактная фабрика, Строитель, Одиночка и Прототип. Каждый из них решает свою специфическую задачу, но все они объединены общей идеей — делегирование создания объектов.

Play ITЗагрузка интерактивного демо…


Фабричный метод

Фабричный метод определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать. Этот паттерн позволяет классу делегировать создание экземпляров дочерним классам, не указывая их конкретные типы.

Основная идея заключается в том, что базовый класс содержит метод, который возвращает объект определённого интерфейса или абстрактного типа. Конкретная реализация этого метода переопределяется в подклассах, и каждый из них создаёт свой вариант продукта. Клиентский код работает с базовым интерфейсом, не зная, какой именно подкласс был использован для создания объекта.

Такой подход особенно полезен, когда система должна быть независимой от способа создания, компоновки и представления продуктов. Например, библиотека для работы с документами может использовать фабричный метод для создания парсеров в зависимости от формата файла: PDF, DOCX или TXT. Каждый формат обрабатывается своим подклассом, но основной код остаётся универсальным.

Фабричный метод также упрощает добавление новых типов. Чтобы поддержать новый формат, достаточно создать новый подкласс и реализовать в нём метод создания — без изменения существующих частей системы.

Коротко:

  • Выносит создание объекта в отдельный метод. Подкласс решает, какой именно объект создать.
  • Есть суперкласс с методом createThing(), а наследники переопределяют его (кто-то создаёт Car, кто-то Ship).
  • У тебя есть иерархия типов, но заранее не знаешь, какой именно понадобится.

Абстрактная фабрика

Абстрактная фабрика предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов без указания их конкретных классов. В отличие от фабричного метода, который создаёт один тип продукта, абстрактная фабрика отвечает за целую группу объектов, которые логически связаны между собой.

Этот паттерн особенно актуален в системах с несколькими вариациями тем или платформ. Например, графическое приложение может поддерживать разные наборы элементов интерфейса: для Windows, macOS и Linux. Каждый набор включает кнопки, поля ввода, меню и другие компоненты, которые должны быть согласованы по стилю и поведению. Абстрактная фабрика позволяет создать одну фабрику для Windows, другую — для macOS, и третью — для Linux. Каждая фабрика реализует один и тот же интерфейс, но возвращает соответствующие реализации компонентов.

Клиентский код получает фабрику как параметр или через конфигурацию и использует её для создания всех необходимых объектов. Он не знает, какая именно фабрика используется, и не зависит от конкретных реализаций. Это обеспечивает высокую степень гибкости и позволяет легко переключаться между темами или платформами.

Абстрактная фабрика также упрощает поддержку системы. Все изменения, связанные с добавлением новой платформы, сосредоточены в одном месте — новой реализации фабрики. Остальной код остаётся неизменным.

Практика на современном .NET (DI-контейнер, делегат-фабрика и keyed services) разобрана в Паттерн "Фабрика" в C#.

Коротко:

  • Фабрика, которая создаёт семейства связанных объектов (например, GUIFactory делает Button+Scrollbar сразу для винды или мака).
  • Надо менять целый набор зависимых объектов одним чекбоксом.

Строитель

Строитель разделяет процесс конструирования сложного объекта от его представления, чтобы один и тот же процесс построения мог создавать разные представления. Этот паттерн применяется, когда объект имеет множество опциональных частей, этапов инициализации или когда его создание требует выполнения последовательности шагов.

Вместо передачи десятков параметров в конструктор или установки множества свойств после создания, строитель предлагает пошаговый подход. Клиент взаимодействует со строителем, вызывая методы для настройки отдельных частей объекта. После завершения настройки вызывается метод получения результата, который возвращает готовый объект.

Строитель особенно полезен при работе с неизменяемыми объектами, где все данные должны быть заданы при создании. Он также помогает избежать ошибок, связанных с неполной инициализацией: пока не вызван метод завершения, объект не считается готовым к использованию.

Часто строитель реализуется в виде fluent-интерфейса, где каждый метод возвращает самого себя, позволяя цепочку вызовов. Это делает код более читаемым и выразительным. Например, создание HTTP-запроса может выглядеть как new RequestBuilder().setMethod("POST").setUrl("...").setBody("...").build().

Строитель также упрощает создание объектов с большим количеством конфигураций. Разные строители могут реализовывать разные стратегии построения, например, для тестовых и продакшен-сред.

Коротко:

  • Создаёт сложный объект по шагам, отделяя конструирование от представления.
  • builder.addWheels(4).addEngine().build(). В конце дёргаешь getResult().
  • У объекта 100 параметров, часть опциональны, или нужно получить разные конфигурации (Pizza: с беконом, без лука).

Практика на Java (immutable-конфигурация HTTP-клиента, build() и валидация) — в Builder в Java.


Одиночка

Одиночка гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Этот паттерн используется, когда необходимость в единственном экземпляре диктуется самой природой задачи — например, центральный объект управления, глобальный регистр настроек или подключение к внешнему ресурсу, которое должно быть уникальным.

Реализация одиночки обычно включает приватный конструктор, чтобы предотвратить прямое создание экземпляров, и статический метод или свойство, которое возвращает единственный экземпляр. При первом обращении экземпляр создаётся, при последующих — возвращается уже существующий.

Одиночка упрощает доступ к общим ресурсам и обеспечивает согласованность состояния. Однако его следует использовать с осторожностью. Глобальное состояние усложняет тестирование, так как создаёт скрытые зависимости между компонентами. Кроме того, одиночка может стать узким местом в многопоточных приложениях, если не обеспечена потокобезопасность.

В современных архитектурах часто предпочитают явное внедрение зависимостей вместо глобального доступа. Тем не менее, одиночка остаётся полезным в ограниченных контекстах, где действительно требуется единственный экземпляр с глобальной видимостью.

Коротко:

  • Класс, у которого может быть только один экземпляр на всё приложение.
  • Приватный конструктор + статический метод getInstance(), который возвращает один и тот же объект.
  • Логгер, конфиг, пул потоков. Не стоит использовать везде — это глобальное состояние.
Практика на C#

Пошаговая реализация с Lazy<T>, сравнением с DI (AddSingleton) и тестами — лабораторный кейс "Singleton на C#".

Маршрут по языку — раздел C#.


Прототип

Прототип задаёт виды создаваемых объектов с помощью экземпляра-прототипа и создаёт новые объекты путём копирования этого прототипа. Этот паттерн полезен, когда создание объекта напрямую через конструктор слишком затратно или сложно, а копирование существующего экземпляра — более эффективный путь.

Вместо повторного выполнения логики инициализации, система клонирует уже настроенный объект. Это особенно актуально для объектов с глубокой иерархией настроек, сложными зависимостями или дорогостоящей подготовкой. Например, в играх часто используют прототипы для создания врагов: сначала создаётся базовый экземпляр с полной конфигурацией, а затем его копии размещаются на карте с минимальными изменениями.

Прототип требует, чтобы объекты поддерживали операцию клонирования. В языках с автоматическим управлением памятью это обычно реализуется через интерфейс или метод, который создаёт глубокую или поверхностную копию. Глубокое копирование воспроизводит всю структуру объекта, включая вложенные объекты, что гарантирует полную независимость копии.

Использование прототипа ускоряет создание объектов и упрощает добавление новых типов: достаточно зарегистрировать новый прототип в реестре, и система сможет создавать его копии без знания внутренней структуры.

Коротко:

  • Создаёшь новый объект клонированием существующего.
  • Вместо new Something(сложные параметры)existing.clone().
  • Создавать объект дороже, чем скопировать, или когда тип не известен до рантайма.

Сравнение и выбор порождающего паттерна

Каждый из порождающих паттернов решает свою задачу, но границы между ними не всегда чёткие. Выбор конкретного паттерна зависит от контекста — от структуры объектов, от требований к гибкости, от необходимости создания семейств или от сложности процесса инициализации.

Фабричный метод подходит, когда нужно делегировать создание одного типа продукта подклассам. Он минимален по своей структуре и не требует введения дополнительных абстракций, кроме базового класса и его наследников. Этот паттерн часто используется в библиотеках и фреймворках, где разработчик предоставляет каркас, а пользователь реализует конкретные варианты поведения.

Абстрактная фабрика применяется, когда система работает с несколькими связанными типами объектов. Если замена одного компонента требует согласованной замены других, абстрактная фабрика обеспечивает целостность конфигурации. Она особенно полезна при поддержке нескольких платформ, тем оформления или наборов зависимостей, где все элементы должны быть согласованы между собой.

Строитель выбирают, когда объект имеет сложную структуру или множество опциональных параметров. Он позволяет отделить логику построения от логики представления и даёт контроль над каждым этапом создания. Строитель также упрощает работу с неизменяемыми объектами, так как все данные задаются до момента получения результата.

Одиночка оправдан только в тех случаях, когда действительно необходим единственный экземпляр с глобальной точкой доступа. Его следует применять осторожно, так как он создаёт скрытые зависимости и усложняет тестирование. В современных системах одиночку часто заменяют внедрением зависимостей с ограниченным временем жизни, например, через контейнеры IoC.

Прототип эффективен, когда создание объекта через конструктор дорого или неудобно, а клонирование уже настроенного экземпляра — более разумный подход. Он особенно популярен в системах с динамической конфигурацией, игровых движках и графических редакторах, где требуется быстрое создание множества похожих объектов.


Практические рекомендации

При проектировании системы важно не стремиться применить паттерн ради самого паттерна. Порождающие паттерны — это инструменты для решения конкретных проблем, а не универсальные рецепты. Их стоит вводить, когда возникает реальная потребность в гибкости, тестируемости или управлении сложностью.

Если код содержит множество вызовов new с жёстко прописанными типами, это сигнал к рассмотрению фабричного метода или абстрактной фабрики. Если объекты создаются с десятками параметров, часть из которых опциональна, строитель поможет упорядочить процесс. Если в системе есть объект, который должен существовать в единственном экземпляре и использоваться повсеместно, одиночка может быть оправдан, но только после тщательного анализа последствий. Если инициализация объекта требует выполнения множества шагов или загрузки данных из внешних источников, прототип позволит избежать повторной настройки.

Важно помнить, что паттерны можно комбинировать. Например, строитель может использовать фабричный метод для создания отдельных частей сложного объекта. Абстрактная фабрика может возвращать прототипы вместо новых экземпляров. Такие гибридные подходы позволяют достичь максимальной адаптивности без избыточной сложности.


Примеры применения в реальных системах

Во многих фреймворках и библиотеках порождающие паттерны используются повсеместно. Например, в .NET фреймворке фабричный метод встречается в классах WebRequest.Create, где в зависимости от URI создаётся нужный подкласс запроса — HTTP, FTP или другой. Абстрактная фабрика реализована в ADO.NET через интерфейсы IDbConnection, IDbCommand и соответствующие фабрики для разных СУБД.

В игровых движках, таких как Unity, прототип активно используется для клонирования префабов — заранее настроенных объектов, которые можно быстро размещать на сцене. Строитель применяется в ORM-библиотеках, например, в Entity Framework, где запросы к базе данных строятся пошагово с помощью fluent-интерфейса.

Одиночка, несмотря на критику, остаётся популярным в логгерах, кэшах и менеджерах настроек. Например, класс Logger в NLog или log4net часто реализуется как одиночка, чтобы гарантировать централизованное управление записью логов.

Эти примеры показывают, что порождающие паттерны не являются теоретическими конструкциями — они решают практические задачи, с которыми сталкиваются разработчики ежедневно.


Быстрые сценарии выбора

Чтобы не путаться между порождающими паттернами, полезно смотреть на форму задачи:

  • нужен один продукт и выбор делается в наследниках -> Фабричный метод;
  • нужно семейство согласованных продуктов -> Абстрактная фабрика;
  • нужен пошаговый конструктор сложного объекта -> Строитель;
  • нужен единый экземпляр на приложение -> Одиночка;
  • нужны быстрые копии заранее настроенных объектов -> Прототип.

Эта карта хорошо работает на code review и архитектурных обсуждениях.


Проверка пользы после внедрения

После рефакторинга под порождающий паттерн проверьте практический эффект:

  • стало ли проще добавить новый тип продукта без изменений клиентского кода;
  • сократилось ли число мест с прямым вызовом сложных конструкторов;
  • стало ли легче писать тесты через подмену механизмов создания;
  • не появилась ли избыточная иерархия ради 1-2 сценариев.

Если первые пункты улучшаются, а последний нет — паттерн применен по делу.


Внутренние связи по энциклопедии

Порождающие паттерны на границе с интеграцией (клиенты HTTP, фабрики каналов) — 2.09.