Название шаблона
Строитель (Builder).
Тип
Порождающий шаблон проектирования (Creational).
Описание
Строитель позволяет отделить процесс создания сложного объекта от его реализации. При этом, результатом одних и тех же операций могут быть различные объекты.
Данный шаблон используется в случае, если:
- процесс создания объекта можно разделить на части (шаги);
- (и) алгоритм этого процесса не должен зависеть от того, из каких частей состоит объект;
- (и) конструирование должно обеспечивать возможность создавать различные объекты.
Лучше понять работу Строителя можно используя такое сравнение: многие порождающие шаблоны, используя конкретные исходные данные, выдают обобщенный результат (интерфейс объекта). Строитель же наоборот, используя обобщенный набор данных, создает известную клиенту конкретную реализацию.
Обратите внимание:
- данный шаблон не скрывает реализацию порождаемых объектов, а создает то, что требуется;
- как следствие, результатом работы могут быть объекты, не связанные явно между собой. Как правило, у них единые цели, но не обязательно есть общие интерфейсы, базовые классы и т.д.
Шаблон Строитель включает двух участников процесса:
- Строитель (Builder) – предоставляет методы для сборки частей объекта, при необходимости преобразовывает исходные данные в нужный вид, создает и выдает объект;
- Распорядитель (Director) – определяет стратегию сборки: собирает данные и определяет порядок вызовов методов Строителя.
Может возникнуть вопрос: если можно напрямую вызывать методы Строителя, то зачем нужен Распорядитель? Его задача – сокрытие стратегии сборки. Это позволит, при необходимости, модифицировать или даже полностью менять ее, не затрагивая остальной код.
Так же Распорядитель, как правило, отвечает за получение данных для конструирования. И уже потом, Строитель преобразовывает их в вид, необходимый для порождаемого объекта. Такое разделение связано с тем, что создаваемый объект скрыт от Распорядителя и, кроме того, может не уметь работать с форматом исходных данных.
Схожие шаблоны и их отличия
Строитель |
Фабричный метод |
Абстрактная фабрика |
Создает в несколько шагов один сложный (составной) объект. |
Порождает один объект с определенным интерфейсом. |
Порождает семейство объектов с определенными интерфейсами. |
Интерфейс строителя, реализуемый классами, и класс для управления процессом. |
Метод класса, который переопределяется потомками. |
Интерфейс, реализуемый классами. |
Скрывает процесс создания объекта, порождает требуемую реализацию. |
Скрывает реализацию объекта. |
Скрывает реализацию семейства объектов. |
Реализация шаблона в общем виде
- определяем шаги конструирования сложного объекта, и на их основе разрабатываем интерфейс Строителя IBuilder;
- если планируется несколько стратегий сборки, то создаем интерфейс Распорядителя IDirector;
- разрабатываем класс Распорядителя MyDirector (реализующий IDirector), работающий со Строителями через интерфейс IBuilder;
- создаем класс Строителя MyBuilder, реализующий интерфейс IBuilder и метод получения результата;
- в клиентском коде экземпляру MyDirector передаем интерфейс IBuilder экземпляра MyBuilder;
- запускаем процесс сборки, вызвав метод Распорядителя;
- получаем созданный экземпляр MyProduct у используемой реализации Строителя MyBuilder.
Возможно возникнет вопрос о необходимости создания интерфейсов. Почему не перейти сразу к классам? Потому, что такой подход позволит в дальнейшем, изменяя реализации Строителя и Распорядителя, влиять результат конструирования. Однако, если не планируются разные стратегии сборки, то разработку интерфейса Распорядителя можно пропустить.
Давайте перейдем к примерам, которые прояснят детали использования шаблона Строитель.
Примеры реализации
1. Создание различных конфигураций одного объекта
В начале, рассмотрим ситуацию, когда Строитель создает различные конфигурации объекта.
Для примера, возьмем часть кода сайта, ответственную за генерацию страниц. Необходимо создать объект Page, который содержит HTML код для выбранной страницы.
Определим шаги конструирования страницы: создаем шапку (Header), добавляем элементы меню (MenuItems), выводим публикации (Post) и завершаем страницу кодом подвала (Footer). Эти четыре шага и будут определять интерфейс Строителя:
public interface IPageBuilder
{
void BuildHeader(HeaderData header);
void BuildMenu(MenuItems menuItems);
void BuildPost(PostData post);
void BuildFooter(FooterData footer);
}
Поскольку стратегия сборки будет одна, то сразу приступим к реализации Распорядителя. Его использование не имеет смысла без экземпляра Строителя. Поэтому конструктор будет требовать передачи ему интерфейса IPageBuilder.
Так же сразу создадим метод BuildPage(), определяющий стратегию сборки страницы. В нем получим данные для выбранной страницы (экземпляр класса PageData) и по шагам вызовем методы Строителя. Код методов, получающих данные страницы, не приведен для краткости.
public class PageDirector
{
private readonly IPageBuilder _builder;
private HeaderData GetHeader(int pageId) { /* SKIPPED */ }
private MenuItems GetMenuItems(int pageId) { /* SKIPPED */ }
private IEnumerable<PostData> GetPosts(int pageId) { /* SKIPPED */ }
private FooterData GetFooter(int pageId) { /* SKIPPED */ }
public PageDirector(IPageBuilder builder)
{
this._builder = builder;
}
public void BuildPage(int pageId)
{
this._builder.BuildHeader(this.GetHeader(pageId));
this._builder.BuildMenu(this.GetMenuItems(pageId));
foreach (PostData post in this.GetPosts(pageId)) {
this._builder.BuildPost(post);
}
this._builder.BuildFooter(this.GetFooter(pageId));
}
}
Осталось создать класс Строителя. Реализуем интерфейса IPageBuilder и напишем метод GetResult(), возвращающий результат сборки. Для упрощения примера, будем просто передавать данные в создаваемый экземпляр класса Page. Обратите внимание на его объявление с использованием readonly. Это гарантирует, что ни один из шагов не пересоздаст объект.
public class PageBuilder : IPageBuilder
{
private readonly Page _page = new Page();
public void BuildHeader(HeaderData header) { this._page.AddHeader(header); }
public void BuildMenu(MenuItems menuItems) { this._page.SetMenuItems(menuItems); }
public void BuildPost(PostData post) { this._page.AddPost(post); }
public void BuildFooter(FooterData footer) { this._page.AddFooter(footer); }
public Page GetResult() { return this._page; }
}
Все готово к использованию, например вот так:
public void PostPage(int pageId)
{
PageBuilder pageBuilder = new PageBuilder();
PageDirector pageDirector = new PageDirector(pageBuilder);
pageDirector.BuildPage(pageId);
Page page = pageBuilder.GetResult();
this.Post(page);
}
Все достаточно просто: создали Строителя и Распорядителя, приказали создать объект и забрали результат и отправили его на вывод.
Дальше потребовалось создать версию страницы для печати. А это значит, что шапка, меню и подвал нам не нужны. Кроме того, надо подготовить публикацию к печати, вызвав PreparePostToPrinter(). Поэтому разработаем еще одного Строителя, который будет собирать нужную нам конфигурацию объекта Page.
public class PrintPageBuilder : IPageBuilder
{
private readonly Page _page = new Page();
private PostData PreparePostToPrinter(PostData post) { /* SKIPPED */ }
public void BuildHeader(HeaderData header) { }
public void BuildMenu(MenuItems menuItems) { }
public void BuildPost(PostData post)
{
PostData postToPrint = this.PreparePostToPrinter(post);
this._page.AddPost(postToPrint);
}
public void BuildFooter(FooterData footer) { }
public Page GetResult() { return this._page; }
}
Как видно из кода, методы ненужных операций стали заглушками. И если, в приведенном выше методе PostPage(), заменить PageBuilder на PrintPageBuilder, то получим сконфигурированный для печати результат.
Можно улучшить приведенный пример, если будет создаваться экземпляры одного класса, но разной конфигурации. В этом случае, надо объявить метод GetResult() в интерфейсе Строителя. Это позволит использовать параметризованный фабричный метод для создания его нужной реализации.
2. Создание различных объектов
Давайте рассмотрим еще один пример. Необходимо получить объект PostImageList, содержащий список изображений в публикациях на выбранной странице.
В классе Распорядителя уже есть методы получения данных страницы. Поэтому достаточно написать нового Строителя для PostImageList. Поставим заглушки на все методы, кроме метода создания публикаций. В нем обработаем данные и сохраним ссылки на все изображения:
public class PageImageListBuilder : IPageBuilder
{
private readonly PageImageList _imageList = new PageImageList();
private IEnumerable PostImages(PostData post) { /* SKIPPED */ }
public void BuildHeader(HeaderData header) { }
public void BuildMenu(MenuItems menuItems) { }
public void BuildFooter(FooterData footer) { }
public void BuildPost(PostData post)
{
foreach (string imageUrl in this.PostImages(post)) {
this._imageList.AddImageUrl(imageUrl);
}
}
public PageImageList GetResult() { return this._imageList; }
}
Тут важно отметить, что, в отличии от первого примера, создается экземпляр другого класса (не Page, а PageImageList). При этом код Распорядителя никак не изменился и был полностью повторно использован. Такой подход можно часто встретить, когда из готового набора данных необходимо создавать различные результаты.