好菜每回味不同——建造者模式

继上一篇“牛市股票还会亏钱?—外观模式”后,本文继续讲解《大话设计模式》第13章“好菜每回味不同——建造者模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 炒面没放盐

时间:4月9日22点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“小菜,讲了半天,肚子饿得厉害,走,去吃夜宵去。”大鸟摸着肚子说道。

“你请客?!”

“我教了你这么多,你也不打算报答一下,还要我请客?搞没搞错。”

“啊,说得也是,这样吧,我请客,你埋单,嘻嘻!”小菜傻笑道,“我身上没带钱。”

“你这个菜穷酸,行了,我来埋单吧。”大鸟等不及了,拿起外套就往外走。

“等等我,我把电脑关一下……”

时间:4月9日 22:30 地点:小区外大排档 人物:小菜、大鸟

小区门口大排档前。

“老板,来两份炒饭。”大鸟对大排档的老板说。

“大鸟太小气,就请吃炒饭呀,”小菜埋怨道,“我要吃炒面。”

“再说废话,你请客了哦!”大鸟瞪了小菜一眼,接着对老板说,“那就一份炒饭一份炒面吧。”

十分钟后。

“这炒饭炒得什么玩意儿,味道不够,鸡蛋那么少,估计只放了半个。”大鸟吃得很不爽,抱怨道。

“炒面好好吃哦,真香。”小菜故意嘟囔着,把面吸得嗦嗪直响。

“让我尝尝,”大鸟强拉过小菜的盘子,吃了一口,“好像味道是不错,你小子运气好。老板,再给我来盘炒面!”

五分钟后。

“炒面来一了一,客官,请慢用。”老板仿佛古时的小二一般来了一句。

“啊,老板,这炒面没放盐……”大鸟叫道。

……

时间:4月9日23:15 地点:回小区的路上 人物:小菜、大鸟

在回去的路上,大鸟感概道:“小菜,你知道为什么麦当劳、肯德基这些不过百年的洋快餐能在有千年饮食文化的中国发展得这么好吗?”

“他们比较规范吧,味道好吃,而且还不出错,不会出现像你今天这样,蛋炒饭不好吃,炒面干脆不放盐的情况。”

“你说得没错,麦当劳、肯德基的汉堡,不管在哪家店里吃,什么时间去吃,至少在中国,味道基本都是一样的。而我们国家,比如那道‘鱼香肉丝’,几乎是所有大小中餐饭店都有的一道菜,但却可以吃出上万种口味来,这是为什么?”

“厨师不一样呀,每个人做法不同的。”

“是的,因为厨师不同,他们学习厨艺方法不同,有人是科班出身,有人是师傅带徒弟,有人是照书下料,还有人是自我原创,哈,这样你说同样的菜名‘鱼香肉丝’,味道会一样吗?”

“还不只是这些,同一个厨师,不同时间烧出来同样的菜也不一样的,盐多盐少,炒的火候时间的长短,都是不一样的。”

“说得好,那你仔细想想,麦当劳、肯德基比我们很多中式快餐成功的原因是什么?”

“就感觉他们比较规范,具体原因也说不上来。”

“为什么你的炒面好吃,而我再要的炒面却没有放盐?这好吃不好吃由谁决定?”

“当然是烧菜的人,他感觉好,就是一盘好面,要是心情不好,或者粗心大意,就是一盘垃圾。”小菜肯定地说。

“哈,说得没错,今天我就吃了两盘垃圾,其实这里面最关键的就在于我们是吃得爽还是吃得难受都要依赖于厨师。你再想想我们设计模式的原则?”

“啊,你的意思是依赖倒转原则﹖抽象不应该依赖细节,细节应该依赖于抽象,由于我们要吃的菜都依赖于厨师这样的细节,所以我们就很被动。”

“:)好,那再想想,老麦老肯他们的产品,味道是由什么决定的?”

“我知道,那是由他们的工作流程决定的,由于他们制定了非常规范的工作流程,原料放多少,加热几分钟,都有严格规定,估计放多少盐都是用克来计量的。而这个工作流程是在所有的门店都必须要遵照执行的,所以我们吃到的东西不管在哪在什么时候味道都一样。这里我们要吃的食物都依赖工作流程。不过工作流程好像还是细节呀。”

“对,工作流程也是细节,我们去快餐店消费,我们用不用关心他们的工作流程?当然是不用,我们更关心的是是否好吃。你想如果老肯发现鸡翅烤得有些焦,他们会调整具体的工作流程中的烧烤时间,如果新加一种汉堡,做法都相同,只是配料不相同,工作流程是不变的,只是加了一种具体产品而已,这里工作流程怎么样?”

“对,这里工作流程可以是一种抽象的流程,具体放什么配料、烤多长时间等细节依赖于这个抽象。”

2 建造小人一

“给你出个题目,看看你能不能真正体会到流程的抽象。我的要求是你用程序画一个小人,这在游戏程序里非常常见,现在简单一点,要求是小人要有头、身体、两手、两脚就可以了。”

“废话,人还会有多手多脚呀,那不成了蜈蚣或螃蟹。这程序不难呀,我回去就写给你看。”

时间:4月9日23:30 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“大鸟,程序写出来了,我建立一支黄色的画笔,在pictureBox1上画了,简单了点,但功能实现了。”

Pen p = new Pen(Color.Yellow);
Graphics gThin = pictureBox1.CreateGraphics();
gThin.DrawEllipse(p, 50,20,30,30);   //头
gThin.DrawRectangle(p,60,50,10,50);  //身体
gThin.DrawLine(p,60,50,40,100);      //左手
gThin.DrawLine (p, 70,50,90,100);    //右手
gThin.DrawLine(p, 60,100,45,150);    //左脚
gThin.DrawLine(p,70,100,85,150);     //右脚
好菜每回味不同——建造者模式

“写得很快,那么我现在要你再画一个身体比较胖的小人呢。”

“那不难呀,我马上做好。”

Graphics gFat = pictureBox2.CreateGraphics();
gFat.DrawEllipse(p,50,20,30,30);
gFat.DrawEllipse(p, 45,50,40,50);
gFat.DrawLine(p, 50,50,30,100);
gFat.DrawLine(p, 80,50,100,100);
gFat.DrawLine(p, 60,100,45,150);
好菜每回味不同——建造者模式

“啊,等等,我少画了一条腿。”

gFat.DrawLine(p,70,100,85,150);

“哈,这就和我们刚才去吃炒面一样,老板忘记了放盐,让本是非常美味的夜宵变得无趣。如果是让你开发一个游戏程序,里面的健全人物却少了一条腿,那怎么能行?”

“是呀,画人的时候,头身手脚是必不可少的,不管什么人物,开发时是不能少的。”

“你现在的代码全写在Form1.cs的窗体里,我要是需要在别的地方用这些画小人的程序怎么办?”

3 建造小人二

“嘿,你的意思是分离,这不难办,我建两个类,一个是瘦人的类,一个是胖人的类,不管谁都可以调用它了。”

// 瘦人的类
class PersonThinBuilder
{
  private Graphics g;
  private Pen p;
  
  // 初始化时确定画板和颜色
  public PersonThinBuilder(Graphics g, Pen p)
  {
    this.g = g;
    this.p = p;
  }
  
  // 建造小人
  public void Build()
  {
    g.DrawEllipse(p,50,20,30,30);
    g.DrawRectangle(p,60,50,10,50);
    g.DrawLine(p, 60,50,40,100);
    g.DrawLine(p,70,50,90,100);
    g.DrawLine(p,60,100,45,150);
    g.DrawLine(p,70,100,85,150);
  }
)

“胖人的类也是相似的。然后我在客户端里就只需这样写就可以了。”

Pen p = new Pen(Color.Yellow);

Graphics gThin = pictureBox1.createGraphics();
PersonThinBuilder ptb = new PersonThinBuilder(gThin, p);
ptb.Build();

Graphics gFat = pictureBox2.CreateGraphics();
PersonFatBuilder pfb = new PersonFatBuilder(gFat, p);
pfb.Build();

“你这样写的确达到了可以复用这两个画小人程序的目的。”大鸟说,“但炒面忘记放盐的问题依然没有解决。比如我现在需要你加一个高个的小人,你会不会因为编程不注意,又让他缺胳膊少腿呢?”

“是呀,最好的办法是规定,凡是建造小人,都必须要有头和身体,以及两手两脚。”

4 建造者模式

“你仔细分析会发现,这里建造小人的‘过程’是稳定的,都需要头身手脚,而具体建造的‘细节’是不同的,有胖有瘦有高有矮。但对于用户来讲,我才不管这些,我只想告诉你,我需要一个胖小人来游戏,于是你就建造一个给我就行了。如果你需要将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示的意图时,我们需要应用于一个设计模式,‘建造者(Builder)模式‘,又叫生成器模式。建造者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。如果我们用了建造者模式,那么用户就只需指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需知道了。”

建造者模式(Builder),将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。[DP]

“那怎么用建造者模式呢?”

“一步一步来,首先我们要画小人,都需要画什么?”

“头、身体、左手、右手、左脚、右脚。”

“对的,所以我们先定义一个抽象的建造人的类,来把这个过程给稳定住,不让任何人遗忘当中的任何一步。”

abstract class PersonBuilder
{
  protected Graphics g;
  protected Pen p;
  
  public PersonBuilder(Graphics g, Pen p)
  {
    this.g = g;
    this.p = p;
  )

  public abstract void BuildHead();
  public abstract void BuildBody();
  public abstract void BuildArmLeft();
  public abstract void BuildArmRight();
  public abstract void BuildLegLeft();
  public abstract void BuildLegRight();

“然后,我们需要建造一个瘦的小人,则让这个瘦子类去继承这个抽象类,那就必须去重写这些抽象方法了。否则编译器也不让你通过。”

class PersonThinBuilder : PersonBuilder
{
  public PersonThinBuilder(Graphics g, Pen p) : base(g, p)
  { }
  
  public override void BuildHead()
  {
    g.DrawE1lipse(p, 50,20, 30,30);
  }
  
  public override void BuildBody()
  {
    g.DrawRectangle(p,60,50,10,50);
  }
  
  public override void BuildArmLeft()
  {
    g. DrawLine (p,60,50,40,100);
  )
  public override void BuildArmRight()
  {
    g.DrawLine(p, 70,50,90,100);
  }
  
  public override void BuildLegLeft()
  {
    g.DrawLine(p, 60,100,45,150);
  }
  
  public override void BuildLegRight()
  {
    g. DrawLine(p, 70,100,85,150);
  )
}

“当然,胖人或高个子其实都是用类似的代码去实现这个类就可以了。”

“这样子,我在客户端要调用时,还是需要知道头身手脚这些方法呀?没有解决问题。”小菜不解地问。

“别急,我们还缺建造者模式中一个很重要的类,指挥者(Director),用它来控制建造过程,也用它来隔离用户与建造过程的关联。”

class PersonDirector
{
  private PersonBuilder pb;
  // 用户告诉指挥者,我需要什么样的小人
  public PersonDirector(PersonBuilder pb)
  {  
    this.pb = pb;
  }
  
  // 根据用户的选择建造小人
  public void createPerson()
  {
    pb.BuildHead();
    pb.BuildBody();
    pb.BuildArmLeft();
    pb.BuildArmRight();
    pb.BuildLegLeft();
    pb.BuildLegRight();
  }
}

“你看到没有,PersonDirector类的目的就是根据用户的选择来一步一步建造小人,而建造的过程在指挥者这里完成了,用户就不需要知道了,而且,由于这个过程每一步都是一定要做的,那就不会让少画了一只手,少画一条腿的问题出现了。”

“代码结构图如下。”

好菜每回味不同——建造者模式

“哈,我明白了,那客户端的代码我来写吧。应该也不难实现了。”

Pen p = new Pen(Color.Ye1low);
PersonThinBuilder ptb = new PersonThinBuilder(pictureBox1.CreateGraphics(), p);
PersonDirector pdThin = new PersonDirector(ptb);
pdThin.CreatePerson();
PersonFatBuilder pfb = new PersonFatBuilder(pictureBox2.CreateGraphics(), p);
PersonDirector pdFat = new PersonDirector(pfb);
pdFat.CreatePerson () ;

“试想一下,我如果需要增加一个高个子和矮个子的小人,我们应该怎么做?”

“加两个类,一个高个子类和一个矮个子类,让它们都去继承PersonBuilder,然后客户端调用就可以了。但我有个问题,如果我需要细化一些,比如人的五官,手的上臂、前臂和手掌,大腿小腿这些,如何办呢?”

“问得好,这就需要权衡,如果这些细节是每个具体的小人都需要构建的,那就应该要加进去,反之,就没必要。其实建造者模式是逐步建造产品的,所以建造者的Builder类里的那些建造方法必须要足够普遍,以便为各种类型的具体建造者构造。”

5 建造者模式解析

“来,我们看看建造者模式的结构。”

好菜每回味不同——建造者模式
建造者模式(Builder)结构图

“现在你看这张图就不会感觉陌生了。来总结一下,Builder是什么?”

“是一个建造小人各个部分的抽象类。”

“概括地说,是为创建一个Product对象的各个部件指定的抽象接口。ConcreteBuilder是什么呢?”

“具体的小人,具体实现如何画出小人的头身手脚各个部分。”

“对的,它是具体建造者,实现 Builder 接口,构造和装配各个部件。Product当然就是那些具体的小人,产品角色了,Director是什么?”

指挥者,用来根据用户的需求构建小人对象。”

“嗯,它是构建一个使用Builder接口的对象。”

“那都是什么时候需要使用建造者模式呢?”

“它主要是用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。”

“哦,是不是建造者模式的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者就可以了。”

“来来来,我们来试着把建造者模式的基本代码推演一下,以便有一个更宏观的认识。”

6 建造者模式基本代码

Product类-产品类,由多个部件组成。

class Product
{
  IList<string> parts = new List<string>();

  // 添加产品部件
  public void Add(string part)
  {
    parts.Add(part);
  }

  public void Show()
  {
    Console.WriteLine("\n产品创建----");

    // 列举所有的产品部件
    foreach (string part in parts)
    {
      Console.WriteLine(part);
    }
  }
}

Builder类——抽象建造者类,确定产品由两个部件 PartA和PartB组成,并声明一个得到产品建造后结果的方法 GetResult。

abstract class Builder
{
  public abstract void BuildPartA();
  public abstract void BuildPartB();
  public abstract Product GetResult();
}

ConcreteBuilder1类——具体建造者类。

class ConcreteBuilder1 : Builder
{
  private Product product = new Product();

  // 建造具体的两个部件是部件A和部件B
  public override void BuildPartA()
  {

    product.Add("部件A");
  }

  public override void BuildPartB()
  {
    product.Add("部件B");
  }

  public override Product GetResult()
  {
    return product;
  }
}

ConcreteBuilder2类-具体建造者类。

class ConcreteBuilder2 : Builder
{
  private Product product = new Product();

  // 建造具体的两个部件是部件A和部件B
  public override void BuildPartA()
  {

    product.Add("部件X");
  }

  public override void BuildPartB()
  {
    product.Add("部件Y");
  }

  public override Product GetResult()
  {
    return product;
  }
}

Director类-指挥者类。

class Director
{
  // 用来指挥建造过程
  public void Construct(Builder builder)
  {
    builder.BuildPartA();
    builder.BuildPartB();
  }
}

客户端代码,客户不需知道具体的建造过程。

static void Main(string[] args)
{
  Director director = new Director();
  Builder b1 = new ConcreteBuilder1();
  Builder b2 = new ConcreteBuilder2();
  director.Construct(b1);

  // 指挥者用ConcreteBuilder1的方法来建造产品
  Product p1 = b1.GetResult();
  p1.Show();
  director.Construct(b2);

  // 指挥者用ConcreteBuilder2的方法来建造产品
  Product p2 = b2.GetResult();
  p2.Show();

  Console.Read();
}

“所以说,建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式。

“如果今天大排档做炒面的老板知道建造者模式,他就明白,盐是一定要放的,不然,编译就通不过。”

“什么呀,不然,钱就赚不到了,而且还大大丧失我们对他厨艺的信任。看来,各行各业都应该要懂模式呀。”


下一篇我们接着读“第14章 老板回来,我不知道——观察者模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:https://mp.weixin.qq.com/s/vKBN6eGuxggYevs0C-nVgw

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论