Строитель – паттерн, конструирующий, собирающий объект.
Условия, Задача, Назначение
Отделяет конструирование сложного объекта от его модели, т.е содержания и представления.
Подробней. Есть клиент, которому часто нужно создавать различные, сложные объекты. При этом процесс создания всех этих объектов – протекает очень схожими этапами, так что можно очертить общий единый алгоритм, включающий выполнение всех возможных действий, которые только могут потребоваться для построения всех сложных объектов.
При этом важный момент – конструируемые сложные объекты в общем случае не схожи между собой, т.е. невозможно разумным образом как-то обобщить все эти создаваемые объекты в единую иерархию, объединить общие свойства и т.д. Более того – на самом деле, это и не нужно делать! В смысле – это никому не нужно: клиент всегда знает где, в каком месте и какой ему нужно создать объект и нет необходимости что-то унифицировать.
Таким образом, получаем: различные, сложные объекты, которые нужно создавать на разных этапах и фазах выполнения приложения, а также довольно схожие алгоритмы их конструирования. Поэтому, чтобы не дублировать похожие места кода создания объектов – было бы очень удобно объединить этот процесс в единый алгоритм (Director, распорядитель), способный создать любой из интересующих сложных объектов.
Дальше – т.к. объекты совершенно разные – необходимо чтобы каждый объект имел свое конкретное внутреннее представление (структуру), т.к. невозможно создать все эти объекты с одинаковым внутреннем представлением. И вот уже для этого – следует выделить еще одну сущность (Builder, строитель), отвечающую непосредственно уже за создание отдельных частей объекта. Эта сущность будет своя на каждый сложный объект, который может быть построен.
Мотивация
Для небольшого примера рассмотрим кодировщик, в который заложена возможность распознавания и чтения документа в формате RTF (RichTextFormat), должна также «уметь» преобразовывать его во многие другие форматы, например в простой ASCII-текст или в представление, которое можно отобразить в виджете для ввода текста. Однако число вероятных преобразований заранее неизвестно. Поэтому должна быть обеспечена возможность без труда добавлять новый конвертор.
Таким образом, нужно сконфигурировать класс RTFReader с помощью объекта TextConverter, который мог бы преобразовывать RTF в другой текстовый формат. При разборе документа в формате RTF класс RTFReader вызывает TextConverter для выполнения преобразования. Всякий раз, как RTFReader распознает лексему RTF (простой текст или управляющее слово), для ее преобразования объекту TextConverter посылается запрос. Объекты TextConverter отвечают как за преобразование данных, так и за представление лексемы в конкретном формате.
Подклассы TextConverter специализируются на различных преобразованиях и форматах. Например, ASCIIConverter игнорирует запросы на преобразование чего бы то ни было, кроме простого текста. С другой стороны, TeXConverter будет реализовывать все запросы для получения представления в формате редактора TJX, собирая по ходу необходимую информацию о стилях. A TextWidgetConverter станет строить сложный объект пользовательского интерфейса, который позволит пользователю просматривать и редактировать текст.
Класс каждого конвертора принимает механизм создания и сборки сложного объекта и скрывает его за абстрактным интерфейсом. Конвертор отделен от загрузчика, который отвечает за синтаксический разбор RTF-документа.
В паттерне строитель абстрагированы все эти отношения. В нем любой класс
конвертора называется строителем, а загрузчик - распорядителем. В применении
к рассмотренному примеру строитель отделяет алгоритм интерпретации формата текста (т.е. анализатор RTF-документов) от того, как создается и представляется документ в преобразованном формате. Это позволяет повторно использовать алгоритм разбора, реализованный в RTFReader, для создания разных текстовых представлений RTF-документов; достаточно передать в RTFReader различные подклассы класса TextConverter.
Признаки применения, использования паттерна Строитель (Builder)
Если:
- Алгоритм создания сложного объекта не должен зависеть от того, из каких частей состоит объект, и как они стыкуются между собой.
- Процесс конструирования должен позволять создавать различные представления результирующего объекта.
Решение
Участники паттерна Строитель (Builder)
- Builder (TextConverter) – строитель, абстрактный класс.
Содержит перечень всех возможных абстрактных методов для создания и стыковки частей различных объектов (продуктов).
- ConcreteBuilder (ASCIIConverter,TeXConverter,TextWidgetConverter) – конкретный строитель, занимающийся созданием какого-либо одного объекта (продукта).
Конструирует и связывает вместе части продукта посредством реализации только тех абстракных методов интерфейса Builder, которые нужны для создания частей этого продукта. Кроме этого – определяет внутреннее представление этого сложного объекта и предоставляет интерфейс для его получения: GetASCIIText, GetTextWidget.
- Director (RTFReader) – распорядитель.
Фиксированный, единый алгоритм создания любых продуктов, для которых имеется соответствующий Builder. Конструирует продукт пользуясь общим интерфейсом Builder-а.
- Product (ASCIIText, TeXText, TextWidget) – конечный продукт, построенный с помощью конкретного Builder-а.
Представляет сложный конструируемый объект. ConcreteBuilder строит внутреннее представление продукта и определяет процесс его сборки. Помимо этого может включать классы, которые определяют составные части, в том числе интерфейсы для сборки конечного результата из частей.
Схема использования паттерна Строитель (Builder)
- Клиент создает объект-распорядитель Director и конфигурирует его нужным объектом-строителем Builder
- Распорядитель уведомляет строителя о том, что нужно построить очередную часть продукта
- Строитель обрабатывает запросы распорядителя и добавляет новые части к продукту
- Клиент забирает продукт у строителя
Следующая диаграмма взаимодействий иллюстрирует взаимоотношения строителя и распорядителя с клиентом:
Вопросы, касающиеся реализации паттерна Строитель (Builder)
Обычно существует абстрактный класс Builder, в котором определены операции для каждого компонента, который распорядитель может «попросить» создать.
По умолчанию эти операции ничего не делают. Но в классе конкретного строите-
ля ConcreteBuilder замещены те операции, которые необходимо будет вызывать для создания соответствующего объекта и формирования правильной его внутренней структуры.
Вот еще некоторые достойные внимания вопросы реализации:
- Интерфейсы сборки и конструирования.
Строители конструируют свои продукты шаг за шагом. Поэтому интерфейс класса Builder должен быть достаточно общим, чтобы обеспечить конструирование при любом виде конкретного строителя.
Ключевой вопрос проектирования связан с выбором модели процесса конструирования и сборки. Обычно бывает достаточно модели, в которой результаты выполнения запросов на конструирование просто добавляются к продукту. В примере с RTF-документами строитель преобразует и добавляет очередную лексему к уже конвертированному тексту.
- Почему нет абстрактного класса для продуктов?
В типичном случае продукты, изготавливаемые различными строителями, имеют настолько разные представления, что изобретение для них общего родительского класса ничего не дает. В примере с RTF-документами трудно представить себе общий интерфейс у объектов ASCIIText и TextWidget, да он и не нужен.
- Поскольку клиент обычно конфигурирует распорядителя подходящим конкретным строителем, то, надо полагать, ему известно, какой именно подкласс класса Builder используется и как нужно обращаться с произведенными продуктами.
Результаты
Достоинства паттерна строитель (Builder):
- Позволяет задавать разное внутреннее представление продукта.
Объект Director оперирует лишь абстрактным интерфейсом класса Builder для создания сложного продукта по его частям. За этим абстрактным интерфейсом может быть скрыто, в принципе, какое угодно внутреннее представление объекта. Как следствие – для изменения этого внутреннего представления, нужно лишь определить новый вид строителя.
- Отделяет процесс конструирования объектов от процесса создания их внутреннего представления.
Паттерн строитель улучшает модульность, инкапсулируя способ конструирования и представления сложного объекта. Клиентам ничего не надо знать о классах, определяющих внутреннюю структуру продукта, они отсутствуют в интерфейсе строителя.
Каждый конкретный строитель ConcreteBuilder содержит весь код, необходимый для создания и сборки конкретного вида продукта. Код пишется только один раз, после чего разные распорядители могут использовать его повторно для построения вариантов продукта из одних и тех же частей.
В примере с RTF-документом мы могли бы определить загрузчик для формата, отличного от RTF, скажем, SGMLReader, и воспользоваться теми же самыми классами TextConverters для генерирования представлений SGML-документов в виде ASCII-текста, ТеХ-текста или текстового виджета.
- Дает полный, более тонкий контроль над процессом конструирования.
В отличие от порождающих паттернов, которые сразу конструируют весь объект целиком, строитель делает это шаг за шагом под управлением распорядителя. И лишь когда продукт завершен, распорядитель забирает его у строителя. Поэтому интерфейс строителя в большей степени отражает процесс конструирования продукта, нежели другие порождающие паттерны. Это позволяет обеспечить более тонкий контроль над процессом конструирования, а значит, и над внутренней структурой готового продукта.
Пример
Рассмотрим реализацию сборочного конвейера автомобилей. Как известно по нему двигается сначала пустой кузов (каркас), и постепенно на каждом новом этапе к этому каркасу дособираются новые части. В общем случае на конвейере, в принципе, все-равно что собирать – грузовики, легковые и т.д.; главное чтобы соответствующие рабочие бригады выполняли свою работу последовательно на каждом этапе.
Поэтому последовательность наших действий, выполняемых конвейером(ProductionLine) может быть очень просто сведена к определенному всегда одинаковому сценарию: сборка, добавление дверей в авто, колес, двигателя и покраска (метод MakeCar()): ProductionLine.java
Соответственно, получим строитель, дающий возможность выполнять все эти действия: CarBuilder.
Все операции построения в классе CarBuilder по умолчанию ничего не делают. Но они не объявлены исключительно абстрактными, чтобы в производных классах можно было замещать лишь часть методов.
Теперь просто смотрим, какие авто нам нужно будет собирать и реализуем соответствующие строители. К счастью в данной ситуации нам нужны лишь «Десятки» и «КамАЗы», поэтому добавляем только: Vaz2110Builder, KamazBuilder.
Обратите внимание, как строитель скрывает внутреннее представление авто(
CarBuilder), то есть классы колес, дверей, двигателя и т.д., и как эти части собираются вместе для завершения построения машины. Кто-то, может, и догадается, что для представления колес, дверей, двигателя – есть особые классы, но относительно остальных, например, трансмиссии, тормозной системы – нет даже намека. За счет этого становится проще модифицировать способ представления авто, поскольку ни одного из клиентов
CarBuilder изменять не надо.
Как и другие порождающие паттерны, строитель инкапсулирует способ создания объектов (также как и инкапсулирует вспомогательные объекты, фигурирующие в этом процессе: трансмиссия, тормозная система и т.д.); в данном случае с помощью интерфейса, определенного классом
CarBuilder. Это означает, что CarBuilder можно повторно использовать и для создания иных видов машин, например, спортивных. В рамках предметной области это выглядит тоже вполне жизненно: на том же контейнере мы начинаем собирать еще и спортивные авто (метод MakeSportCar()):
ProductionLine
Мы могли бы поместить все операции класса
KamazBuilder и
Vaz2110Builder в класс
Car и позволить каждому авто собирать себя самому. Но чем меньше класс, тем проще он для понимания и модификации, a
Vaz2110Builder легко отделяется от
Car. Еще важнее то, что разделение этих двух классов позволяет иметь множество разновидностей класса
CarBuilder, в каждом из которых есть собственные классы для колес, дверей, сидений.
Но и это еще не все. Вы можете выполнять и различные другие бизнес операции с помощью все той же построенной архитектуры паттерна. Например, вот как вы можете без лишних перекомпиляций решить задачу подсчета движущихся частей в автомобиле:
RotatingPartCountBuilder.
Обратите внимание, что традиционные операции сборки соответствующих частей (MakeDoor, MakeWheel) теперь вообще ничего не делают, кроме добавления предполагаемого количества вращающихся деталей, которые в них содержатся. А также обратите внимание на геттер getCount(), не определенный в интерфейсе CarBuilder конечно, т.к. это не нужно – когда мы хотим подсчитать количество вращающихся деталей авто, собираемых на данном конвейере, мы явно создаем
RotatingPartCountBuilder и после всего процесса обращаемся к нему же за количеством через getCount().
Вот как клиент мог бы все это использовать:
Client