Builder模式的误区:将复杂对象的构建进行封装,就是Builder模式了吗?

最近重读GOF的《设计模式》,读到Builder模式的时候,发现还是不能领悟;网上搜了下其他人的解释,发现很多人都用错了Builder模式,结构形似Builder,实际上却更像Template、或者Factory Method,或者四不像,并没有体现出Builder模式的思想和威力;通过对比学习,也逐渐加深了我对Builder模式的认识,于是就有了这篇文章。

0. GOF - Builder模式

下面是GOF对Builder模式的部分阐述,先列出来,用于与后文中的错误案例进行对比。文字很精辟,不易理解;但若真正理解了,会发现这些文字对已经将Builder模式的精髓描述完了。
(1) 意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

(2) 适用性:
同时满足以下情况的时候可以使用Builder模式
    a. 当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式
    b. 当构造过程必须允许构造的对象有不同的表示

(3) 结构:

留意图中红色注释部分,尤其是循环,灰常灰常重要,呵呵,这也是很多人在使用Builder模式时所忽略的部分。(想想,为什么不是一句builder->BuildPart()就够了,为什么要有这个循环呢?)

(4) 协作:

(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

 
与之前的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模式的范畴,先留着。

相关模式[DP]:Abstract Factory与Builder相似,因为它可以创建复杂对象。主要的区别是Builder模式着重于一步步构造一个复杂对象,而Abstract Factory着重于多个系列的产品对象(简单的或复杂的)。Builder是最后一步返回产品,而Abstract Factory是立即返回产品。Composite通常是用Builder生成的。


happyhippy作者:Silent Void
出处:http://happyhippy.cnblogs.com/
转载须保留此声明,并注明在文章起始位置给出原文链接。

Builder模式的误区相关推荐

  1. Java 常用设计模式 -- Builder模式

    Builder模式是在Java中最流行的模式之一.它很简单,有助于保持对象不可变,并且可以使用Project Lombok的@Builder或Immutables等工具生成,仅举几例. 模式的流畅变体 ...

  2. Creational模式之Builder模式

    1.意图 将一个复杂对象的构建与它表示分离,使得相同的构建过程能够创建不同的表示. 查看很多其它请点击 2.别名 无 3.动机 一个RTF(Rich Text Format)文档交换格式的阅读器应能将 ...

  3. 可扩展性的builder模式的构建方法

    http://www.cnblogs.com/happyhippy/archive/2010/09/01/1814287.html 使用builder模式的时候: 本篇文章的原因:builder模式接 ...

  4. GOF对Builder模式的定义(转载)

    (1)意图 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. (2)适用性 1. 当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式: 2. 当构造过程必须允许 ...

  5. 创建型模式之Builder模式

    1.意图 将一个复杂对象的创建与它的表示分享,使得同样的构建过程可以创建不同的表示. 2.适用性 (1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时 (2)当构造过程必须允许被构 ...

  6. java设计模式:Builder模式

    Builder模式关键是在建立对象是一步一步处理 package customer;class Starbucks {private String size;private String drink; ...

  7. JAVA Builder模式构建MAP/LIST的示例

    我们在构建一个MAP时,要不停的调用put,有时候看着觉得很麻烦,刚好,看了下builder模式,觉得这思路不错,于是乎,照着用builder模式写了一个构建MAP的示例,代码如下: import j ...

  8. 设计模式-Builder模式

    目录 一个例子(做汤) 人工做汤 机器做汤(使用Builder模式) 优缺点 优点 缺点 Builder模式属于创建型模式. 它是将一个复杂对象的构建过程隐藏起来,让使用者只关系自己要生成什么样的对象 ...

  9. Intellij idea generate builder 插件-用于自动生成builder模式代码

    2019独角兽企业重金招聘Python工程师标准>>> 目的:开发中喜欢builder模式去构造一个实例,而当一个对象的属性过多的时候,手动的去完成一个类的builder是很繁琐的: ...

  10. java的设计模式 - Builder模式

    Builder 模式的目的? 构造对象的方式过于复杂,不如将之抽离出来.比如,构造器参数过多 这样说也有点抽象,举个例子吧. 举个例子 比如 非常热门的消息队列RabbitMQ 的 AMQP.Basi ...

最新文章

  1. 用R来分析洛杉矶犯罪
  2. 谈C/C++指针精髓
  3. golang操作mysql用例
  4. 产品小白的知识点1——用户周期
  5. 如何计算一只鸡的表面积?各大专业的奇葩解法
  6. 通过界面生成时不存在的数据刷新界面引起的卡顿问题
  7. java+什么时候才需要deploy_细思极恐 - 什么才是真正的会写 Java ?
  8. 深入理解simhash原理
  9. 排名前100的PHP函数及分析
  10. vue-router linkActiveClass问题
  11. Ubuntu-解决包依赖关系的三种办法
  12. 数据结构题及c语言版4.31答案,数据结构参考题及答案修正版.doc
  13. Linux内核的裁剪移植,Linux内核裁剪移植学习分享
  14. 12306验证码的一些思考
  15. docker打包镜像--go语言编写的http项目
  16. 猴子吃桃问题(三种方法解决)
  17. Android 9 导航栏添加截图按钮
  18. 学习VGG(网络讲解+代码)
  19. 我的世界服务器背景音乐修改,我的世界怎么自定义背景音乐教程攻略
  20. HPA控制器介绍以及实战案例

热门文章

  1. erpc(EmbeddedRPC)入门笔记
  2. i3 10100F和i3 10105F差距大不大
  3. earth orientation parameter(EOP)地球指向参数
  4. Pascal VOCdata数据集读取(pytorch)
  5. mysql 分组查询 语句_MySQL分组查询
  6. PR导入视频后无声音
  7. 《从0开始做运营[张亮]》——读书笔记
  8. 实用网址备忘(实时更新)
  9. 怎么把win10右下角的隐藏图标调出来(把倒三角调出来)
  10. oCPC实践录 | 成本优化策略之eCPC(1)