我也设计模式——4.Builder
但凡是模式,总要有一个UML才能说清楚。一般而言,我只记UML图,有些模式有自己的固定实现代码(只是其中一部分),尤其在C#中,甚至只是把用户当作一个Client端,而不暴露其内部实现,只把封装好的模式接口公布。于是,我会再记一些固定方法实现。还有就是适用场合(多看例子),优缺点。最后,横向的看模式与模式之间的比较,联合与差异。以上都做到了,这套心法也就成正果了。
OO的思想也很重要,三个基本特征尤其是多肽,接口与虚拟类,构造体与类,委托,索引,泛型,都是很值得探讨的。
开始正题,先给出Builder的UML图,如下:
Builder:生成器,抽象接口,有BuildPart()这样的若干虚方法。
ConcreteBuilder:具体实现者,GetResult()方法返回生成的产品。
Director:导航器,保持一个对接口的引用,可以被Client端实例化,从而通过Concrete()方法(见下),间接操纵ConcreteBuilder中的BuildPart()方法。
public void Concrete()
{
b.BuildPart(); //这里b来自Director构造函数中的Builder型参数
}
{
b.BuildPart(); //这里b来自Director构造函数中的Builder型参数
}
这样子,在Director类中,就实现了面向接口编程,即导航器Director在不知道具体是什么样的ConcreteBuilder时,建立接口“对象”b,访问其BuildPart()虚方法。
于是在Client端,我可以这么写:
ConcreteBuilder b = new ConcreteBuilder();
Director c = new Director (b); //将具体生成类放入导航器
c.Concrete();
//这时调用b.GetResult(); 返回生成的对象,即产品。
Director c = new Director (b); //将具体生成类放入导航器
c.Concrete();
//这时调用b.GetResult(); 返回生成的对象,即产品。
可以看到,如果要使用ConcreteBuilderB,而不使用原来的ConcreteBuilder类,那么只要更改Client端第1句话为
ConcreteBuilderB b = new ConcreteBuilderB(); 其他都不用更改。这就是Builder模式所要追求的效果。
可以看“画脸谱”这样一个例子,体会Builder模式的使用。代码如下:
public abstract class FaceBuilder
{
public abstract void BuildFace();
}
public class GUIFaceBuilder : FaceBuilder
{
public override void BuildFace()
{
grap.DrawEllipse(p, 100, 100, 200, 300);
}
private Graphics grap;
private Pen p;
public GUIFaceBuilder()
{
grap = g;
p = new Pen(Color.Red);
}
}
public class FaceDirector
{
private FaceBuilder b;
public FaceDirector(FaceBuilder b)
{
this.b = b;
}
public void CreateFace()
{
b.BuildFace();
}
}
{
public abstract void BuildFace();
}
public class GUIFaceBuilder : FaceBuilder
{
public override void BuildFace()
{
grap.DrawEllipse(p, 100, 100, 200, 300);
}
private Graphics grap;
private Pen p;
public GUIFaceBuilder()
{
grap = g;
p = new Pen(Color.Red);
}
}
public class FaceDirector
{
private FaceBuilder b;
public FaceDirector(FaceBuilder b)
{
this.b = b;
}
public void CreateFace()
{
b.BuildFace();
}
}
注意,这个Sample的生成器中没有GetResult方法,这是因为已经得到g并进行画图,所以不再需要返回产品了,从而省略之。
让我们回过头来,看Builder模式的意图:
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。——《设计模式》
注意1:对象构建过程被封装在Director中,具体的表示由生成器(ConcreteBuilder)完成。从而构建与表示彻底分离。
注意2:相同的Director(构建过程)。
注意3:不同的表示(ConcreteBuilder),上文的ConcreteBuilder对象与ConcreteBuilderB对象具有不同的类型,也就是说,Builder模式不关心产生的结果。
和抽象工厂的区别:前不久面试,被问到这里卡住了,其实很简单啦,抽象工厂生成一个产品系列,接口相同;而Builder模式产生的成品可以有不同接口,只要创建过程是同样的方法就可以。还有就是,如果是抽象工厂,Client端不需要知道返回对象的具体类型,因为抽象工厂构造的是具有相同接口的产品系列;如果是Builder模式,用户必须知道返回对象的具体类型,因为不同的生成器返回的类型是不同的。
另一个例子是登录界面,既支持Windows应用,又支持Web应用,这是两个不同接口的产品界面,但画出来的UI是一样的,所以要用Builder模式实现。只要保证在虚方法实现时把不同的控件扔进Panel,最后通过GetResult返回Panel对象。在各自不同的Clinet端使用这个Panel。
注意,Builder模式=Builder+Director,二者缺一不可;.NET平台有一个StringBuilder,这只是Builder,与模式没关系。
另外,.NET平台中,Page类中的OnInit()等方法的实现。我们在写一个Web页面的时候。他的codebehind代码都是继承System.Web.UI.Page基类的。OnInit()函数是可以重写的,这也是Builder模式的应用之一:导航器和Builder接口封装在Framework中。生成器也在封装在Framework中,只是对外暴露一个虚方法实现OnInit(),由你来实现。Client端调用机制也在Page生成周期中自动实现。——.NET充分利用了模式来方便开发者,但是不说清楚,一般开发人是不会留意的。
补注:
1. GetResult()方法的位置。
文中讲的是放在具体生成器中。可不可以放在Builder接口中呢,或者省略这个方法呢?答案是可以的。
1)当不需要返回一个产品时,就不需要GetResult这个方法。比如说文中讲的画脸谱,在BuildPart()方法中就已经实现了g.Draw这样的操作,这时候是否生成产品就不重要了。
2)当需要返回一个产品时,分两种情况:如果这些产品具有相同的接口,则可以将GetResult()方法抽象到Builder接口,因为返回相同的产品类型;但是如果这些产品的接口不同,则必须各自实现在具体的生成器中。
2. Builder的缺点:不方便添加一个新BuilderPart()方法。这是因为,如果需要增加新的BuilderPart(),要同时改动接口,原有的生成器,还有导航器,也就是说,所有的都要改变,这是不可以的。
3. AbstractFactory认为构造过程和生成都是很简单的,这时候,如果这些产品具有相同的接口,使用抽象工厂可好,而不使用Builder。
4.既然Director和Builder接口都是不便的,那么是否可以不要Director,而把其Concrete()方法放入Builder接口中呢?同时不暴露Builder接口的BuildPart()方法,将其访问权限设为protected(允许具体生成器重写实现)。新的UML图如下:
代码相应为:
public abstract class Builder
{
protected abstract void BuildPartA();
protected abstract void BuildPartB();
public void Concrete()
{
this.BuildPartA();
this.BuildPartB();
}
}
public class ConcreteBuilder : Builder
{
protected override void BuildPartA()
{
Console.WriteLine();
}
protected override void BuildPartB()
{
Console.WriteLine();
}
public string GetResult()
{
return "1";
}
}
public class Client
{
public void TestBuilder()
{
ConcreteBuilder bui = new ConcreteBuilder();
bui.Concrete();
}
}
{
protected abstract void BuildPartA();
protected abstract void BuildPartB();
public void Concrete()
{
this.BuildPartA();
this.BuildPartB();
}
}
public class ConcreteBuilder : Builder
{
protected override void BuildPartA()
{
Console.WriteLine();
}
protected override void BuildPartB()
{
Console.WriteLine();
}
public string GetResult()
{
return "1";
}
}
public class Client
{
public void TestBuilder()
{
ConcreteBuilder bui = new ConcreteBuilder();
bui.Concrete();
}
}
这样一来,就蜕化成为Template模式了,已经体现不出来Builder的宗旨。尤其是当对应于一套Builder,存在多个导航器的时候,确实需要抽象出一个Director来。