引言

假设在游戏中建一个房子,房子可能是各式各样的,有茅草屋、砖瓦房、别墅等等

构建房子都是有固定的步骤:打地基、铺地板、墙壁、窗户、房顶、装修等等。
但是,对于房子类别,每个步骤做的事情又不一样,如做墙壁这一个步骤中,茅草屋和砖瓦房做的事情各不相同。

  1. class House{
  2. public:
  3. void Init() {
  4. this->BuildPart1();
  5. for(int i=0; i<4; ++i) {
  6. this->BuildPart2();
  7. }
  8. bool flag = this->BuildPart3();
  9. if(flag) {
  10. this->BUildPart4();
  11. }
  12. this->BuildPart5();
  13. }
  14. virtual ~House() {}
  15. protected:
  16. virtual void BuildPart1() = 0; //构建的第一步,可能是构建地基
  17. virtual void BuildPart2() = 0; //第二步,可能是构建地板
  18. virtual bool BuildPart3() = 0; //第三步,可能是构建墙
  19. virtual void BuildPart4() = 0;
  20. virtual void BuildPart5() = 0;
  21. //对于砖瓦房、茅草屋而言,每一步又要做不同的动作
  22. //所以都写成了虚函数,在具体房子种类中实现具体的内容
  23. };
  24. //在具体的房子种类中,实现具体的动作
  25. class StoneHouse: public House{
  26. protected:
  27. virtual void BuildPart1() {
  28. }
  29. virtual void BuildPart2() {
  30. }
  31. virtual bool BuildPart3() {
  32. }
  33. virtual void BuildPart4() {
  34. }
  35. virtual void BuildPart5() {
  36. }
  37. };
  38. int main() {
  39. House* pHouse = new StoneHouse();
  40. pHouse->Init();
  41. delete pHouse;
  42. return 0;
  43. }

思考:Init()可不可以写成构造函数

思考:既然是构建,Init()的内容可以放到构造函数中吗?

不能。在C++中,在构造函数中调用虚函数实际上是静态绑定,并不是动态绑定

  1. 简单来说,在构造函数中调用虚函数,它不会调用子类的函数,实际调用的函数是父类的函数(即House::BuildPart1),但此函数是一个纯虚函数,最终是会报错
  2. 【为什么在构造函数调用虚函数是静态绑定?】子类的构造函数是先调用父类的构造函数,如果此处是动态绑定,那么父类的构造函数会调用子类的override函数,会造成一个后果:子类还未初始化,其函数就被调用。这违背对象的基本伦理:你得先生下来,你才能发生行使行为。
  3. 但其他语言(如Java、C#)并不是这样的,在构造函数中调用虚函数是动态绑定的

    迭代一:拆分

    如果House足够复杂,除了构造的步骤之外(BuildPart1BuildPart2等),还有很多其他的方法、其他字段。
    为了不让其他方法、其他字段和步骤搅在一起,可以做进一步的拆分。

马丁福勒:不能有太肥的类,一个类的行为不能有太多。

一个类的构建尽然如此复杂,我们可以把构建过程提取出来,提成一个单独的类。即将House类一步一步拆分,一个类专门做构建,一个类专门做状态行为

  1. //将House拆分
  2. //1. 一个类专门做状态和行为
  3. class House{
  4. //....
  5. };
  6. //2. 一个类专门做构建
  7. class HouseBuilder {
  8. public:
  9. void Init(){
  10. this->BuildPart1();
  11. for (int i = 0; i < 4; i++){
  12. this->BuildPart2();
  13. }
  14. bool flag=this->BuildPart3();
  15. if(flag){
  16. this->BuildPart4();
  17. }
  18. this->BuildPart5();
  19. return this->GetResult();
  20. }
  21. //得到构建结果
  22. House* GetResult(){
  23. return pHouse;
  24. }
  25. virtual ~HouseBuilder(){}
  26. protected:
  27. House* pHouse; //要使得HouseBuilder很方便的使用到House里的成员,用friend也行
  28. virtual void BuildPart1()=0;
  29. virtual void BuildPart2()=0;
  30. virtual void BuildPart3()=0;
  31. virtual void BuildPart4()=0;
  32. virtual void BuildPart5()=0;
  33. };
  34. class StoneHouse: public House{
  35. };
  36. class StoneHouseBuilder: public HouseBuilder{
  37. protected:
  38. virtual void BuildPart1(){
  39. //pHouse->Part1 = ...;
  40. }
  41. virtual void BuildPart2(){
  42. }
  43. virtual void BuildPart3(){
  44. }
  45. virtual void BuildPart4(){
  46. }
  47. virtual void BuildPart5(){
  48. }
  49. };
  50. int main() {
  51. StoneHouseBuilder pBuilder;
  52. pBuilder->Init();
  53. House* pHouse = pBuilder->GetResult();
  54. reutrn 0;
  55. }

迭代二:将Init分离

Init很像模板方法,并且Init基本是不变得。所以我们可以将House::Init拆分出来。

  1. class House{
  2. //....
  3. };
  4. class HouseBuilder {
  5. public:
  6. House* GetResult(){
  7. return pHouse;
  8. }
  9. virtual ~HouseBuilder(){}
  10. protected:
  11. House* pHouse;
  12. virtual void BuildPart1()=0;
  13. virtual void BuildPart2()=0;
  14. virtual void BuildPart3()=0;
  15. virtual void BuildPart4()=0;
  16. virtual void BuildPart5()=0;
  17. };
  18. //把House::Init(构建的大致步骤)拆成一个类
  19. //这个类永远都是这个样子了
  20. //以后想重写的时候,重写HouseBuilder即可
  21. class HouseDirector{
  22. public:
  23. HouseBuilder* pHouseBuilder; //存一个HouseBuilder指针
  24. HouseDirector(HouseBuilder* pHouseBuilder){
  25. this->pHouseBuilder=pHouseBuilder;
  26. }
  27. //把原来的House::Init改成Construct
  28. House* Construct(){
  29. //把this改成HouseBuilder
  30. pHouseBuilder->BuildPart1();
  31. for (int i = 0; i < 4; i++){
  32. pHouseBuilder->BuildPart2();
  33. }
  34. bool flag=pHouseBuilder->BuildPart3();
  35. if(flag){
  36. pHouseBuilder->BuildPart4();
  37. }
  38. pHouseBuilder->BuildPart5();
  39. return pHouseBuilder->GetResult();
  40. }
  41. };
  42. class StoneHouse: public House{
  43. };
  44. class StoneHouseBuilder: public HouseBuilder{
  45. protected:
  46. virtual void BuildPart1(){
  47. //pHouse->Part1 = ...;
  48. }
  49. virtual void BuildPart2(){
  50. }
  51. virtual void BuildPart3(){
  52. }
  53. virtual void BuildPart4(){
  54. }
  55. virtual void BuildPart5(){
  56. }
  57. };
  58. int main() {
  59. HouseDirector director(new StoneHouseBuilder);
  60. House* pHouse = director.Construct();
  61. return 0;
  62. }

这是一个复杂的版本,不一定要实现到这个版本,太复杂了。
迭代到这个版本,主要有以下考虑:

  1. 将对象的表示(House)和构建(HouseBuilder)相分离(即迭代一)
  2. 使同样的构建过程(HouseDirector::Construct),可以创建出不同的表现(可以构建出石头房、砖瓦房等),即迭代二

简单的原则:类复杂就拆拆拆,类简单就合并合并合并

  1. 拆按照行为来拆,单一原则来拆,把稳定的东西拆出来,把变化的东西拆出来
  2. 如果有东西不稳定怎么办,用虚函数,继续划分子类。但是虚函数要求参数稳定,返回值稳定
  3. 如果参数和返回值也变,怎么办呢?参数和返回值继续用子类划分。如果你能在变化中找到稳定,你就可以用设计模式了;如果你在变化中找不出稳定的部分,那对不起,没有哪个设计模式可以满足你

    动机

    在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象各个部分面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?

模式定义

将一个复杂对象的构建与其表示相分离,使得同样的构建过程(稳定)可以创建不同的表示(变化)。——《设计模式》GoF

结构

image.png

要点总结

要点一:Builder模式主要用于“分步骤构建一个复杂的对象”。在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化

要点二:变化点在哪里,封装哪里——Builder模式主要在于应对“复杂对象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建算法”的需求变动

要点三:在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs C#)

  1. C++中的构造函数不能直接调用虚函数