Effective Java(3rd)-Item2:当构造方法有很多参数的时候考虑使用建造者模式

  静态工厂和构造器都有一个限制:他们对于大量的可选参数不能实现很好的扩展。考虑
一个类代表出现在包装食品上营养成分标签的情况。这些标签有一些必填项——分量,每份容器的分量,每份卡路里量以及二十多类可选参数,比如总脂肪,饱和脂肪,反式脂肪,胆固醇,钠等。大多数产品只有少数这些可选字段具有非零值.
  这样的一个类你如何通过构造方法或者静态工厂方法来表示呢?传统上,程序员会使用重叠构造模式(telescoping constructor),也就是提供一个只有必填参数的构造方法,再提供一个有着一个可选参数的构造方法,再提供一个有两个可选参数的构造方法,以此类推。最终提供一个有着所有可选参数的构造方法。下面是以上描述的例子.简单起见,只展示了四个可选区域。

image.png

image.png

  如果要创建实例,可以使用带有包含要设置的所有参数的最短参数列表的构造函数:

NutritionFacts cocaCola =
        new NutritionFacts(240, 8, 100, 0, 35, 27);

  通常这个构造器调用将需要调用许多不想设置的参数,但是无论如何你还是要被迫传递这些参数。在这种情况,我们为fat这个属性传递值0。只有六个参数的时候可能没有那么糟糕,但是当参数的数量增加后就会变得一发不可收拾。
  简而言之,重叠构造器模式是有效的,但是很难用它去实现有很多参数的情况,同时,这样也很难去阅读。读者会疑惑这些所有值都是什么意思,也会更加专注数参数的数量。一长串相同类型的参数会造成微妙的bug。如果客户端偶然反向了两个相同类型的参数,编译器不会报错,但是程序将在运行时发生错误行为。
  当你面对一个构造器有许多可选参数时,你可以选择使用JavaBean模式。你可以调用一个无参方法来创建这个对象,然后将每个需要的参数set进去:

image.png

image.png

  这个模式就没有重叠构造器模式的缺点。它创建实例很简单,就是有点啰嗦,但是读代码也很容易。


image.png

  不幸的是,JavaBean模式本身有着严重的缺点。因为构造跨越多次调用,JavaBean可能在其构造的中途处于不一致的状态。仅仅检查构造器参数的有效性,该类并没有强制一致性的选项。尝试当这个对象在一个不一致的状态下使用该对象,可能会导致与包含该bug的错误代码相距甚远的失败,从而难以调试。相关缺点是JavaBean模式排除了使类不可变的可能性,需要程序员额外的努力来保证线程安全。
  当构造方法完成时通过手动“凝固”对象来阻止被使用直到冻住,这是可行的,但是这个变体是笨重的,并且在实践中几乎不被使用。此外,这会在运行时导致错误,因为编译器不能确保程序员在使用对象前调用冻结方法。

  幸运的是,这里有第三个替代方案结合了重叠构造器模式的安全性和JavaBean模式的可读性。这就是建造者模式的其中一种形式。不直接创建期望的对象,而是客户端调用一个带有所有需要参数的构造方法(或静态工厂)来得到一个建造器对象(builder object)。然后客户端调用类似setter的方法对这个建造器对象设置每个选择的参数。最后,客户端调用无参方法build来构成对象,这通常是不可变的。建造器在build时通常是一个静态成员类(item17),如下描述了在实践中是如何表现的:

image.png

  NutritionFacts类是不可变的(译者注:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值)。所有参数的默认值在同一地方。建造器的setter方法返回了这个建造器本身以至于可以进行链式调用,生成了一个流畅的api(fluent API),如下描述客户端的代码:

image.png

这样的客户端代码容易书写,更重要的是,更容易去阅读。建造者模式模拟在Python和Scala中找到命名可选参数
  有效性校验为了简短而忽略了。为了尽快检测无效参数,在建造器和方法中来进行参数有效性的检查。在构造方法被build方法调用时检查不变量涉及多参数。为了确保这些不变量免受攻击,在从建造器复制参数后对对象字段进行检查 (item50)。如果检查失败,抛出一个IllegalArgumentException(item72)详细描述消息来表明哪一个参数是无效的( item75)。
  建造者模式非常适合类层次结构。使用并行的建造器层次结构,每个建造器嵌套在相应的类中。抽象类有抽象建造器;具体类有具体的建造器。举个例子,考虑一个抽象类代表不同种类的披萨的父类:

image.png

  注意到Pizza.builder是一个带着递归类型参数的泛型类型(generic type)item30),这样,除了抽象类本身的方法以外,也允许方法链在子类中正常工作,而没有必要强制类型转换。如此解决方法是基于Java缺少一个自我类型从而模拟自我类型的习惯。
  这里有两个Pizza的实现类,其中一个代表了标准纽约风格披萨,另一个是卡颂。前者需要一个大小的参数,后者需要你确定酱料是放进去还是分开:

image.png

  注意到build方法在不同子类的建造器中被声明返回相应的子类:NyPizza的build方法返回NyPizza,另一个就返回CalZone。这种技巧,其中声明子类方法返回父类中声明的返回类型的子类型,称为协变式返回类型。这允许客户端使用这些建造器无需强制类型转换。
  这些“有层次的建造器”的客户端代码基本上与简单NutritionFacts建造器相同。为了简洁起见,如下示例的客户端代码假定静态引入了枚举常量:
image.png

  建造器比构造器的的一个次要优势是建造器可以有多个不定参数因为每个参数都是由它自己的方法指定的。或者,建造器可以将传递给方法的多个调用的参数聚合到单个字段中,如之前的addTopping方法所示。
  建造者模式相当灵活。一个单独的建造器可以被多次使用来build多个对象。在构建方法调用之前调整建造器的参数,可以改变创建的对象。
  建造者模式也有缺点。为了创建一个对象,你必须先创建它的建造器。虽然在实践中创建建造器的开销并没有那么明显,但是在性能瓶颈的情况下可能会出现问题。同样的,建造者模式比伸缩构造函数模式更冗长,所以它应该在有足够多的参数情况下才值得使用,比如四个或多个。但是记住你可能在将来会添加更多参数。但是如果你从构造方法或静态工厂开始,并在类演化到参数数量失控时转用建造器,那么过时的构造方法或静态工厂将拇指一样突出。因此,最好从一开始就使用建造器。
  总结,在设计类时,构造方法或静态工厂需要一只手数不过来的参数时,建造者模式是一个好选择,特别是很多参数都是可选的,类型也相同。
相比伸缩构造函数模式而言,客户端代码更容易读写,比JavaBean更安全。

本文写于2018.10.23,历时23天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容