(转)Builder模式的误区:将复杂对象的构建进行封装,就是Builder模式了吗?
最近重读GOF的《设计模式》,读到Builder模式的时候,发现还是不能领悟;网上搜了下其他人的解释,发现很多人都用错了Builder模式,结构形似Builder,实际上却更像Template、或者FactoryMethod,或者四不像,并没有体现出Builder模式的思想和威力;通过对比学习,也逐渐加深了我对Builder模式的认识,于是就有了这篇文章。
0. GOF - Builder模式
下面是GOF对Builder模式的部分阐述,先列出来,用于与后文中的错误案例进行对比。文字很精辟,不易理解;但若真正理解了,会发现这些文字对已经将Builder模式的精髓描述完了。
(1) 意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
(2) 适用性:
当同时满足以下情况的时候可以使用Builder模式
a. 当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式;
b. 当构造过程必须允许构造的对象有不同的表示;
(3) 结构:
留意图中红色注释部分,尤其是循环,灰常灰常重要,呵呵,这也是很多人在使用Builder模式时所忽略的部分。(想想,为什么不是一句builder->BuildPart()就够了,为什么要有这个循环呢?)
(5) 相关模式:
Composite通常是用Builder生成的。
先不解释这些文字,先看两个例子,看看使用Builder模式的误区。这两个例子皆来自书上(对不起,我不是恶意挑刺,实在是两位大哥太出名了-_-,还请见谅)。
1. 组合不同数量的现有部件,需要定义新的Builder子类吗?
先看一个例子:Builder模式应用实践
(此图从原文中copy来的)
这个例子中,设备(Equipment)是一个复杂对象,由一个Machine和一个(或多个)输入端口(InputPort)或者输出端口(OutputPort)组成;此设计中定义了一个LCDFactory(充当导向器[Director]的角色)、一个设备生成器(EQPBuilder),及三个ConcreteBuilder:
InputEQPBuilder生成的Equipment = 1个Machine + 1个InputPort;
OutputEQPBuilder生成的Equipment = 1个Machine + 1个OutputPort;
IOPutEQPBuilder生成的Equipment = 1个Machine + 1 个InputPort + 1个OutputPort;
此设计对复杂对象Equipment的创建过程进行了封装,在应对需求变化上,作者的解释是:“例如要求创建的Equipment包含一个Machine对象,一个Input类型的Port,两个Output类型的Port,那么我们可以在不修改原有程序集的前提下,新定义一个IO2PutEQPBuilder类,并继承自抽象类EQPBuilder”。
也就是说,每当要给设备增加端口的时候,我们就要创建新的Builder子类。我们把这个需求扩大化,如果要创建一个Equipment,其可能包含0~M(M>=0)个InputPort、0~N(N>=0)个OutputPort,这样可能组合出(M+1) * (N + 1)个Equipment,因此我们就需要创建(M + 1) * (N + 1)个ConcreteBuilder,这会带来Builder子类数量的急剧膨胀;其本质上是通过继承来达到构建不同的Equipment。这与Builder模式的思想是相违背的,结合Builder模式的结构图来看,导向器(Diretor)是调用BuildPart()方法,来将部件(Part)组合到目标Product中的;如果只是组合不同数量的现有部件,则不用定义新的ConcreteBuilder。
因此,虽然这个类图几乎形似Builder模式,但却并不是Builder模式的应用。
2. Builder模式就是创建复杂对象的模板吗?
一些Builder模式的“应用”,感觉更像是一个创建复杂对象的模板;而对Builder模式与Template Method模式的区分,则认为Builder模式是侧重于创建复杂对象,而Template Mehod则侧重于对象的行为。
在我看来,这个观点是错误的。如果把Builder当作一个创建复杂对象的模板,则基本上可以断定,Builder模式被误用了。Builder模式的类图结构中,装配复杂对象的组成部分,是用BuilderPart()方法来定义,如果我们把这个装配操作视为一个操作行为,是不是意味着这种情况下的Builder模式就是一个Template Method了呢?我认为答案是否定的。Builder模式与Template Method模式有着天壤之别,二者毫不相干;前者偏重于通过聚合来组装对象,后者偏重于通过继承来重写对象的行为。
下面再来看下,《大话设计模式》中的例子:该应用中,要求画一个小人,要有头、身体 、两手、两脚。给出的类图结构如下所示:
(此图从该书中copy来的)
在这个例子中,原作者要求小人不能缺胳膊少腿;我猜测,其中也隐含着小人不能有三头六臂。作者认为“这里构造小人的‘过程’是稳定的,都需要头身手脚,而具体构造的‘细节’是不同的,有胖有瘦有高有矮”。在应对高矮胖瘦的需求变化时,只需要增加新的PersonBuilder子类就行了。如果需要细化‘细节’,“比如人的五官、手的上臂、前臂和手掌,大腿小腿”,“这些细节是每个具体的小人都需要构建的”,则需要将这些接口加到Builder接口中;Builder模式中的Builder接口必须要“足够普遍,以便各种类型的具体建造者构造”。
虽然这个类图几乎神似Builder模式,但细细斟酌,却也有不妥的地方:
(1)需求变化时,人可高可矮可胖可瘦,所以可以该设计中,就可为高矮胖瘦分别创建ConcreteBuilder,但假如需求继续变化,要实现高胖、矮胖、高瘦、矮瘦呢,是否需要继续扩展Builder?在我的理解中,高矮胖瘦体现的是“人的特征”;游戏中小人的特征可能非常多(方脑壳、圆脑壳、O型腿、八字脚、咸猪手),为创建每个不同特征的小人,都配置一个Builder,可能不太现实。
(2) 此示例中,原作者认为“构造小人的‘过程’是稳定的”;偏重于去说明,面对“细节”(即组成人的部件:头、身、手、脚)的差异,需要创建新的ConcreteBuilder;这就容易给人造成一种错觉:造小人的过程是一个类似于一个模板,构造不同的小人时,需要继承该模板、重写差异来实现新小人的生成。
(3) Builder模式构造出来的不同种类的Product,这些Product的组成部分(part)相互不能进行替换或组合,否者将会带来ConcreteBuilder数量的急剧膨胀。这一点在后面再来说。
Builder模式的核心是“聚合”,这个例子中,并没有把Builder模式的思想体现出来。
3. Builder模式示例
为了避免构造新的示例,便于比较和理解,我直接在上面两个例子的基础上进行修改:
3.1 改进版的EQPBuilder
(1).InputPort、OutputPort、Machine等,是复杂对象Equipment的组成部分,这些部件的装配方式在AddXXOOPort、BuildMachine等方法中定义;而如何根据这些部件来创建复杂Equiment的算法,在导向器类LCDFactory中定义;这就使得“创建复杂对象的算法独立于该对象的组成部分以及他们的装配方式”。
(2).AddXXOOPort、BuildXXOOMachine等接口,封装了部件(Port、machine)与产品(Equipment)的装配方式,Add操作可能比较复杂,其可能封装了初始化Port设备、执行插拔、焊接等操作;LCDFactory作为创建设备导向器,如果其构造设备的过程中,要增加多个Port,则只需要多次复用Builder的Add操作即可。因此,如果只是组合不同数量的现有部件,本质上只是“创建复杂对象的算法”被改变了;因为我们只用调整算法部分LCDFactory就可以了,而不用去创建新的ConcreteBuilder;
(3). 当且仅当新的部件(SuperXXOO)需要加入到系统时,才需要去创建新的ConcreteBuilder;如果要创建SuperEquipment,我们只需要将SuperEQPBuilder的示例传递给LCDFactory就足够了,这里复用了原有的构造Equipment的算法。
3.2 改进版的PersonBuilder
与之前PersonBuilder的差别:
(1). 奥特曼是机器人,变形金刚是汽车人,统统可以抽象出来当人,有头有身体有胳膊有腿(暂时不考虑汽车人变形的情况)。BuildPart(Head/Body/Arm/Leg)封装了创建部件、并装配人身上的操作;这个Build操作供导向器复用。PersonDirector定义了创建人的多种算法,不同算法调用BuildPart()的顺序和次数不同,可以生成出具有1头1身2臂2腿的常规人,也可以创建独臂刀客,还可以创建三头六臂的超人等。
(2).构造小人的算法是灵活多变的,该算法在PersonDirector中定义;至于如何变,可以用其他设计模式来实现,这里不与讨论,这里侧重的是Builder模式的应用。只要我们将不同的ConcreteBuilder传递给同一PersonDirector,就可以得到不同的人(人类、机器人、汽车人),从而复用了创建Person的算法,达到同样的构件过程可以创建不同的表示。
3.3 特例:StringBuilder
在这个Builder模式的实现中,Client同时充当了Director的角色;StringBuilder同时充当了Builder接口和ConcreteBuilder。这是一个最简化的Builder模式的实现。
1: //Client同时充当了Director的角色
2: StringBuilder builder = new StringBuilder();
3: builder.Append("happyhippy" );
4: builder.Append(".cnblogs" );
5: builder.Append(".com" );
6: //返回string对象:happyhippy.cnblogs.com
7:
builder.ToString();
4. Builder模式的核心思想
将一个“复杂对象的构建算法”与它的“部件及组装方式”分离,使得构件算法和组装方式可以独立应对变化;复用同样的构建算法可以创建不同的表示,不同的构建过程可以复用相同的部件组装方式。
抽象的Builder类,为导向者可能要求创建的每一个构件(Part)定义一个操作(接口)。这些操作缺省情况下什么都不做。一个ConcreteBuilder类对它所感兴趣的构建重定义这些操作。每个ConcreteBuilder包含了创建和装配一个特定产品的所有代码(注意:ConcreteBuilder只是提供了使用部件装配产品的操作接口,但不提供具体的装配算法,装配算法在导向器[Director]中定义)。这些代码只需要写一次;然后不同的Director可以复用它,以在相同部件集合的基础上构建不同的Product。
回过头再来看,类图结构中对Director的注释,为什么不是一句builder->BuildPart()就够了,为什么要有这个循环呢?BuildPart方法封装了创建Part、并组装到Product中的操作,循环调用调用多次时,可以反复复用BuildPart操作,让目标Product聚合多个Part。再进一步:如果Part中可以聚合多个Part,然后递归下去,可以组合成一颗树型结构,这就是Composite了;在来理解相关模式中的这句话:“Composite通常是用Builder生成的”,就很容易理解了。
另外,需要指出的一点。单纯的Builder模式中,“不同Product类型”的组成部件之间,不能进行组合或替换。譬如上面的两个示例中:组成普通Equipment的普通InputPort、OutputPort、Machine,不允许与组成SuperEquipment的SuperInputPort、SuperOutputPort、SuperMachine进行组合创建新的Equipment;人的头身臂腿,与奥特曼的头身臂腿,或者汽车人的头身臂腿,三者之间的部件不能兼容或替换。这一点GOF在DP中并没有说明,但是在他们给出的两个例子中,充分体现了这一点:RTF的三个转换器,ASCIIConvert只负责组合ASCIICharactar,TeXConverter之负责组合自身格式的部件(Charactor、FontChange、Paragraph),TextWidgetConverter同理;因此不可能出现由TextWidget格式的Charactor和TeX格式的Paragraph组合而成的Text。GOF的另一个Builder模式的应用示例是StandardMazeBuilder与CountingMazeBuilder;GOF在介绍创建型模式时,前后多次用到Wall/BombedWall、Room/RoomWithABomb,为什么这里GOF偏偏不用BombedMazeBuilder,而别出心裁搞出个CountingMazeBuilder;他们很巧妙地回避了部件替换问题。假如允许“不同Product类型”的组成部件之间进行组合或替换,譬如我们允许将奥特曼的头与变形金刚的头进行互换,或者允许将机器人的身体替换的人的身体来构建出钢铁侠,或者使用其他组合来构建金刚狼,我们该怎么办呢?这个问题已经超出了Builder模式的范畴,先留着。