Причина
Відокремлює конструкцію комплексних об'єктів від його представлення, отож один процес конструювання може створювати різні представлення.
Мотивація
Читач перетворювача для документів формату RTF (Rich Text Format) повинен вміти перетворювати RTF у велику кількість інших текстових форматів. Читач може перетворювати документи RTF у звичайний текст ASCII чи у текстові віджети, які можуть бути редаговані інтерактивно. Проблема, однак, полягає у тому, що кількість можливих перетворень необмежена. Отож необхідно легко додавати нові перетворення без правки читача.
Рішенням являється конфігурування класу RTFReader за допомогою об'єкту TextConverter, який перетворює RTF у інше текстове представлення. Коли RTFReader аналізує документ RTF, він використовує TextConverter для виконання перетворення. Коли RTFReader розпізнає який-небудь RTF символ (простий текст або контрольне слово RTF), він надсилає запит до TextConverter для перетворення даного символу. Об'єкти TextConverter є відповідальними за виконання перетворення даних і за відображення символів у певному форматі.
Потомки TextConverter спеціалізовуються на різних перетвореннях і форматах. Наприклад, ASCIIConverter ігнорує запити для перетворення будь-чого окрім звичайного тексту.
TeXConverter, з іншого боку, реалізовуватиме операція для усіх запитів для створення TeX репрезентації, що охоплює усю стилістичну інформацію у тексті. TexWidgetConverter буде генерувати складний об'єкт інтерфейсу користувача, який дозволяє користувачу переглядати і редагувати текст.
Кожен клас типу перетворювача бере механізм для створення і складання складного об'єкту і закладає його за абстрактним інтерфейсом. Конвертер відділений від читача, який відповідальний за аналіз RTF документу.
Шаблон Будівник охоплює усі ці відносини. Кожен клас перетворювача називається будівником в шаблоні, і читач називається директором (director). Застосований до цього прикладу, шаблон Будівник відокремлює алгоритм від інтерпретування текстового формату (тобто аналізатора для RTF документів) від того, як перетворений формат стовюється і представляється. Це дозволяє нам повторно використати аналізуючий алгоритм RTFReader'а для створення різних текстових представлень від RTF документу — просто конфігуруючи RTFReader за допомогою різних потомків класу TextConverter.
Застосовуваність
Використовуйте шаблон Будівник коли
- алгоритм для створення складних об'єктів повинен бути незалежним від частин, які створюють об'єкт і того, як вони складаються;
- процес конструювання повинен дозволяти різні представлення для об'єктів, які конструюються;
Структура
Учасники
- Будівник (Builder) (TextConverter)
- визначає абстрактний інтерфейс для створення частин об'єкта продукту (Product)
- ConcreteBuilder (ПевнийБудівник — ASCIIConverter, TeXConcerter, TextWidgetConverter)
- конструює і складає частини продукту реалізовуючи інтерфейс Будівника (Builder);
- визначає і обліковує представлення яке створює.
- забезпечує інтерфейс для отримання продукту (GetASCIIText, GetTextWidget).
- Директор (Director — RTFReader)
- конструює об'єкт використовуючи інтерфейс Будівника.
- Продукт (Product — ASCIIText, TeXText, TextWidget)
- відображає складні об'єкти під час конструювання. ConcreteBuilder (ПевнийБудівник) будує внутрішнє представлення продукту і визначає процес його складання.
- включає класи, які визначають складові частини, включаючи інтерфейси для складання частин у фінальний результат.
Співпрацювання
Клієнт створює об'єкт Director і конфігурує його за допомогою бажаного об'єкту Будівника.
Директор (Director) сповіщає будівника коли необхідно побудувати частину продукту.
Клієнт отримує продукт від будівника.
Наступна діаграма взаємодій ілюструє як Будівник (Builder) і Директор (Director) співпрацюють з клієнтом.
Наслідки
Ось ключові наслідки шаблону Будівника:
- Він дозволяє вам змінювати внутрішнє представлення продукту. Об'єкт Будівника забезпечує директора з абстрактним інтерфейсом для конструювання продукту. Інтерфейс дозволяє будівнику сховати представлення і внутрішню структуру продукту. Вона також приховує те, як продукт складається. Через те, що продукт конструююється через абстрактний інтерфейс, усе що вам необхідно змінити, щоб змінити внутрішнє представлення продукту полягає у визначені нового типу будівника.
- Він ізолює код для конструювання і представлення. Шаблон Будівник покращує модульність інкапсулюючи спосіб у який відбувається складання і представлення складного об'єкта. Клієнтам непотрібно знати будь-що про класи, які визначають внутрішню структуру продукту; такі класи не з'являються у інтерфейсі Будівника.
Кожен ConcreteBuilder (ПевнийБудівник) містить увесь код для створення і складання певного типу продукту. Код написаний один раз, може бути повторно використаним багатьма Директорами для побудови варіантів Продукту (Product) з того ж набору частин. У попередньому прикладі RTF документів, ми могли б визначити читача іншого ніж RTF, скажемо, читача SGMLReader і використати ті ж текстові перетворювачі для генерування ASCIIText, TeXText i TextWidget перетворених від документів SGML.
- Він дає вам найпрекрасніший контроль над процесом конструювання. На відміну від створюючих шаблонів, які конструюють продукти за один раз, шаблон Будівник конструюює продукт крок за кроком під контролем директора. Тільки коли продукт закінчений, директор отримує його від будівника. Отже інтерфейс Будівника відображає процес конструювання продукту більше ніж інші створюючі шаблони. Це дає вам кращий контроль над процесом конструювання і відповідно над внутрішньою структурою результуючого продукту.
Реалізація
Типово існує абстрактний клас Будівник (Builder), який визначає операцію для кожного компоненту якого директор може створити запит на створення. Операція не робить нічого за умовчанням. Клас ConcreteBuilder (ПевнийБудівник) перевизначає операції для компонентів у яких він зацікавлений в створенні.
Ось інші питання реалізації які необхідно врахувати:
- Інтерфейс складання і конструювання. Будівники конструюють їхні продукти в покроковій манері. Тому інтерфейс класу Будівник повинен бути достатньо загальним для того, щоб дозволити конструювання продуктів для усіх типів конкретних будівників.
Ключова інженерна проблема стосується моделі для процесу конструювання і складання. Модель де результати запитів конструювання є просто доданими до продукту являється зазвичай достатньою. У прикладі з RTF документами, будівник конвертує і додає наступний символ до тексту, який він вже перетворив.
Але інколи ви можете потребувати доступ до частин сконструйованого раніше продукту. У прикладі Лабіринту який ми представили у секції Приклад Коду, інтерфейс класу MazeBuilder дозволяє вам додавати двері між існуючими кімнатами. Деревоподібні структури на подобі дерев аналізу, які побудовані з верху в низ являються іншим прикладом. У цьому випадку, будівник повернить потомкові вузли до директора, який тоді передасть їх назад до будівника для побудови батьківських вузлів.
- Чому немає абстрактного класу для продуктів? У загальному випадку, продукти створені конкретними будівниками відрізняються так сильно у їхньому представлені, що ми отримаємо мало вигоди від спільного батьківського класу різних продуктів. У прикладі RTF читача, об'єкти ASCIIText i TextWidget мало ймовірно будуть мати загальний інтерфейс, і вони такого не потребують. Через те, що клієнти зазвичай конфігурують директора за допомогою відповідного конкретного будівника, клієнт усвідомлений який конкретний потомок Будівника (Builder) використовується і може обробити його продукти відповідно.
- Порожні методи за замовчуванням у Будівнику. У С++, методи побудови навмисно оголошені як звичайні методи, а не віртуальні функції. Замість того, вони визначені як пусті методи, дозволяючи клієнтам перевизначати тільки ті операції, у яких вони зацікавлені.
Приклад Коду
Ми створимо варіант методу CreateMaze (СтворитиЛабіринт), який бере будівника класу MazeBuilder в якості аргументу.
Клас MazeBuilder визначає наступні примірники для побудови лабіринтів:
class MazeBuilder
{
public:
virtual void BuildMaze () { }
virtual void BuildRoom (int room) { }
virtual void BuildDoor (int roomFrom, int roomTo) { }
virtual Maze* GetMaze () { return 0; }
protected:
MazeBuilder();
} ;
Даний інтерфейс може створити три речі: (1) лабіринт, (2) кімнати з певним номером, і (3) двері між пронумерованими кімнатами. Операція GetMaze повертає лабіринт до клієнта. Потомки класу MazeBuilder перевизначатимуть ці операції для повернення лабіринту, який вони створили.
Усі операції створення лабіринтів класу MazeBuilder за умовчанням нічого не роблять. Вони не визначені чистими віртуальними функціями для того, щоб дозволити похідним класам перевизначати тільки ті операції, у яких вони зацікавлені. Визначаючи інтерфейс для MazeBuilder, ми можемо змінити метод CreateMaze так, щоб він брав будівник як параметр.
Maze* MazeGame :: CreateMaze (MazeBuilder& builder)
{
builder.BuildMaze();
builder.BuildRoom(1);
builder.BuildRoom(2);
builder.BuildDoor(1, 2);
return builder.GetMaze();
}
Порівняйте дану версію CreateMaze з оригіналом. Зверніть увагу на те, як будівник приховує внутрішнє представлення класу Maze (Лабіринт) — тобто класи які визначають кімнати, двері і стіни, і як ці частини складаються до завершеного фінального лабіринту. Дехто може здогадуватися, що існують класи для представлення кімнат і дверей, але немає такого натяку на стіни. Це полегшує зміну способу представлення лабіринту, оскільки ні один клієнт класу MazeBuilder, не повинен бути зміненим.
Як і інші створюючі шаблони, шаблон Будівник інкапсулює те, як об'єкти створюються, у цьому випадку через інтерфейс визначеним у MazeBuilder. Це означає, що ми можемо повторно використати MazeBuilder для побудови різних типів лабіринтів. Операція CreateComplexMaze дасть нам приклад:
Maze* MazeGame :: CreateComplexMaze (MazeBuilder& builder)
{
builder.BuildRoom(1);
// ...
builder.BuildRoom(1001);
return builder.GetMaze();
}
Зверніть увагу на те, що MazeBuilder самостійно не створює лабіринти; його головна ціль полягає у визначені інтерфейсу для створення лабіринтів. Він визначає порожню реалізацію для зручності. Потомки класу MazeBuilder фактично виконують усю роботу.
Потомок StandartMazeBuilder являється реалізацією, яка будує прості лабіринти. Він зберігає лабіринт, який він будує у змінній _currentMaze.
class StandardMazeBuilder : public MazeBuilder
{
public:
StandardMazeBuilder () ;
virtual void BuildMaze () ;
virtual void BuildRoom (int) ;
virtual void BuildDoor (int, int) ;
virtual Maze* GetMaze () ;
private:
Direction CommonWall (Room*, Room*) ;
Maze* _currentMaze ;
} ;
CommonWall являється корисною операцією, яка визначає напрямок загальної стіни між двома кімнатами.
Конструктор StandardMazeBuilder просто ініціалізовує _currentMaze.
StandardMazeBuilder :: StandardMazeBuilder ()
{
_currentMaze = 0 ;
}
BuildMaze створює примірник Maze, який інші операції складуть і в кінцевому рахунку повернуть до клієнта (за допомогою GetMaze).
void StandardMazeBuilder::BuildMaze ()
{
_currentMaze = new Maze ;
}
Maze* StandardMazeBuilder::GetMaze ()
{
return _currentMaze ;
}
Операція BuildRoom створює кімнату і будує стіни навколо неї.
void StandardMazeBuilder::BuildRoom (int n)
{
if (!_currentMaze->RoomNo(n))
{
Room* room = new Room(n) ;
_currentMaze->AddRoom(room) ;
room->SetSide(North, new Wall) ;
room->SetSide(South, new Wall) ;
room->SetSide(East, new Wall) ;
room->SetSide(West, new Wall) ;
}
}
Для побудови дверей між кімнатами, StandardMazeBuilder переглядає дві кімнати у лабіринт і визначає їхню спільну стіну:
void StandardMazeBuilder::BuildRoom (int n)
{
if (!_currentMaze->RoomNo(n))
{
Room* room = new Room(n) ;
_currentMaze->AddRoom(room) ;
room->SetSide(North, new Wall) ;
room->SetSide(South, new Wall) ;
room->SetSide(East, new Wall) ;
room->SetSide(West, new Wall) ;
}
}
Клієнти тепер можуть використати CreateMaze у поєднанні з StandardMazeBuilder для створення лабіринту:
Maze* maze ;
MazeGame game ;
StandardMazeBuilder builder ;
game.CreateMaze(builder) ;
maze = builder.GetMaze() ;
Ми можемо вкласти усі операції StandardMazeBuilder у Maze і дозволити кожному лабіринту (Maze) побудувати себе самого. Але роблячи Maze меншим, полегшує розуміння і модифікування, і StandardMazeBuilder легко відрізнити від Maze (Лабіринту). Найважливіше те, що їх відокремлення дозволяє вам варіювати будівники MazeBuilders, кожен використовує різні класи для кімнат, стін і дверей.
Більш екзотична версія MazeBuilder є CountingMazeBuilder. Цей будівник не створює лабіринти взагалі; він просто обраховує різні типи компонентів, які повинні бути створеними.
class CountingMazeBuilder : public MazeBuilder
{
public :
CountingMazeBuilder() ;
virtual void BuildMaze () ;
virtual void BuildRoom (int) ;
virtual void BuildDoor (int, int) ;
virtual void AddWall (int, Direction);
void GetCounts(int&, int&) const ;
private:
int _doors ;
int _rooms ;
} ;
Конструктор ініціалізовує лічильники і перевизначені операції MazeBuilder інкрементують їх відповідно.
CountingMazeBuilder::CountingMazeBuilder ()
{
_rooms = _doors = 0 ;
}
void CountingMazeBuilder::BuildRoom (int)
{
_rooms++ ;
}
void CountingMazeBuilder::BuildDoor (int, int)
{
_doors++ ;
}
void CountingMazeBuilder::GetCounts ( int& rooms, int& doors ) const
{
rooms = _rooms ;
doors = _doors ;
}
Ось як клієнти можуть використовувати клас CountingMazeBuilder:
int rooms, doors ;
MazeGame game ;
CountingMazeBuilder builder ;
game.CreateMaze(builder) ;
builder.GetCounts(rooms, doors) ;
cout << "Лабіринт має "
<< rooms << " кімнат і "
<< doors << " дверей" << endl ;
Відомі використання
Перетворююча програма RTF з ET++ [WGM88]. Її будівник текстових блоків використовує будівник для обробки тексту збереженого у форматі RTF.
Будівник являється загальним шаблоном у Smalltalk-80 [Par90]:
- Клас Parser у компілюючій підсистемі являється директором (Director), який бере об'єкт ProgramNodeBuilder як аргумент. Об'єкт Parser кожного разу повідомляє його об'єкт ProgramNodeBuilder коли він розпізнає синтаксичну конструкцію. Коли аналіз закінчений, він опитує будівника для отримання дерева аналізу, який він побудував і повертає його до клієнта.
- ClassBuilder являється будівником, який використовує Classes для створення потомків їх самих. У цьому випадку Class являється одночасно Директором (Director) і Продуктом (Product).
- ByteCodeStream являється будівником, який створює складні методи у вигляді масивів байтів. ByteCodeStream являється нестандартним використанням шаблону Будівника через те, що складні об'єкти, які він будує, кодуються у вигляді масивів байтів, а не як нормальні об'єкти Smalltalk. Але інтерфейс до ByteCodeStream є типовим для будівника і через це легко замінити ByteCodeStream іншим класом, який представляє програми як композитні об'єкти.
Фреймворк Service Configurator з Adaptive Communications Environment використовує будівник для конструювання компонентів сервісів мережі, які зв'язані у сервер під час виконання програми [SS94]. Компоненти описані за допомогою мови конфігурації, яка аналізується за допомогою аналізатора LALR(1). Семантичні дії аналізатора виконує операції на будівнику, який додає інформацію до компонентів сервісів. У цьому випадку, аналізатор являється Директором (Director).
Зв'язані Шаблони
Абстрактна фабрика є подібною до Будівника у тому, що вона може також конструювати складні об'єкти. Основна відмінність полягає у тому, що шаблон Будівника фокусується на конструюванні складних об'єктів покроково. Абстрактна фабрика робить акцент на фаміліях об'єктів продуктів (як складних так і простих). Будівник повертає продукт як фінальний крок, а Абстрактна Фабрика негайно повертає клієнту продукт, як тільки вона перестає бути в ньому зацікавленою.
Композитор являється тим, що будівник часто будує.