引言
假设在游戏中建一个房子,房子可能是各式各样的,有茅草屋、砖瓦房、别墅等等
构建房子都是有固定的步骤:打地基、铺地板、墙壁、窗户、房顶、装修等等。
但是,对于房子类别,每个步骤做的事情又不一样,如做墙壁这一个步骤中,茅草屋和砖瓦房做的事情各不相同。
class House{public:void Init() {this->BuildPart1();for(int i=0; i<4; ++i) {this->BuildPart2();}bool flag = this->BuildPart3();if(flag) {this->BUildPart4();}this->BuildPart5();}virtual ~House() {}protected:virtual void BuildPart1() = 0; //构建的第一步,可能是构建地基virtual void BuildPart2() = 0; //第二步,可能是构建地板virtual bool BuildPart3() = 0; //第三步,可能是构建墙virtual void BuildPart4() = 0;virtual void BuildPart5() = 0;//对于砖瓦房、茅草屋而言,每一步又要做不同的动作//所以都写成了虚函数,在具体房子种类中实现具体的内容};//在具体的房子种类中,实现具体的动作class StoneHouse: public House{protected:virtual void BuildPart1() {}virtual void BuildPart2() {}virtual bool BuildPart3() {}virtual void BuildPart4() {}virtual void BuildPart5() {}};int main() {House* pHouse = new StoneHouse();pHouse->Init();delete pHouse;return 0;}
思考:Init()可不可以写成构造函数
思考:既然是构建,Init()的内容可以放到构造函数中吗?
不能。在C++中,在构造函数中调用虚函数实际上是静态绑定,并不是动态绑定
- 简单来说,在构造函数中调用虚函数,它不会调用子类的函数,实际调用的函数是父类的函数(即
House::BuildPart1),但此函数是一个纯虚函数,最终是会报错 - 【为什么在构造函数调用虚函数是静态绑定?】子类的构造函数是先调用父类的构造函数,如果此处是动态绑定,那么父类的构造函数会调用子类的override函数,会造成一个后果:子类还未初始化,其函数就被调用。这违背对象的基本伦理:你得先生下来,你才能发生行使行为。
- 但其他语言(如Java、C#)并不是这样的,在构造函数中调用虚函数是动态绑定的
迭代一:拆分
如果House足够复杂,除了构造的步骤之外(BuildPart1、BuildPart2等),还有很多其他的方法、其他字段。
为了不让其他方法、其他字段和步骤搅在一起,可以做进一步的拆分。
马丁福勒:不能有太肥的类,一个类的行为不能有太多。
一个类的构建尽然如此复杂,我们可以把构建过程提取出来,提成一个单独的类。即将House类一步一步拆分,一个类专门做构建,一个类专门做状态行为
//将House拆分//1. 一个类专门做状态和行为class House{//....};//2. 一个类专门做构建class HouseBuilder {public:void Init(){this->BuildPart1();for (int i = 0; i < 4; i++){this->BuildPart2();}bool flag=this->BuildPart3();if(flag){this->BuildPart4();}this->BuildPart5();return this->GetResult();}//得到构建结果House* GetResult(){return pHouse;}virtual ~HouseBuilder(){}protected:House* pHouse; //要使得HouseBuilder很方便的使用到House里的成员,用friend也行virtual void BuildPart1()=0;virtual void BuildPart2()=0;virtual void BuildPart3()=0;virtual void BuildPart4()=0;virtual void BuildPart5()=0;};class StoneHouse: public House{};class StoneHouseBuilder: public HouseBuilder{protected:virtual void BuildPart1(){//pHouse->Part1 = ...;}virtual void BuildPart2(){}virtual void BuildPart3(){}virtual void BuildPart4(){}virtual void BuildPart5(){}};int main() {StoneHouseBuilder pBuilder;pBuilder->Init();House* pHouse = pBuilder->GetResult();reutrn 0;}
迭代二:将Init分离
Init很像模板方法,并且Init基本是不变得。所以我们可以将House::Init拆分出来。
class House{//....};class HouseBuilder {public:House* GetResult(){return pHouse;}virtual ~HouseBuilder(){}protected:House* pHouse;virtual void BuildPart1()=0;virtual void BuildPart2()=0;virtual void BuildPart3()=0;virtual void BuildPart4()=0;virtual void BuildPart5()=0;};//把House::Init(构建的大致步骤)拆成一个类//这个类永远都是这个样子了//以后想重写的时候,重写HouseBuilder即可class HouseDirector{public:HouseBuilder* pHouseBuilder; //存一个HouseBuilder指针HouseDirector(HouseBuilder* pHouseBuilder){this->pHouseBuilder=pHouseBuilder;}//把原来的House::Init改成ConstructHouse* Construct(){//把this改成HouseBuilderpHouseBuilder->BuildPart1();for (int i = 0; i < 4; i++){pHouseBuilder->BuildPart2();}bool flag=pHouseBuilder->BuildPart3();if(flag){pHouseBuilder->BuildPart4();}pHouseBuilder->BuildPart5();return pHouseBuilder->GetResult();}};class StoneHouse: public House{};class StoneHouseBuilder: public HouseBuilder{protected:virtual void BuildPart1(){//pHouse->Part1 = ...;}virtual void BuildPart2(){}virtual void BuildPart3(){}virtual void BuildPart4(){}virtual void BuildPart5(){}};int main() {HouseDirector director(new StoneHouseBuilder);House* pHouse = director.Construct();return 0;}
这是一个复杂的版本,不一定要实现到这个版本,太复杂了。
迭代到这个版本,主要有以下考虑:
- 将对象的表示(House)和构建(HouseBuilder)相分离(即迭代一)
- 使同样的构建过程(HouseDirector::Construct),可以创建出不同的表现(可以构建出石头房、砖瓦房等),即迭代二
简单的原则:类复杂就拆拆拆,类简单就合并合并合并
- 拆按照行为来拆,单一原则来拆,把稳定的东西拆出来,把变化的东西拆出来
- 如果有东西不稳定怎么办,用虚函数,继续划分子类。但是虚函数要求参数稳定,返回值稳定
- 如果参数和返回值也变,怎么办呢?参数和返回值继续用子类划分。如果你能在变化中找到稳定,你就可以用设计模式了;如果你在变化中找不出稳定的部分,那对不起,没有哪个设计模式可以满足你
动机
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象各个部分面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?
模式定义
将一个复杂对象的构建与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。——《设计模式》GoF
结构

要点总结
要点一:Builder模式主要用于“分步骤构建一个复杂的对象”。在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化
要点二:变化点在哪里,封装哪里——Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法”的需求变动
要点三:在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs C#)
- C++中的构造函数不能直接调用虚函数
