引言
假设在游戏中建一个房子,房子可能是各式各样的,有茅草屋、砖瓦房、别墅等等
构建房子都是有固定的步骤:打地基、铺地板、墙壁、窗户、房顶、装修等等。
但是,对于房子类别,每个步骤做的事情又不一样,如做墙壁这一个步骤中,茅草屋和砖瓦房做的事情各不相同。
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改成Construct
House* Construct(){
//把this改成HouseBuilder
pHouseBuilder->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++中的构造函数不能直接调用虚函数