Andrey on .NET | Порождающие шаблоны: Строитель (Builder)

Порождающие шаблоны: Строитель (Builder)

Название шаблона

Строитель (Builder).

Тип

Порождающий шаблон проектирования (Creational).

Описание

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

Данный шаблон используется в случае, если:

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

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

Обратите внимание:

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

Шаблон Строитель включает двух участников процесса:

  • Строитель (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). При этом код Распорядителя никак не изменился и был полностью повторно использован. Такой подход можно часто встретить, когда из готового набора данных необходимо создавать различные результаты.

Комментарии (13) -

Александр 12.10.2010 16:55:50

Спасибо большое за статью, в интернете полным полно описаний паттернов, а толковых статей единицы, жду продолжения!

Антон 02.11.2010 16:02:57

Спасибо! Давно искал что-то подобное!

Чем пользуетесь, чтобы рисовать такие симпатичные диаграммы?

@ MegaHerz:

PowerPoint 2010

Денис 08.09.2011 20:02:44

Спасибо, подтвердил свои знания =)

Андрей 24.10.2013 13:41:33

Спасибо! Сколько раз я пытался разобраться с паттернами и так ничем не оканчивалось. Было либо нудно, либо скучно, либо бестолково - у Вас настолько всё здорово. Описание, диаграммы, примеры, сценарии и тонкости использования.
Если только ещё как-то заметно выделить минусы и антипримеры Smile Но это, конечно, титанический труд.
Опечатка в ссылке "Это позволит использовать параметризованный фабричный метод": andrey.moveax.ru/.../...tional-factory-method.aspx
надо: andrey.moveax.ru/.../factory-method#parameterized

@ Андрей: Спасибо за отзыв и наблюдательность, поправил ссылку.

Николай 20.05.2015 2:16:05

А может ли распорядитель определять какой строитель использовать?
Например в зависимости от входного параметра. Как я понял, в ваше случае данные получаются приватными методами распорядителя в зависимости от userid. А что если на вход в метод Build подается userId и тип отчет reportType. И в зависимости от типа отчета можно выбрать строитель.

Николай На мой взгляд не желательно, т.к. в этом случае распорядитель должен знать о всех типах строителей. Это порождает лишние нежелательные зависимости. Для создания пар "распорядитель+строитель" по каким-то параметрам можно использовать фабрики.

В последнем примере, где создается новый Builder появляются методы-заглушки. Но по сути это лишние методы для этого класса. Мне кажется, что нарушение принципов SOLID а именно - Принцип разделения интерфейса, который гласит что  клиенты не должны зависеть от методов, которые они не используют. Если я прав, то если рабочие варианты устранения этой проблемы ?

Alex А почему вы решили что клиент не использует эти методы (хотя может конечно не стоило называть их "заглушками")? Строитель используется Распорядителем. Он не знает пустые методы или нет. Что конкретно при этом делает Строитель для него не важно. В данном случае у нас просто нет действий по построению некоторых частей объекта.

Мне вчера приходила подобная мысль. Вобщем то да, наверно в контексте паттерна не стоит рассматривать вопрос о нарушении SOLID.

В этом примере важно другое - что шаблон Распорядитель даже не в курсе объект какого типа будет создан. Может было там так же собирать например строку с данными страницы в формате json (тогда все методы бы использовались). Важно что в одном случае мы получали экземпляр Page, в другом PageImageList, а для json вообще бы был string.

Добавить комментарий