Effective.Java 读书笔记(2)使用Builder

2.Consider a builder when faced with many constructor parameters

大意为当你面对大量的构造参数时考虑使用Builder

静态工厂和构造器都有一个限制,它们不能够很好地缩减大量地选项参数,想象一下一种情况,你的类有着很多的成员变量,有些必须填写有些可以选填,那么如果使用传统的构造方法的话,排列组合一下可以想象会有多少个构造方法出现,这样的情况不是我们所需要的,程序员们通常会使用一种名为”伸缩构造函数模式(Telescoping constructor pattern )“的办法,就是先提供必须要的选项参数作为最简单的构造方法,然后把非必须的选项参数逐渐加上去构成新的构造方法,不考虑组合的问题

举个例子,现在你的构造方法有2个必须的参数A和B,然后有三个选填的参数C,D和E,那么如果我们使用Telescoping constructor pattern,那么代码看上去还是比较简洁的,只有4个不同的构造函数,如下

constructor(A a,B b){ //.....   }

constructor(A a,B b,C c){ //.....   }

constructor(A a,B b,C c,D d){ //.....   }

constructor(A a,B b,C c,D d,E e){ //.....   }

这样出现的问题很明显,当你想使用A,B和E作为参数的时候,你不得不填上其他的所有参数,也就是C和D你也必须填上去

好吧,可能多填两个参数你还是可以接受的话,不妨想象一下我们现在有着上百个参数,那么麻烦就大了

简要的总结一下,伸缩构造函数的模式的确起作用了,但是这对于代码编写和阅读仍然有着一定的困难

这样情况下,我们在使用的时候可能会因为参数列表过长,然后不小心相互位置放错而导致程序炸了,这在编译阶段可能看不出来错误

在你面对这样许多参数的情况,有一种方法叫做JavaBeans的模式,这个模式很简单,就是你只需要构造一个含所必需参数的构造函数,其他的选项都使用setter来设置即可,当然你的参数都是private的

这样的模式消灭了伸缩构造带来的烦恼,很简单去实现,而且易于阅读

但是这样的模式也存在着或多或少的问题,因为构造会在多次反复调用中分裂,一个JavaBean 可能在他的构造中是不一致的状态,什么意思呢,就是说你如果使用JavaBean 那么你所构造的类的参数是否完整并不是必须的,而且参数可能之前没有,过一段代码流程你又添加了,这就是不一致性,你所构造的类可能是缺少参数的,但是我们在调用一些方法的时候并不会去检查这些参数的存在性,那么就可能导致问题的出现,debug起来可能也较为困难

还有一个JavaBean模式的问题就是,这一种模式排除了使一个类变成不变的类(Immutable Class)的可能性,而且需要在程序员保重线程安全的部分做出额外的努力

这些缺点呢,我们可以当构造结束时手动地”冰冻“(freezing)这些对象并且不允许被它使用直到它被解冻来减少这些缺点,当然这个方法也有许多问题存在,比如编译器并不能确定你所使用地方法是否被冻结了。

幸运的是,这里有第三种解决方案,既包括了伸缩构造模式的安全性又有JavaBeans模式的可读性。它就是Builder模式,并非直接地创建一个需要的对象,用户先调用一个需要全部必需参数的构造方法,然后得到一个builder对象,接着用户使用类似setter的方法来在builder上设置参数,最后调用build方法来生成对象,这样生成的对象是immutable(不可变的),builder在它所build的类中是一个静态的成员类

这里给出书中的例子

public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
          { calories = val; return this; }
public Builder fat(int val)
          { fat = val; return this; }
public Builder carbohydrate(int val)
          { carbohydrate = val; return this; }
public Builder sodium(int val)
          { sodium = val; return this; }
public NutritionFacts build() {
          return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

从例子中我们可以看出,这一模式就是利用Builder类来初始化参数,设置参数,然后再把自己作为参数传入主类的构造函数中,并且给参数赋值实现对象的建立,注意其中的类似于setter的设置,返回的是this,所以可以使用链式的调用,比如

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();

这样的程序语句易于我们去编写,更重要的是,容易读,Builer模式模拟命名了选项参数可以在Python或者Ada中看到它的踪影

就像一个构造方法一样,一个Builder能够强加不变的性质在它的参数上,build方法可以检查这些不变量,重要的是,它们在复制参数从builder到对象之后会被检查,是在对象的域进行检查。如果有不变量是冲突的,build会抛出一个名为 IllegalStateException的异常,这个方法会提示哪一个不变量是冲突的

在多个参数中强加不变量的另一个方法是使用setter方法设置整个参数组,如果不变量不满足要求,那么setter方法就会抛出一个名为IllegalArgumentException的异常

对于builder来说,一个次要的优点是builder可以拥有多个变量参数。而构造方法只能有一个变量参数,因为builder使用分离的多个方法来设置相应的参数(解释一下,构造方法,或者说方法的变量参数只能是一个 比如 A(int a){},就不能是A(int a1 a2 a3){}这样)

Builder模式十分灵活,一个builder可以被用来build多个对象,builder的参数可以被调整使得对象不同,builder可以自动的补充某个域,在对象生成的时候会自动产生一系列的数字

一个所拥有参数被事先设置的builder构成一个良好的抽象工厂(Abstract Factory),换句话说,我们可以通过这样一个builder来变成一个方法去创建多个对象,为了实现这样的使用方案,我们需要一个类型来表达builder

// A builder for objects of type T
public interface Builder<T> {
     public T build();
}

拥有这一个Builder实例地方法会特别地约束builder的类型参数,这个类型参数使用有界通配符,举个例子,这里有个使用用户提供的Builder实例来创建相对应节点来创建一棵树的方法

Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }

传统的抽象工厂在Java上的实现曾经是一个类的对象,有着newInstance方法,这个方法起到了build方法的作用。这样的用法有着问题,这个newInstance方法呢经常企图调用类的无参构造方法,但这个无参的构造方法可能并不存在,当这个类没有可用的无参构造方法的时候你不会在编译阶段得到一个error,那么应对这个问题我们使捕获InstantiationException或者IllegalAccessException来解决,但是这样太丑了而且不方便。Class.newInstance 破坏了编译阶段exception的检查,使用Builder接口就可以解决这些缺陷

当然Builder模式也是有缺点的,创建一个类的时候你必须先创建builder,你必须确定一下创建一个builder的代价开销,在某些情况下可能是个重要的问题。当然builder对于伸缩构造模式来说更为详细,它只创建你需要的参数下的对象,当然参数足够多建议使用builder,否则可能没有什么意义,如果你的参数有4个或者更多而且后期可能继续添加,请第一时间想到使用builder模式作为类编写的开始。

总结,Builder模式当我们设计一个有着许多需要处理的参数的类的时候是一个好的选择,特别是其中的许多参数都是可选的,我们的代码使用builders比使用传统伸缩构造模式更加易于读和写,比起JavaBeans更加安全。

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

推荐阅读更多精彩内容

  • 文章作者:Tyan博客:noahsnail.com Item 2: Consider a builder when...
    SnailTyan阅读 672评论 0 1
  • 对象的创建与销毁 Item 1: 使用static工厂方法,而不是构造函数创建对象:仅仅是创建对象的方法,并非Fa...
    孙小磊阅读 1,957评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 给叶子换上翅膀 缓缓而起,御风而行 经过深秋冷雨 和河面初成的薄冰 星雪轻抚眼角风尘 枯枝招摇慵懒的冬 案头一本 ...
    陶墨墨阅读 376评论 1 4
  • 我正在细读一本书 里面的文字 是华丽的语言 不可修改 惟一的 里面的图片 是美丽的瞬间 不可复制 永恒的 当我读到...
    天枰霜降918阅读 298评论 4 2