最近重读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生成的。

本文转自Silent Void博客园博客,原文链接:http://www.cnblogs.com/happyhippy/archive/2010/09/01/1814287.html,如需转载请自行联系原作者

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

  1. 使用Builder模式创建复杂可选参数对象

    在新建对象时,若需要对大量可选参数进行赋值,最常见的做法是JavaBeans模式,即调用一个无参构造方法创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数.代码示例如下: ...

  2. Factory Method模式的误区:Factory Method模式是简化版的Abstract Factory吗?

    FactoryMethod是一个相对比较简单的创建型模式,但是能领悟或者用对的并不多见:很多示例都没有反应出Factory Method的核心思想,只是实现了一个简化版的Abstract Factor ...

  3. java静态注解处理器_java – 使用mapstruct中的builder(使用immutables注释处理器)将对象映射到不可变对象...

    我们使用 immutables framework生成所有DTO.现在我们想用 mapstruct将这些对象映射到另一个.但生成的DTO是不可变的,没有setter,也没有构造函数,对应于builde ...

  4. C++ 设计模式 建造者模式(复杂对象的构建与其表示分离)肯德基不同烧鸡的制作过程

    文章目录 1. 理论基础 2. 逻辑代码 3. 应用 3.1 做汉堡咯 思考: 为何肯德基麦当劳这些快餐能在中国这个上下五千年的国都站住脚? 中国的鱼香肉丝为何不能成为令人追捧的快餐? 因为麦当劳肯德 ...

  5. 如何利用装饰者模式在不改变原有对象的基础上扩展功能

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:双子孤狼 blog.csdn.net/zwx9001 ...

  6. 处理对象的多种状态及其相互转换——状态模式

    本文转载自 :http://blog.csdn.net/lovelion/article/details/8522982 "人有悲欢离合,月有阴晴圆缺",包括人在内,很多事物都具有 ...

  7. 依赖注入模式中,为什么用对象而不是用数组传递?

    依赖注入(Dependence Injection, DI) 依赖注入是控制反转的一种设计模式.依赖注入的核心是把类所依赖的单元的实例化过程,放到类的外面去实现.依赖注入的实现离不开反射. 依赖注入( ...

  8. Auto CAD:CAD软件之底边菜单栏(捕捉模式、极轴状态、对象捕捉、允许禁止动态UCS、动态输入、显示隐藏线宽、快速查看图形、选择循环命令、切换空间按钮)简介之详细攻略

    Auto CAD:CAD软件之底边菜单栏(捕捉模式.极轴状态.对象捕捉.允许禁止动态UCS.动态输入.显示隐藏线宽.快速查看图形.选择循环命令.切换空间按钮)简介之详细攻略 目录

  9. 设计模式系列:搞懂组合模式,单对象与组合对象对外统一接口

    组合模式的定义:又叫作整体-部分(Part-Whole)模式,通过将单个对象(叶节点)和组合对象用相同的接口表示,使客户端对单个对象和组合对象的访问具有一致性.它是一种将对象组合成树状的层次结构的模式 ...

  10. 【设计模式自习室】享元模式 Flyweight Pattern:减少对象数量

    前言 <设计模式自习室>系列,顾名思义,本系列文章带你温习常见的设计模式.主要内容有: 该模式的介绍,包括: 引子.意图(大白话解释) 类图.时序图(理论规范) 该模式的代码示例:熟悉该模 ...

最新文章

  1. Python内置数据结构之双向队列
  2. 赠书 | 联邦学习如何在视觉领域应用?
  3. private MyAddin(System.IntPtr mdlDesc) : base(mdlDesc)这一步后就出bug
  4. 小波滤波器与其他滤波器的区别_小波变换(六):小波变换在机器学习中的应用(上)...
  5. left outer join 和 right outer join 和 join 的区别
  6. centos uwsgi自动调用python2环境,指定uwsgi调用版本
  7. 【软件工程】软件开发方法
  8. python 科学计算设计_《Python科学计算-(第2版)》怎么样_目录_pdf在线阅读 - 课课家教育...
  9. 客户端负载均衡及透明应用切换(TAF)tnsnames failover=on
  10. Python 网络爬虫与信息获取(二)—— 页面内容提取
  11. 安装php-zbarcode的步骤方法
  12. idea中java导包快捷键_JAVA入门:IntelliJ Idea 常用快捷键
  13. 系统架构设计师:分布式系统(中间件技术)
  14. 思维导图软件哪个好?盘点10款好用的思维导图软件
  15. dp算法求解矩阵连乘的问题
  16. python求解一元二次方程考虑复数_Python学习笔记:求解一元二次方程
  17. 70个城市房价上涨,令人忐忑
  18. Windows系统邮件中如何绑定QQ邮箱
  19. java实现阿里云短信验证
  20. 某小说App返回数据 解密分析

热门文章

  1. redis数据类型:hashes
  2. JAVA的反射机制原理
  3. [BZOJ2753][SCOI2012]滑雪与时间胶囊(特殊的有向树形图)
  4. spring管理bean容器(笔记)[继]
  5. autocad.net QQ群:193522571 判断string中是否包含集合中所有的字符串
  6. With great power comes great responsibility
  7. Win XP系统下局域网内无法访问其他计算机的共享如何解决
  8. Spark官方3 ---------Spark Streaming编程指南(1.5.0)
  9. [转载]如何捕获控制台消息
  10. 解析Disruptor的依赖关系