Строитель (Builder) – шаблон проектирования, который инкапсулирует создание объекта и позволяет разделить его на различные этапы.
Когда использовать паттерн Строитель?
- Когда процесс создания нового объекта не должен зависеть от того, из каких частей этот объект состоит и как эти части связаны между собой
- Когда необходимо обеспечить получение различных вариаций объекта в процессе его создания
Формальное определение на C# могло бы выглядеть так:
class Client
{
void Main()
{
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.Construct();
Product product = builder.GetResult();
}
}
class Director
{
Builder builder;
public Director(Builder builder)
{
this.builder = builder;
}
public void Construct()
{
builder.BuildPartA();
builder.BuildPartB();
builder.BuildPartC();
}
}
abstract class Builder
{
public abstract void BuildPartA();
public abstract void BuildPartB();
public abstract void BuildPartC();
public abstract Product GetResult();
}
class Product
{
List<object> parts = new List<object>();
public void Add(string part)
{
parts.Add(part);
}
}
class ConcreteBuilder : Builder
{
Product product = new Product();
public override void BuildPartA()
{
product.Add("Part A");
}
public override void BuildPartB()
{
product.Add("Part B");
}
public override void BuildPartC()
{
product.Add("Part C");
}
public override Product GetResult()
{
return product;
}
}
Участники
- Product: представляет объект, который должен быть создан. В данном случае все части объекта заключены в списке parts.
- Builder: определяет интерфейс для создания различных частей объекта Product
- ConcreteBuilder: конкретная реализация Buildera. Создает объект Product и определяет интерфейс для доступа к нему
- Director: распорядитель – создает объект, используя объекты Builder
Рассмотрим применение паттерна на примере выпечки хлеба. Как известно, даже обычный хлеб включает множество компонентов. Мы можем использовать для представления хлеба и его компонентов следующие классы:
//мука
class Flour
{
// какого сорта мука
public string Sort { get; set; }
}
// соль
class Salt
{}
// пищевые добавки
class Additives
{
public string Name { get; set; }
}
class Bread
{
// пшеничная мука
public Flour WheatFlour { get; set; }
// ржаная мука
public Flour RyeFlour { get; set; }
// соль
public Salt Salt { get; set; }
// пищевые добавки
public Additives Additives { get; set; }
public override string ToString()
{
StringBuilder sb = new StringBuilder();
if (WheatFlour != null)
sb.Append("Пшеничная мука " + WheatFlour.Sort + "n");
if (RyeFlour != null)
sb.Append("Ржаная мука " + RyeFlour.Sort + " n");
if (Salt != null)
sb.Append("Соль n");
if (Additives != null)
sb.Append("Добавки: "+Additives.Name+" n");
return sb.ToString();
}
}
Кстати в данном случае для построения строки используется одна из встроенных реализаций паттерна Builder в .NET – класс StringBuilder.
Хлеб может иметь различную комбинацию компонентов: ржаной и пшеничной муки, соли, пищевых добавок. И нам надо обеспечить выпечку разных сортов хлеба. Для разных сортов хлеба может варьироваться конкретный набор компонентов, не все компоненты могут использоваться. И для этой задачи применим паттерн Builder:
class Program
{
static void Main(string[] args)
{
// содаем объект пекаря
Baker baker = new Baker();
// создаем билдер для ржаного хлеба
BreadBuilder builder = new RyeBreadBuilder();
// выпекаем
baker.Bake(builder);
Bread ryeBread = builder.Bread;
Console.WriteLine(ryeBread.ToString());
// оздаем билдер для пшеничного хлеба
builder = new WheatBreadBuilder();
baker.Bake(builder);
Bread wheatBread = builder.Bread;
Console.WriteLine(wheatBread.ToString());
Console.Read();
}
}
// абстрактный класс строителя
abstract class BreadBuilder
{
public Bread Bread { get; private set; }
public BreadBuilder()
{
Bread = new Bread();
}
public abstract void SetWheatFlour();
public abstract void SetRyeFlour();
public abstract void SetSalt();
public abstract void SetAdditives();
}
// пекарь
class Baker
{
public void Bake(BreadBuilder breadBuilder)
{
breadBuilder.SetWheatFlour();
breadBuilder.SetRyeFlour();
breadBuilder.SetSalt();
breadBuilder.SetAdditives();
}
}
// строитель для ржаного хлеба
class RyeBreadBuilder : BreadBuilder
{
public override void SetWheatFlour()
{
// не используется
}
public override void SetRyeFlour()
{
this.Bread.RyeFlour = new Flour { Sort = "1 сорт" };
}
public override void SetSalt()
{
this.Bread.Salt = new Salt();
}
public override void SetAdditives()
{
// не используется
}
}
// строитель для пшеничного хлеба
class WheatBreadBuilder : BreadBuilder
{
public override void SetWheatFlour()
{
this.Bread.WheatFlour = new Flour { Sort = "высший сорт" };
}
public override void SetRyeFlour()
{
// не используется
}
public override void SetSalt()
{
this.Bread.Salt = new Salt();
}
public override void SetAdditives()
{
this.Bread.Additives = new Additives { Name = "улучшитель хлебопекарный" };
}
}
Консольный вывод программы:
Ржаная мука 1 сорт Соль Пшеничная мука высший сорт Соль Добавки: улучшитель хлебопекарный
В данном случае с помощью конкретных строителей RyeBreadBuilder и WheatBreadBuilder создаются объекты Bread с определенным набором. В роли распорядителя выступает класс пекаря Baker, который вызывает методы конкретных строителей для построения нового объекта.
Реализация шаблона в общем виде
- определяем шаги конструирования сложного объекта, и на их основе разрабатываем интерфейс Строителя 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; }
}
Тут важно отметить, что, в отличии от первого примера, возвращается экземпляр другого класса. Однако использование Строителя почти аналогичное. Надо только поменять имена его класса и класса результата. Код Распорядителя никак не изменился и был полностью повторно использован для новой цели.
Такой подход можно часто встретить, когда из готового набора данных надо создавать различные варианты. Например, из XML надо создавать объекты, содержащие его HTML, RTF или TXT представления. В этом случае Распорядитель будет отвечать за получение данных из XML, а Строители за преобразование в нужный формат и создание объектов.