【设计模式】Builder(生成器)模式
概念
Builder(生成器)模式的目的是:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以有不同的细节表示。
个人理解
1. 适用范围
- 创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式;
- 构造过程必须允许被构造的对象有不同的表示。
一个通俗的例子是,我要创建一个游戏人形npc,它的 组成部分 是 固定 的:头+身+左臂+右臂+左腿+右腿,而至于它的头是人的头还是动物的头这些创建的细节,是存在区别的。那么这个固定的组成部分就是可以复用的,我们只需要为不同的细节创建不同的生成器即可。
同理,如果一个对象,你需要对其各个部分的 创建顺序 做一个 固定 的流程,那么这个固定的流程也是可以复用的。例如,做一道简单素+肉炒菜,我的操作步骤可以固定为:放油+放肉类+放素菜+放调料翻炒出锅,注意这个顺序是固定的,而你要加橄榄油还是花生油、鸡肉还是猪肉、青椒还是番茄这些细节就是区别,我们就可以把这个流程复用到一个抽象的对象中,让具体的类来完成细节的装配 。
这两种固定的可复用的部分,把它写入到新建的一个对象,称为 导向器(Director) ,于是客户最终需要创建的对象(Product)实际上是在导向器的控制下一步步的完成创建的。而创建各部分的细节(具体生成器 Concrete Builder),继承于一个抽象的对象—— 生成器(Builder) 。
适用范围中的第二点表示,创建过程中也是可以有相同的细节的。我们只需要把相同的部分放在那个抽象的生成器中即可。
2. 代码实现(C++)
2.1 思路
Product 简化为一个链表,每个节点存放一个常量字符串。创建一个 product 就是为这个链表按 固定流程 添加 固定节点 的过程。而各个节点的具体字符串就是上面提到的 细节 部分。
Product 结构如下,为方便 add 操作保存链表尾部的指针 tail 。
代码如下:
typedef struct ListNode
{
const char* str;
ListNode* next;
ListNode(const char* s)
{
str = s;
next = nullptr;
}
}ListNode;
class Product
{
public:
Product()
{
head = new ListNode("head");
tail = head;
}
void add(const char* s)
{
ListNode* p = new ListNode(s);
tail->next = p;
tail = p;
}
ListNode* GetList()
{
return head;
}
private:
ListNode* head;
ListNode* tail;
};
2.2 类图即代码
分为 Product 、Director、Builder 三个部分,其中 Builder 下有两个子类 ConcreteBuilderA、ConcreteBuilderB 用于区分需要不同细节的不同生成器。类图如下:
2.2.1 抽象生成器
主要为具体生成器提供接口,其中 BuildPartA 和 BuildPartB 都是虚函数,但不能是纯虚函数,否则 Director 中的 Construct 会提示抽象类无法实例化。
代码如下:
class Builder
{
public:
virtual void BuildPartA() {};
virtual void BuildPartB() {};
Product& GetResult()
//此处必须返回引用类型,否则无法访问到原始product
{
return product;
}
private:
Product product;
};
2.2.2 具体生成器1
2 类似不列出。
代码如下:
class ConcreteBuilder1 : public Builder
{
public:
void BuildPartA()
{
GetResult().add("Concrete Part A 1");
}
void BuildPartB()
{
GetResult().add("Concrete Part B 1");
}
};
2.2.3 导向器
导向器 Director 中的具体建造函数 Construct 即上面提到的 固定流程。
- 添加 Part A 字符串节点。
- 添加 Part B 字符串节点。
代码如下:
参数为 Builder& ,传入需要的具体生成器以构建想要的细节。
class Director
{
public:
void Construct(Builder& builder)
//此处必须引用传递,否则使用的是基类的BuildPartA和BuildPartB,相当于啥事没做。
{
builder.BuildPartA();
builder.BuildPartB();
}
};
2.3 运用
要使用生成器模式,我们只需要实例一个具体的生成器类,将它传入导向器的 Construct 函数,于是导向器按固定流程并使用具体生成器的细节构造出我们想要的product。
可以看出,在使用的过程中,实际上是不清楚product的具体表示的,我并不需要知道它是不是一个链表,直接调用具体生成器就可以自动的创建。
代码如下:
int main()
{
ConcreteBuilder1 builder1;
ConcreteBuilder2 builder2; //未使用到
Director director;
director.Construct(builder1);
Product product = builder1.GetResult();
ListNode* p = product.GetList();
cout << "start -> ";
while (p->next != NULL)
{
p = p->next;
cout << p->str << " -> ";
}
cout << "end" << endl;
return 0;
}
运行结果如下:
2.4 需要注意的点
(这部分主打的就是记录一个C++菜鸟犯的错误)
- Construct 函数中传入的参数必须加上 & ,即引用型的参数 Builder& 。由于 C++ 默认函数参数是传值,这里如果不加 & ,即使我们新建的是 ConcreteBuilder1 这样的子类,传入到函数中,实际上是用这个子类新构建了一个 Builder 类型的局部对象,于是后续调用 BuildPartA 这些函数是 调用的局部对象的那个虚函数 !!然后就会得到 Constrict 什么也没做的结果。
- GetResult 函数的返回值必须加上 & ,即引用型,作用是返回的最原始的 product 对象。实际上跟第一点是一个问题,这里会 存在两份 product 对象 ,一份是父类的,一份是子类,我们 add 的时候只会在子类的那一份里更改 tail 指针,当用 GetResult 时它返回的又是父类中的那一份 product ,此时父类的那一份的 tail 指针是不会更改的,于是就会出现一直在头节点后面添加节点的现象。加上&后,我们直接获得子类中更改过 tail 的那个 product ,就可以按所想的那样一直延长链表的同时还能存储链表尾部节点了。