Паттерн Builder (строитель)

Назначение паттерна Builder

Паттерн Builder может помочь в решении следующих задач:

  • В системе могут существовать сложные объекты, создание которых за одну операцию затруднительно или невозможно. Требуется поэтапное построение объектов с контролем результатов выполнения каждого этапа.
  • Данные должны иметь несколько представлений. Приведем классический пример. Пусть есть некоторый исходный документ в формате RTF (Rich Text Format), в общем случае содержащий текст, графические изображения и служебную информацию о форматировании (размер и тип шрифтов, отступы и др.). Если этот документ в формате RTF преобразовать в другие форматы (например, Microsoft Word или простой ASCII-текст), то полученные документы и будут представлениями исходных данных.

Описание паттерна Builder

Паттерн Builder отделяет алгоритм поэтапного конструирования сложного продукта (объекта) от его внешнего представления так, что с помощью одного и того же алгоритма можно получать разные представления этого продукта.

Поэтапное создание продукта означает его построение по частям. После того как построена последняя часть, продукт можно использовать.

Для этого паттерн Builder определяет алгоритм поэтапного создания продукта в специальном классе Director (распорядитель), а ответственность за координацию процесса сборки отдельных частей продукта возлагает на иерархию классов Builder. В этой иерархии базовый класс Builder объявляет интерфейсы для построения отдельных частей продукта, а соответствующие подклассы ConcreteBuilder их реализуют подходящим образом, например, создают или получают нужные ресурсы, сохраняют промежуточные результаты, контролируют результаты выполнения операций.

UML-диаграмма классов паттерна Builder

Класс Director содержит указатель или ссылку на Builder, который перед началом работы должен быть сконфигурирован экземпляром ConcreteBuilder, определяющим соответствующе представление. После этого Director может обрабатывать клиентские запросы на создание объекта. Получив такой запрос, с помощью имеющегося экземпляра строителя Director строит продукт по частям, а затем возвращает его пользователю.

Сказанное демонстрирует следующая диаграмма последовательностей.

UML-диаграмма последовательности паттерна Builder

Для получения разных представлений некоторых данных с помощью паттерна Builder распорядитель Director должен использовать соответствующие экземпляры ConcreteBuilder. Ранее говорилось о задаче преобразования RTF-документов в документы различных форматов. Для ее решения класс Builder объявляет интерфейсы для преобразования отдельных частей исходного документа, таких как текст, графика и управляющая информация о форматировании, а производные классы WordBuilder, AsciiBuilder и другие их реализуют с учетом особенностей того или иного формата. Так, например, конвертор AsciiBuilder должен учитывать тот факт, что простой текст не может содержать изображений и управляющей информации о форматировании, поэтому соответствующие методы будут пустыми.

По запросу клиента распорядитель Director будет последовательно вычитывать данные из RTF-документа и передавать их в выбранный ранее конвертор, например, AsciiBuilder. После того как все данные прочитаны, полученный новый документ в виде ASCII-теста можно вернуть клиенту. Следует отметить, для того чтобы заменить формат исходных данных (здесь RTF) на другой, достаточно использовать другой класс распорядителя.

Реализация паттерна Builder

Приведем реализацию паттерна Builder на примере построения армий для военной стратегии "Пунические войны". Подробное описание этой игры можно найти в разделе Порождающие паттерны.

Для того чтобы не нагромождать код лишними подробностями будем полагать, что такие рода войск как пехота, лучники и конница для обеих армий идентичны. А с целью демонстрации возможностей паттерна Builder введем новые виды боевых единиц:

  • Катапульты для армии Рима.
  • Боевые слоны для армии Карфагена.
#include <iostream>
#include <vector>

// Классы всех возможных родов войск
class Infantryman
{
  public:
    void info() { 
      cout << "Infantryman" << endl; 
    }
};

class Archer
{
  public:
    void info() { 
      cout << "Archer" << endl; 
    }
};

class Horseman
{
  public:        
    void info() { 
      cout << "Horseman" << endl; 
    }
};

class Catapult
{
  public:    
    void info() { 
      cout << "Catapult" << endl; 
    }
};

class Elephant
{
  public:    
    void info() { 
      cout << "Elephant" << endl; 
    }
};


// Класс "Армия", содержащий все типы боевых единиц
class Army 
{
  public:
    vector<Infantryman> vi; 
    vector<Archer>      va; 
    vector<Horseman>    vh;     
    vector<Catapult>    vc;     
    vector<Elephant>    ve; 
    void info() {   
      int i;
      for(i=0; i<vi.size(); ++i)  vi[i].info();
      for(i=0; i<va.size(); ++i)  va[i].info();
      for(i=0; i<vh.size(); ++i)  vh[i].info();
      for(i=0; i<vc.size(); ++i)  vc[i].info();
      for(i=0; i<ve.size(); ++i)  ve[i].info();
    }
};


// Базовый класс ArmyBuilder объявляет интерфейс для поэтапного 
// построения армии и предусматривает его реализацию по умолчанию

class ArmyBuilder
{
  protected: 
    Army* p;
  public:    
    ArmyBuilder(): p(0) {}
    virtual ~ArmyBuilder() {}
    virtual void createArmy() {}
    virtual void buildInfantryman() {}
    virtual void buildArcher() {}
    virtual void buildHorseman() {}
    virtual void buildCatapult() {}
    virtual void buildElephant() {}    
    virtual Army* getArmy() { return p; }    
};


// Римская армия имеет все типы боевых единиц кроме боевых слонов
class RomanArmyBuilder: public ArmyBuilder
{    
  public:    
    void createArmy() { p = new Army; }
    void buildInfantryman() { p->vi.push_back( Infantryman()); }
    void buildArcher() { p->va.push_back( Archer()); }
    void buildHorseman() { p->vh.push_back( Horseman()); }    
    void buildCatapult() { p->vc.push_back( Catapult()); }
};


// Армия Карфагена имеет все типы боевых единиц кроме катапульт
class CarthaginianArmyBuilder: public ArmyBuilder
{    
  public:    
    void createArmy() { p = new Army; }
    void buildInfantryman() { p->vi.push_back( Infantryman()); }
    void buildArcher() { p->va.push_back( Archer()); }
    void buildHorseman() { p->vh.push_back( Horseman()); }
    void buildElephant() { p->ve.push_back( Elephant()); }    
};


// Класс-распорядитель, поэтапно создающий армию той или иной стороны.
// Именно здесь определен алгоритм построения армии.
class Director
{    
  public:    
    Army* createArmy( ArmyBuilder & builder ) 
    { 
        builder.createArmy();
        builder.buildInfantryman();
        builder.buildArcher();
        builder.buildHorseman();
        builder.buildCatapult();
        builder.buildElephant();
        return( builder.getArmy());
    }
};


int main()
{   
    Director dir;
    RomanArmyBuilder ra_builder;
    CarthaginianArmyBuilder ca_builder;

    Army * ra = dir.createArmy( ra_builder);
    Army * ca = dir.createArmy( ca_builder);
    cout << "Roman army:" << endl;
    ra->info();
    cout << "\nCarthaginian army:" << endl;
    ca->info();
    // ...

    return 0;
}

Вывод программы будет следующим:

Roman army:
Infantryman
Archer
Horseman
Catapult

Carthaginian army:
Infantryman
Archer
Horseman
Elephant

Очень часто базовый класс строителя (в коде выше это ArmyBuilder) не только объявляет интерфейс для построения частей продукта, но и определяет ничего неделающую реализацию по умолчанию. Тогда соответствующие подклассы (RomanArmyBuilder, CarthaginianArmyBuilder) переопределяют только те методы, которые участвуют в построении текущего объекта. Так класс RomanArmyBuilder не определяет метод buildElephant, поэтому Римская армия не может иметь слонов. А в классе CarthaginianArmyBuilder не определен buildCatapult(), поэтому армия Карфагена не может иметь катапульты.

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

Результаты применения паттерна Builder

Достоинства паттерна Builder

  • Возможность контролировать процесс создания сложного продукта.
  • Возможность получения разных представлений некоторых данных.

Недостатки паттерна Builder

  • ConcreteBuilder и создаваемый им продукт жестко связаны между собой, поэтому при внесеннии изменений в класс продукта скорее всего придется соотвествующим образом изменять и класс ConcreteBuilder.

results matching ""

    No results matching ""