© 2025
Александр Легалов
Содержание
Строитель (Builder)
Строитель отделяет конструирование сложного объекта от его представления. В результате одного и того же процесса конструирования могут получаться разные представления. Он применяется, когда:
- алгоритм создания сложного объекта не должен зависеть от того, из каких частей состоит объект и как они стыкуются между собой;
- процесс конструирования должен обеспечивать различные представления конструируемого объекта.
Структура паттерна "Строитель"
Объектно-ориентированная реализация Строителя
Продолжая примеры с животными, попытаемся "построить"> ферму, в которую можно дополнять различных животных и, в принципе, в любом количестве. Этим занимается некоторый "фермер", который в данном случае занимается разведением уток и приобретением собак, для охраны своего хозяйства. Владелец игрушек может также играть в ферму, выполняя соответствующие приобретения. Ограничимся одной уткой и одной собакой, так как вопрос наполнения для паттерна непринципиален. При этом даже не нужно их создавать, так как достаточно в Строителях только информировать о приобретении домашних животных или игрушек.
Будем хранить протокол покупок в массиве, имитирующем некоторую "Амбарную книгу":
// Состав животных на ферме или в коллекции
class Animals{
public:
std::vector<std::string> parts_;
void ListParts()const{
std::cout << "Animals:\n";
for (size_t i=0;i<parts_.size();i++){
if(parts_[i]== parts_.back()){
std::cout << " " << parts_[i];
}else{
std::cout << " " << parts_[i] << ",\n";
}
}
std::cout << "\n\n";
}
};
Интерфейс строителя и конкретные его реализации, обеспечивающие заполнение различных амбарных книг могут выглядеть следующим образом:
// Интерфейс Строителя объявляет методы для приобретения животных.
class Builder{
public:
virtual ~Builder(){}
virtual void BuyDuck() const =0;
virtual void BuyDog() const =0;
};
// Классы Конкретного Строителя следуют интерфейсу предоставляют конкретные
// шаги построения. Программа может иметь нескольковариантов Строителей,
// реализованных по-разному.
class DomesticFarmBuilder : public Builder{
private:
Animals* farm;
// Новый экземпляр строителя должен содержать пустой объект продукта,
// который используется в дальнейшей сборке.
public:
DomesticFarmBuilder(){
this->Reset();
}
~DomesticFarmBuilder(){
delete farm;
}
void Reset(){
this->farm = new Animals();
}
// Все этапы производства работают с одним и тем же экземпляром продукта.
void BuyDuck()const override{
// Здесь можно еще и создать купленную утку
this->farm->parts_.push_back("We have bought the Domestic Duck");
}
void BuyDog()const override{
// Здесь можно еще и создать купленную собаку
this->farm->parts_.push_back("We have bought the Domestic Dog");
}
// Конкретные Строители должны предоставить свои собственные методы
// получения результатов. Это связано с тем, что различные типы строителей
// могут создавать совершенно разные продукты с разными интерфейсами.
Animals* GetProduct() {
Animals* result = this->farm;
this->Reset();
return result;
}
};
class ToyFarmBuilder : public Builder{
private:
Animals* farm;
// Новый экземпляр строителя должен содержать пустой объект продукта,
// который используется в дальнейшей сборке.
public:
ToyFarmBuilder(){
this->Reset();
}
~ToyFarmBuilder(){
delete farm;
}
void Reset(){
this->farm = new Animals();
}
// Все этапы производства работают с одним и тем же экземпляром продукта.
void BuyDuck()const override {
// Здесь можно еще и создать игрушечную купленную утку
this->farm->parts_.push_back("We have bought the Toy Duck");
}
void BuyDog()const override{
// Здесь можно еще и создать купленную игрушечную собаку
this->farm->parts_.push_back("We have bought the Toy Dog");
}
// Конкретные Строители должны предоставить свои собственные методы
// получения результатов. Это связано с тем, что различные типы строителей
// могут создавать совершенно разные продукты с разными интерфейсами.
Animals* GetProduct() {
Animals* result = this->farm;
this->Reset();
return result;
}
};
Следует отметить, что в общем случае строители могут быть совершенно различными.
Управляющий фермой может иметь различные варианты строительства. Он может и отсутствовать. Тогда шаги по построению различных конструкций с использованием различных строителей по одной и той же схеме можно выполнять напрямую (что показано в демонстрационном примере).
class Director{
private:
Builder* builder;
// Директор работает с любым экземпляром строителя, который передаётся ему
// клиентским кодом. Таким образом, клиентский код может изменить конечный
// тип вновь собираемого продукта.
public:
void set_builder(Builder* builder){
this->builder=builder;
}
// Директор может строить несколько вариаций продукта, используя одинаковые
// шаги построения.
void BuildMinimalViableProduct(){
this->builder->BuyDuck();
}
void BuildFullFeaturedProduct(){
this->builder->BuyDuck();
this->builder->BuyDog();
}
};
Строитель с позиций процедурного подхода
В Строители используется только одна альтернатива, определяющая варианты субпродуктов, из которых формируется составная конструкция. Количество различных субпродуктов, определяет интерфей с Строителя, вырождающийся в набор полиморфных функций. В продолжение работы с животными выделим пару таких функций, обеспечивающих добавление уток и собак (рисунок~\ref{ppp-builder}). В данной ситуации обработчики получают специализированный строитель также в виде перечислимого типа. Обычные функции--конструкторы используют конкретных Cтроителей для построения из отдельных частей нужных конфигураций. Они также могут получать дополнительные параметры, используемые для взаимодействия с окружающей средой.
Графическое представление ПП аналога Строителя
Процедурно-параметрическая модель Строителя
Учитывая, что это, все-таки, C а не C++, сделаем замену библиотечного вектора на более простую структуру для хранения строк символов:
typedef struct Animals {
int animal_size;
char animalsList[10][128];
} Animals;
void ListParts(Animals* a) {
printf("Animal list:\n");
for (int i=0; i< a->animal_size; i++){
if(i == a->animal_size - 1) {
printf(" %s", a->animalsList[i]);
} else {
printf(" %s,\n", a->animalsList[i]);
}
}
printf("\n\n");
}
В качестве Строителя, предоставляющего в ОО подходе только интерфейс, используется эволюционно расширяемый перечислимый тип на основе ПП обобщения. Каждая из специализаций обеспечивает запуск своего обработчика, сформированного от обобщенных функций построения утки или собаки.
typedef struct Builder{}<> Builder;
void BuyDuck<Builder* b>() = 0;
void BuyDog<Builder* b>() = 0;
typedef struct DomesticFarmBuilder {Animals* animals;} DomesticFarmBuilder;
Builder + <DomesticFarmBuilder;>;
void BuyDuck<Builder.DomesticFarmBuilder* b>() {
Animals* a = b->@animals;
strcpy(a->animalsList[a->animal_size++], "We have bought the Domestic Duck");
}
void BuyDog<Builder.DomesticFarmBuilder* b>() {
Animals* a = b->@animals;
strcpy(a->animalsList[a->animal_size++], "We have bought the Domestic Dog");
}
// Новый экземпляр строителя должен содержать пустой объект продукта,
// который используется в дальнейшей сборке.
void ResetDomestic(DomesticFarmBuilder* b){
b->animals= malloc(sizeof(Animals));
b->animals->animal_size = 0;
}
void DomesticFarmBuilderInit(DomesticFarmBuilder* b){
ResetDomestic(b);
}
void DomesticFarmBuilderDelete(DomesticFarmBuilder* b){
free(b->animals);
}
Animals* GetDomesticResult(DomesticFarmBuilder* b) {
Animals* result= b->animals;
ResetDomestic(b);
return result;
}
typedef struct ToyFarmBuilder {Animals* animals;} ToyFarmBuilder;
Builder + <ToyFarmBuilder;>;
void BuyDuck<Builder.ToyFarmBuilder* b>() {
Animals* a = b->@animals;
strcpy(a->animalsList[a->animal_size++], "We have bought the Toy Duck");
}
void BuyDog<Builder.ToyFarmBuilder* b>() {
Animals* a = b->@animals;
strcpy(a->animalsList[a->animal_size++], "We have bought the Toy Dog");
}
void ResetToy(ToyFarmBuilder* b){
b->animals= malloc(sizeof(Animals));
b->animals->animal_size = 0;
}
void ToyFarmBuilderInit(ToyFarmBuilder* b){
ResetToy(b);
}
void ToyFarmBuilderDelete(ToyFarmBuilder* b){
free(b->animals);
}
Animals* GetToyResult(ToyFarmBuilder* b) {
Animals* result= b->animals;
ResetToy(b);
return result;
}
Конкретные строители также могут обрабатываться дополнительными функциями, в которые они передаются как обычные параметры. В частности, таким образом можно получать дополнительную информацию, связанную с результатами построения окончательных конфигураций. В частности, этими функциями может пользоваться директор, формирующий различные композиции из базовый продуктов:
// Директор отвечает за выполнение шагов построения в определённой последовательности.
typedef struct Director{
Builder* builder;
} Director;
// Директор работает с любым экземпляром строителя, который передаётся ему
// клиентским кодом. Таким образом, клиентский код может изменить конечный
// тип вновь собираемого продукта.
void SetBuilder(Director* d, Builder* builder){
d->builder = builder;
}
// Директор может строить несколько вариаций продукта, используя одинаковые
// шаги построения.
void BuildMinimalViableProduct(Director* d){
BuyDuck<d->builder>();
}
void BuildFullFeaturedProduct(Director* d){
BuyDuck<d->builder>();
BuyDog<d->builder>();
}
Строитель и ППП
Моделирование Строителя с использованием ПП подхода также позволяет практически полностью повторить ОО решение. Вместе с тем появляется допонительная гибкость, если необходимо расширить номенклатуру альтернативных строительных блоков, используемых для построения более разнообразных комбинаций. Как и в случае предыдущих паттернов это обуславливается использованием внешних обработчиков специализаций, которые, в отличие от интерфейсов, добавляются без изменения ранее написанного кода.
Содержание
|