建造者模式(Builder)
引子
先来看一个现象,那就是肯德基或者是麦当劳的例子,
肯德基和麦当劳光在中国就有无数的店面,但是为什么每个店面里的食品味道都是差不多的,
你在店 A 中吃的东西的味道,在店 B 中还是这个味道,这是如何做到的呢?
为什么在中国, n 个餐馆里面,一道青椒炒肉有 n 个不同的味道呢?
首先,看中国菜--青椒炒肉,n 个餐馆里面的厨师都是不同的,
鬼晓得这些厨师是自学成才还是从新东方烹饪学校毕业的呢?
也就是不同的厨师的手艺是不同的,自学成才的厨师做的青椒炒肉有它的特色,
而新东方烹饪学校毕业的呢,稍微符合标准一点,但可能有的厨师学的好,还有一些厨师在学校混日子的,
学的不行,所以,造成一道青椒炒肉 n 个味道,
同时,如果一个厨师今天心情好给中彩票了,那么做出来的青椒炒肉可能味道也不错(含有快乐的成分),
相反,如果这个厨师昨天和老婆吵架了正在闹离婚,
那么做出了的青椒炒肉有可能没放盐,或者把洗衣粉当盐放了也不一定(心情不好嘛,心不在焉),
所以,就算是同一个厨师做出来的青椒炒肉也会有差别的 !!!
相反,肯德基或者麦当劳的话,他们有自己的一套非常规范工作流程,比如,原料放多少,盐放几克,
加热几分钟,先放盐还是先放油,都是有严格规定的,而后就是所有的店面都必须严格遵守这个工作流程,
这样的话,自然做出来的食物味道都差不多了(排除那一天微波炉坏了,给加热过头了)。
其实上面肯德基和麦当劳的例子反映的就是一个建造者模式了。
通常一个产品有不同的组成成分来作为产品的零件,而这些零件呢,有可能是对象,也有可能不是对象,
它们通常又叫做产品的内部表象。不同的产品可以有不同的内部表象,也就是不同的零件。
使用建造者模式可以使客户端不需要知道所生成的产品对象有哪些零件,每个产品的对应零件彼此有何不同,
是怎么被建造出来的,以及怎样组成产品。
还有一些情况下,一个对象的一些性质必须按照某个顺序赋值才有意义,在某个性质没有赋值之前,
另一个性质则无法赋值。
何为建造者模式(也叫生成器模式)?
建造者模式就是将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。
上面关于肯德基或者麦当劳的例子来说的话,只需要指定需要的是什么食物就 OK ,
而不需要去控制加多少盐,烤多久等等细节操作(在这里将这些看做一个工作流程),说白点,就是抽象不应该依赖于细节。
上面的例子就可以很明显的体现出建造者模式的优点:
使用建造者模式的话,它封装了一个产品的构造过程,并且允许按照预定的步骤构造产品。
同时,向客户端隐藏了产品的内部表现。
结构类图
下面就来解释一下上面的这幅结构类图:
首先是 AbstractBuilder 这个抽象类:
定义了一系列的抽象接口,用来规范 Product 对象的各个组成成分的构建。
然后是 ConcreteBuilder 具体类:
实现抽象类 AbstractBuilder 类中的接口,用来构造和装配各个组成成分。
还有指挥者 Director 这个类:
调用具体建造者 ConcreteBuilder 创建产品对象。
需要注意的是,Director 和 Product 之间没有任何的直接关系,Director 是和 客户端打交道的。
最后就是 Product 类:
自然就是要建造的复杂对象了。
下面看建造者模式的基本代码
AbstractBuilder:
namespace Builder
{
public abstract class AbstractBuilder
{
//构建产品的 A 部件
public abstract void BuilderPartA();
//构建产品的 B 部件
public abstract void BuilderPartB();
//返回构建好的产品
public abstract Product GetResult();
}
}
ConcreteBuilderA:
namespace Builder
{
public class ConcreteBuilderB : AbstractBuilder
{
private Product product = new Product();
public override void BuilderPartA()
{
product.AddPart("具体类 A 的组件 A");
}
public override void BuilderPartB()
{
product.AddPart("具体类 A 的组件 B");
}
public override Product GetResult()
{
return product;
}
}
}
ConcreteBuilderB:
namespace Builder
{
public class ConcreteBuilderA : AbstractBuilder
{
//保存一个产品的引用
private Product product = new Product();
//实现组装部件 A
public override void BuilderPartA()
{
product.AddPart("具体类 B 的组件 A");
}
//实现组装部件 B
public override void BuilderPartB()
{
product.AddPart("具体类 B 的组件 B");
}
//返回构建好的产品
public override Product GetResult()
{
return product;
}
}
}
Product:
using System;
using System.Collections.Generic;
namespace Builder
{
public class Product
{
private IList<string> parts = new List<string>();
public void AddPart(string part)
{
parts.Add(part);
}
public void ShowProduct()
{
foreach (string str in parts)
{
Console.WriteLine(str);
}
}
}
}
Director:
namespace Builder
{
public class Director
{
//通过指挥者的 Construct 来按照规定的顺序组装产品
public void Construct(AbstractBuilder builder)
{
//先组装 A 部件
builder.BuilderPartA();
//在组装 B 部件
builder.BuilderPartB();
}
}
}
客户端代码:
using System;
using Builder;
namespace BuilderTest
{
class Program
{
static void Main(string[] args)
{
//定义一个组装产品的指挥者
Director director = new Director();
Console.WriteLine("产品 A:");
//指挥组装产品 A
AbstractBuilder builderA = new ConcreteBuilderA();
director.Construct(builderA);
builderA.GetResult().ShowProduct();
//指挥组装产品 B
Console.WriteLine("\n产品 B:");
AbstractBuilder builderB = new ConcreteBuilderB();
director.Construct(builderB);
builderB.GetResult().ShowProduct();
Console.Read();
}
}
}
得到效果图:
上面便是建造者模式的基本代码了。
可以看到我通过建造者模式建造了两个产品(产品 A 和产品 B),
我通过在具体建造者类中定义实现产品的各个部件的接口,然后通过指挥者来指挥组装这些部件成为一个产品。
可以看出建造者模式可以用于创建一些复杂的对象,
这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。
比如,建房子,一般顺序是看地段,设计蓝图,打地基,建造框架,完成建造。
一般来说的话,建房子的步骤基本上就是上面的这几个,也就是说基本的建造房子的顺序基本上是确定的,
但是对于其中的每一项都是有千万种变化的,比如建造框架,就可以建造成方的,圆的,半圆的,半方的等等,
上面说明的就是把房子看做一个对象,那么建造这个对象呢,需要在内部构建诸如看地段,设计蓝图等等部件,
而这些部件都是有千变万化的,所以我们便可以使用建造者模式来建造房子,
我在 AbstractBuilder 中建立基本的建造房子的对外接口,比如有看地段,设计蓝图等,
然后我如果需要建造一个方的房子的话,我只需要一个 ConcreteBuilder ,
然后使其继承自 AbstractBuilder ,并且按建方房子的方式重写在 AbstractBuilder 中定义的接口。
这样便可以实现建造一个方房子。
而如果我又需要建造一个圆房子,那么我只需要添加一个 ConcreteBuilder ,
并且让其继承自 AbstractBuilder ,并且按建造圆房子的方式重写 AbstractBuilder 中定义的接口就 OK 了。
从上面的这个建房子的例子中也可以看出建造者模式的一些优点,
建造者模式让建造代码和表示代码分离,由于建造者隐藏了该产品是如何组装的(由指挥者完成的组装,客户端根本就不知道),
所以需要改变一个产品的内部表示时,只需要添加一个具体建造者类就 OK 了。
下面我们就将上面的这个关于建造房子的例子使用建造者模式完成:
HouseBuilder:
namespace BuilderHouse
{
public abstract class HouseBuilder
{
//看地段
public abstract void FindGroundsill();
//设计蓝图
public abstract void DesignBluePrint();
//打地基
public abstract void BuildFoundation();
//建造框架
public abstract void BuildFrame();
//完成建造
public abstract void FinishBuild();
//返回建造好的房子
public abstract House GetHouse();
}
}
BlockHouseBuilder :
namespace BuilderHouse
{
/// <summary>
/// 圆木小屋的建造接口
/// </summary>
public class BlockHouseBuilder : HouseBuilder
{
private House house = new House();
public override void FindGroundsill()
{
house.AddPart("建造圆木小屋--看地段");
}
public override void DesignBluePrint()
{
house.AddPart("建造圆木小屋--设计蓝图");
}
public override void BuildFoundation()
{
house.AddPart("建造圆木小屋--打地基");
}
public override void BuildFrame()
{
house.AddPart("建造圆木小屋--建造框架");
}
public override void FinishBuild()
{
house.AddPart("建造圆木小屋--完成建造");
}
public override House GetHouse()
{
return house;
}
}
}
LogCabinBuilder :
namespace BuilderHouse
{
/// <summary>
/// 建造小巧木屋
/// </summary>
public class LogCabinBuilder : HouseBuilder
{
private House house = new House();
public override void FindGroundsill()
{
this.house.AddPart("建造小巧木屋--看地段");
}
public override void DesignBluePrint()
{
this.house.AddPart("建造小巧木屋--设计蓝图");
}
public override void BuildFoundation()
{
this.house.AddPart("建造小巧木屋--打地基");
}
public override void BuildFrame()
{
this.house.AddPart("建造小巧木屋--建造框架");
}
public override void FinishBuild()
{
this.house.AddPart("建造小巧木屋--完成建造");
}
public override House GetHouse()
{
return house;
}
}
}
House:
using System;
using System.Collections.Generic;
namespace BuilderHouse
{
public class House
{
private IList<string> parts = new List<string>();
public void AddPart(string part)
{
this.parts.Add(part);
}
public void ShowHouse()
{
foreach (string str in parts)
{
Console.WriteLine(str);
}
}
}
}
Director:
namespace BuilderHouse
{
public class Director
{
public void BuildHouse(HouseBuilder builder)
{
//先看地段
builder.FindGroundsill();
//再设计蓝图
builder.DesignBluePrint();
//再打地基
builder.BuildFoundation();
//再建造框架
builder.BuildFrame();
//最后就是完成建造了
builder.FinishBuild();
}
}
}
客户端代码:
using System;
using BuilderHouse;
namespace BuilderHouseTest
{
class Program
{
static void Main(string[] args)
{
//先实例化一个指挥者
Director director = new Director();
//建造圆木小屋
Console.WriteLine("\n建造圆木小屋\n");
HouseBuilder blockHouse = new BlockHouseBuilder();
//通过指挥者来建造圆木小屋
director.BuildHouse(blockHouse);
//下面这步骤就无关紧要了,纯粹是为了显示而已
blockHouse.GetHouse().ShowHouse();
//建造小巧木屋
Console.WriteLine("\n建造小巧木屋\n");
HouseBuilder logCabin = new LogCabinBuilder();
director.BuildHouse(logCabin);
logCabin.GetHouse().ShowHouse();
Console.ReadKey();
}
}
}
效果展示:
上面的代码便完成了建房子这个例子了,
为了清楚的明白建造者模式,下面我在考虑如果我要建造一个别墅 Villadom ,
那么应该如何做呢,很简单,加下面一个类,
namespace BuilderHouse
{
/// <summary>
/// 建造富豪别墅
/// </summary>
public class Villadom : HouseBuilder
{
private House house = new House();
public override void FindGroundsill()
{
this.house.AddPart("建造富豪别墅--看地段");
}
public override void DesignBluePrint()
{
this.house.AddPart("建造富豪别墅--设计蓝图");
}
public override void BuildFoundation()
{
this.house.AddPart("建造富豪别墅--打地基");
}
public override void BuildFrame()
{
this.house.AddPart("建造富豪别墅--建造框架");
}
public override void FinishBuild()
{
this.house.AddPart("建造富豪别墅--完成建造");
}
public override House GetHouse()
{
return house;
}
}
}
然后就可以在客户端通过指挥者来建造别墅了,这里便体现出了建造者模式的优势。
总结:
建造者模式,通过将建造代码和表现代码分离,来实现解耦组装过程和具体部件的创建过程。
并且对客户端隐藏了产品是如何组装这一细节,从而实现客户端无需关心产品如何组装而来,只需使用即可。
肯德基和麦当劳正是由于学习了建造者模式,让所有的食物都通过一个工作流程来完成,
将工作流程看做一种抽象,而其中的诸如加盐多少,加油多少,烘烤多久以及这些操作的执行顺序等细节全部都有严格的规定,
并且都是依赖于工作流程这个抽象,各个肯德基,麦当劳店面严格遵守这一工作流程(客户端依赖于抽象),
所以各个店面的食物味道都是一样的,这样才使得肯德基,麦当劳在中国大火。