5评论

读书笔记-设计模式-可复用版-Builder 建造者模式

Mitty 2019-03-06 1.2k浏览

Builder 建造者模式是5种创建型模式中的一种,重复那句话,既然是创建型模式,他的目的就是用来“构建其它对象“的。


但Builder,通常是用于”构建“复杂的对象,并强制一步一步构建的过程,来生成复杂的对象。


概念:


将一个复杂的对象构建和它的表示分离。使得同样的构建过程,可以创建不同的表示。


结构图:



既然是创建型,就需要有,我们具体要创建的东西(Product)产品,这些产品类通常比较复杂,比如有众多的组件(拥有众多的参数),而这些组件之间的组合是多式多样的


那么,如果你想要通过构造函数去创建,你就都需要提供很多种,而且需求变更更带来的修改成本......显然是不合理的,所以产品的创建,必须应该具有灵活性


Builder类是建造者模式的核心,里面包括了构建产品的所有接口(每一个环节)。但Builder通常并不生成最终的构建结果,最终的构建我们通常是放在Director(主管或导演)中


可以说是将构建过程和构建结果再次分离。


这里举一个例子:


我有一个怪物Monster的对象(Product),他可以包含头,眼睛,嘴,耳朵,手,脚等等很多部分,但怪物的设定,可以是很随意的组合,可以像人,也可以是四不像


比如我需要一个似人的怪物,一个长着三只眼睛两条腿的,一只眼睛一只胳膊一条腿的,两个头三张嘴四只脚的......你会发现,组合是多样化的


通过建立Builder类,我们将实现构建Monster的每一个环节(添加头,脚,眼睛,腿等),上面提到过,Builder只包含怪物的每一个环节,步骤,但最终构建成什么样子,这需要放在Director中构建。


Builder只提供构建产品每一个环节的接口。


简化的代码:


public class Monster{


 public int hp;

 public int maxHp;

 ....

 public List<MonsterComponent> components = new List<MonsterComponent>();



现在我们的需求是构建这些不同风格的怪物,将构建的代码直接添加到Product中是可以的,但并不建议这样做,因为我构建部分的修改,不应该影响到Product本身。


所以,通常将构建的过程定义在一个新的类或是接口中,从而实现构建和表示分离。


另一点好处是,当需求发生了变更,可能要有更复杂的构建,我可能需要有多个类需去实现构建的接口,习惯性定义成接口,面对难以预料的需求变更,代码会更具有扩展性。


简化的代码:


public class IMonsterBuilder{


Monster create();

void AddHead(...);

void AddHand(...);

void AddEar(...);

void AddLeg(...");


.....


通过具体的Builder类(ConcreteBuilder),实现IMonsterBuilder接口,并实现相应的方法。(并不是每一个方法都需要去实现,这里就提高了更多的灵活性)


简化代码:


public class MonsterBuilder:IMonsterBuilder{

public Monster monster;

public Monster create()

return xxx;


public void AddHead(..)

 ......


public void AddHand(...)

 .......


MonsterBuilder中包含了构建一个完整的复杂的对象的每一个环节,但通常他并不包含最终构建的结果。


因为同样,不同组合构建出来的结果,不应该影响到构建的环节。


换句话说,我随意的组合产品,这些环节(接口)都是MonsterBuilder提供的,但如果写在一起(耦合),那么,我可能只是去掉了结果中,这只怪物的胳膊或是增加两条腿的修改,但导致MonsterBuilder构建的类也被编译了,甚至可能会引起错误的修改,这都是不合理的设计


所以,具体不同样式的构建我就需要放在另外一个类中去实现,这就是最终的Director了。


现在,Product,Builder,Director的结构关系,应该可以明确了。


这样,无论我怎么调整构建结果,Builder构建类和Product产品,都不会受到影响。  


所以在设计中,一定要考虑,这些接口为什么要分开,是否可以耦合在一起?


Builder可以定义为接口,也可以定义为Class,下面是书中定义成class的形式:


class MazeBuilder{

public:

 virtual void BuildMaze();

 virtual void BuildRoom(int room){}

 virtual void BuildDoor(int ,roomFrom,int roomTo){}


 virtual Maze* GetMaze() {return 0;}

protected:

 MazeBuilder();

可以看到,MazeBuilder并没有将所有的接口,定义为纯虚函数,这是为了便于派生类可以只重新定义它所感兴趣的接口,而不必像接口那样,要重新定义所有的接口。


所以,我个人也是更偏向于定义成抽象类,而不是接口的形式。


有了具体实现MazeBuilder的派生类,在Director中,就可以去一步一步的去构建复杂的对象了。


沿用上面的例子。在Director中可以定义如下方法来创建最终的复杂对象。


这里Director被定义为MazeGame


Maze* MazeGame::CreateMaze(MazeBuilder& builder){

 builder.BuildMaze();//创建Maze对象,即Product


 builder.BuildRoom(1);

 builder.BuildRoom(2);

 builder.BuildDoor(1,2);


 return builder.GetMaze();

可以发现,每一个环节都被builder隐藏了内部的实现细节。


函数接受MazeBuilder& 引用,这就意味着,我可以重用MazeBuilder来创建更多不同组合的复杂对象。


比如:


Maze* MazeGame::CreateComplexMaze(MazeBuilder& builder)

 builder.BuildRoom(1);

 //...

 builder.BuildRoom(1001);

 

 return builder.GetMaze();


再次重复,MazeBuilder,Builder并不创建最终的结果,只提供创建结果的每一个必要的环节。而且通常环节的部分,也是由派生类来实现的,这样会更具有扩展性。


public class StandardMazeBuilder:MazeBuilder{

 public:

  StandardMazeBuilde();


  virtual void BuildMaze();

  virtual void BuildRoom(int);

  virtual void BuildDoor(int,int);

  virutal Maze* GetMaze();

 

 private:

  //定义具体的产品类,StandardMazeBuilder包含了具体产品类所有的创建环节

  Maze* _currentMaze;

  


public class ClassicMazeBuilder:MazeBuilder{

  //...


public class OldSchoolMazeBuilder:MazeBuilder{

  //...


在StandardMazeBuilder中定义了_currentMaze对象,即是我们的Product,

通过BuildMaze()函数,完成_currentMaze的new初始化操作,并通过GetMaze函数,返回给调用者(Director)


最后看下时序图:




ConcreteBuilder aConcreteBuilder= new ConcreteBuilder();

Director aDirector = new Director(aConcrteBuilder);

aDirector.Construct();

//aDirector.ConstructB();

//aDirector.ConstructC();

//......

ProductXXX product = aDirector.GetResult();


创建ConcreteBuilder,具体的Builder对象,并做为参数,传给Director,Director可以利用Builder对象提供的接口,去一步一步的构建出复杂的对象(最终的结果)


如果让我说一个Builder最常见,最典型应用的地方,我会脱口而出:弹出式对话框


接触过Android开发都看到如下这样的代码:



典型的Builder建造者模式应用,之前看到有的文章说,Android这种方式省去了Director,其实还是没理解Builder概念上的意图,如果你需要创建多个复杂的对象,当然还是需要Director来负责生成多个不同需求的复杂对象。

上面的代码好在Builder接口中默认均返回Builder本身,这样我构建的时候,可以直接通过点号调用其它的接口,书写起来很方便,很灵活。

我个人也是很建议写成这种形式。

而且我们可以看到上面的Builder是一个内部类,AlertDialog.Builder,好处是可以明确Builder的来源,不需要想着应该定义成什么名字,内部类,直接就定义叫Builder就好了。

另外,如果查看源码,发现在AlertDialog.Builder中,定义的并不是AlertDialog对象,而是AlertParams,将对话框所需要的参数也定义成class的形式,create时,再将AlertParams应用到AlertDialog中,也是很不错的设计。

以前写过一篇关于建造者模式的应用
https://www.jianshu.com/p/7dec2f4f3367

这次再复习,对建造者模式有了更多的认识,本来想重新实现一下厨房设计的案例,既然上面提到过创建不同的怪物,和厨房设计的结构是一样的,就实现一下应用建造模式来创建不同种类的怪物

例子比较简单,能够表达意思即可。

public class MonsterComponent
//...

public class HeadComponent : MonsterComponent
//...

public class EyeComponent : MonsterComponent
//...

public class MouthComponent : MonsterComponent
//...

public class HandComponent : MonsterComponent
//...

public class LegComponent : MonsterComponent
//...


public class Monster
  public string name;
  public List<MonsterComponent> components = new List<MonsterComponent>();

  public Monster(string name)
  {
    this.name = name;
  }

  public void AddComponent(MonsterComponent component)
  {
    components.Add(component);
  }

  public void print()
  {
    int headcount = 0;
    int eyecount = 0;
    int mouthcount = 0;
    int handcount = 0;
    int legcount = 0;

    GetComponentCount(out headcount, out eyecount, out mouthcount, out handcount, out legcount);

    Debug.Log(string.Format("怪物名字:{0} 拥有{1} 个头 {2} 只眼睛 {3} 张嘴 {4} 只手 {5} 条腿",
name,headcount, eyecount, mouthcount, handcount, legcount));
  }

  private void GetComponentCount(out int headcount, out int eyecount, 
out int mouthcount, out int handcount, out int legcount)
  {
    headcount = eyecount = mouthcount = handcount = legcount = 0;
    for (int i = 0; i < components.Count; ++i)
    {
      MonsterComponent com = components[i];
      if (com.ToString().StartsWith("Head"))
      {
        headcount++;
      }
      if (com.ToString().StartsWith("Eye"))
      {
        eyecount++;
      }
      if (com.ToString().StartsWith("Mouth"))
      {
        mouthcount++;
      }
      if (com.ToString().StartsWith("Hand"))
      {
        handcount++;
      }
      if (com.ToString().StartsWith("Leg"))
      {
        legcount++;
      }

    }
  }

public class MonsterBaseBuilder
  public virtual Monster create() { return null; }
  public virtual MonsterBaseBuilder BuildMonster(string name) { return null; }

  public virtual MonsterBaseBuilder AddHead() { return null; }

  public virtual MonsterBaseBuilder AddEye() { return null; }

  public virtual MonsterBaseBuilder AddMouth() { return null; }

  public virtual MonsterBaseBuilder AddHand() { return null; }

  public virtual MonsterBaseBuilder AddLeg() { return null; }



public class MonsterBuilder : MonsterBaseBuilder
  private Monster monster;
  public override Monster create() { return monster; }
  public override MonsterBaseBuilder BuildMonster(string name)
  {
    monster = new Monster(name);
    return this;
  }

  public override MonsterBaseBuilder AddHead()
  {
    monster.AddComponent(new HeadComponent());
    return this;
  }

  public override MonsterBaseBuilder AddEye()
  {
    monster.AddComponent(new EyeComponent());
    return this;
  }

  public override MonsterBaseBuilder AddMouth()
  {
    monster.AddComponent(new MouthComponent());
    return this;
  }

  public override MonsterBaseBuilder AddHand()
  {
    monster.AddComponent(new HandComponent());
    return this;
  }

  public override MonsterBaseBuilder AddLeg()
  {
    monster.AddComponent(new LegComponent());
    return this;
  }


public class MosnterDirector
  private MonsterBaseBuilder builder;
  public MosnterDirector(MonsterBaseBuilder builder)
  {
    this.builder = builder;
  }
  public Monster CreateMonster_1()
  {
    return builder.BuildMonster("monster1")
    .AddHead()
    .AddEye()
    .AddMouth()
    .AddLeg()
    .AddLeg()
    .AddLeg()
    .AddLeg()
    .AddLeg()
    .create();
  }

  public Monster CreateMonster_2()
  {
    return builder.BuildMonster("monster2")
    .AddHead()
    .AddHead()
    .AddEye()
    .AddEye()
    .AddMouth()
    .AddHand()
    .AddHand()
    .AddLeg()
    .AddLeg().
    create();

  }

  public Monster CreateMonster_3()
  {
    return builder.BuildMonster("monster3")
    .AddHead()
    .AddHead()
    .AddHead()
    .AddEye()
    .AddEye()
    .AddEye()
    .AddMouth()
    .AddMouth()
    .AddMouth()
    .AddHand()
    .AddHand()
    .AddLeg()
    .AddLeg()
    .create();

  }


CreateMonster_1,CreateMonster_2,CreateMonster_3方法中一长串的代码可以进行重构优化,但这里为了体现一步一步创建的过程。

Monster:Product,产品类,我们需要创建不同的Monster
MonsterComponent: 敌人组件,基类,由各个组件(部位)派生实现

HeadComponent
EyeComponent
MouthComponent
.....
均是具体部位的派生类

MonsterBaseBuilder:Builder的基本类,派生实现,实现创建Monster的每一个环节,构建的过程。

MonsterBuilder:派生自MonsterBaseBuilder,完成每一个环节的实现,这里采用了Android中AlertDialog的方式,每个函数返回this,方便书写,灵活使用。

MosnterDirector:导向器,用于创建构建最终的对象,接受MonsterBaseBuilder对象,复用它并进行不同需求的构建。

提供了三个方法:
CreateMonster_1();
CreateMonster_2();
CreateMonster_3();

测试代码:

MonsterBuilder builder = new MonsterBuilder();
MosnterDirector monsterDirector = new MosnterDirector(builder);

Monster monster1 = monsterDirector.CreateMonster_1();
Monster monster2 = monsterDirector.CreateMonster_2();
Monster monster3 = monsterDirector.CreateMonster_3();

monster1.print();
monster2.print();
monster3.print();

输出结果:

再想一个Builder构建者的例子,插花,花束是Product,包含了N种不同的花,
通过Builder可以获取所有可用的花,并提供了组合他们的方法
Director则是通过Builder提供的接口,一步一步的构建出最后的花束

放在现实中,花束是Product,Builder就是花店,提供了各种花,Director就是你自己或是说
插花人,由你来决定要构建成什么样子。




感谢您的阅读, 如文中有误,欢迎指正,共同提高 


欢迎关注我的技术分享的微信公众号,Paddtoning帕丁顿熊,期待和您的交流




本文作者

Mitty

欢迎关注我的技术分享微信公众号:PaddingtonCoder (Paddington帕丁顿熊,很喜欢这个名字,第一次出国就是英国,很意外的机会,了解了一点英国的历史,知道了帕丁顿熊,看了帕丁顿熊的电影,来到了伦敦的帕丁顿地铁站,随处可见肥肥的鸽子总是抢镜......很有趣儿)

腾讯游戏学院公众号