effective java 3th item2:考虑 builder 模式,当构造器参数过多的时候
yiaz 读书笔记,翻译于 effective java 3th 英文版,可能有些地方有错误。欢迎指正。
静态工厂方法和构造器都有一个限制:当有许多参数的时候,它们不能很好的扩展。
比如试想下如下场景:考虑使用一个类表示食品包装袋上的营养成分标签。这些标签只有几个是必须的——每份的含量、每罐的含量、每份的卡路里,除了这几个必选的,还有超过 20
个可选的标签——总脂肪量、饱和脂肪量等等。对于这些可选的标签,大部分产品一般都只有几个标签的有值,不是每一个标签都用到。
(
telescoping constructor
)重叠构造器模式对于这种情况,你应该选择哪种构造器或者静态工厂方法。一般程序员的习惯是采用 (
telescoping constructor
)重叠构造器模式。在这种模式中,提供一个包含必选参数的构造器,再提供其他一些列包含可选参数的构造器,第一个包含一个可以参数、第二个包含两个可选参数,以此类推下去,直到包含所有的可选参数。示例代码:
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
} public NutritionFacts(int servingSize, int servings,int calories) {
this(servingSize, servings, calories, 0);
} public NutritionFacts(int servingSize, int servings,int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
} public NutritionFacts(int servingSize, int servings,int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
} public NutritionFacts(int servingSize, int servings,int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}当你想创建一个实例的时候,你只需要找包含你需要的并且是最短参数列表的构造器即可。
这里有一些问题,比如看下面的代码:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 0, 0, 35, 27);
其中,第 1,2 个可选参数,我们是不需要的,但是程序中没有提供直接赋值第 3,4个可选参数的构造器,因此,我们只能选择包含了 1,2,3,4 个参数的构造器。这里面要求了许多你不想设置的参数,但是你又被迫的设置它们,在这里,传入对应的属性的默认值 0。并且这种模式,随着参数的增加,将变得越来越难以忍受,无论是编写程序的人,还是调用程序的人。
总而言之,(
telescoping constructor
)重叠构造器模式,可以使用,但是它对客户端来说,很不友好,写和读都是一件困难的事情。它们很难搞懂那些参数对应的到底是什么属性,必须好好的比对构造器代码。并且当参数很多的时候,很容易出bug
,如果使用的时候,无意间颠倒了两个参数的位置,编译器是不会出现警告的,因为这里的类型一样,都是int
,直到运行的时候才会暴露出。Javabeans 模式
我们还有一种选择,使用 Javabeans 模式 。
在此模式中,我们提供一个 无参构造器 创建实例,然后利用
setXXX
方法,设置每一个必须的属性和每一个需要的可选属性。示例代码:
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0; public NutritionFacts() { } // Setters
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
Javabeans
模式,没有 重叠构造器模式 的缺点,对于冗长的参数,使用它创建对象,会很容易,同时读起来也是容易。正如下面看到的,我们可以清晰的看到,每一个属性的值。NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸运的是,Javabeans模式 本身有着严重的缺点:因为,创建对象被分割为多个步骤,先是利用无参构造器创建对象,然后再依次设置属性。这导致一个问题: Javabean 在其创建过程中,可能处于不一致[1]的状态。 类不能通过检查构造器的参数,来保证对象的一致性。
另外一个缺点是,将创建一个可变的类的难度提高了好几个级别,因为有
setXXX
方法的存在。可以通过一些手段来减少不一致的问题,通过一些手段 冻结 对象,在对象被创建完成之前。并且不允许使用该对象,直到 解冻 。但是这种方式非常笨拙,在实践中很少使用。因为,编译器无法确认程序员在使用一个对象之前,该对象是否已经 解冻 。
Builder
模式幸运的是,这里还有一种方法
Builder
模式,兼顾 重叠构造器 的安全以及 Javabean模式 的可读性。客户端先通过调用构造器或者静态工厂方法,传入必须的参数,获得一个
builder
对象,代替直接获取目标对象。然后客户端在该builder
对象上调用setXXX
方法,为每一个感兴趣的可选属性赋值,最后客户端调用一个 无参构造器 生成最终的目标对象,该对象一般是不可变的。其中Builder
类是目标类的静态内部类示例代码:
// Builder Pattern
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 sodium = 0;
private int carbohydrate = 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 sodium(int val)
{
sodium = val;
return this;
}
public Builder carbohydrate(int val)
{
carbohydrate = 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;
}
}
其中
NutritionFacts
类为不可变类,类的成员变量全部被final
修饰,参数的默认值被放在一个地方。Builder
类setXXX
方法返回Builder
本身,这种写法,可以将设置变成一个链,一直点下去(fluent APIs
):NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
这样的客户端代码,容易编写,更容易阅读。
示例代码中,为了简洁,省去了有效性的检查。一般,为了尽快的检查到非法参数,我们在
builder
的构造器和方法中,对其参数进行检查。还需要检查
build
方法中调用的构造器的多个不可变参数[2]。这次检查延迟到object
中,为了确保这些不可变参数不受到攻击,在builder
将属性复制到object
中的时候,再做一次检查。如果检验失败,则抛出IllegalArgumentException
异常,异常信息中提示哪些参数不合法。Bulider
模式很适合类的层次结构。可以使用一个builder
的平行结构,即每一个builder
嵌套在一个对应的类中,抽象类中有抽象的builder
,具体类中有具体的builder
。像下面的代码所示:// Builder pattern for class hierarchies
abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
} final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
} abstract Pizza build(); // Subclasses must override this method to return "this"
protected abstract T self();
} Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // See Item 50
}
} class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE} private final Size size; public static class Builder extends Pizza.Builder<Builder> {
private final Size size; public Builder(Size size) {
this.size = Objects.requireNonNull(size);
} @Override
public NyPizza build() {
return new NyPizza(this);
} @Override
protected Builder self() {
return this;
}
} private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
} class Calzone extends Pizza {
private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default public Builder sauceInside() {
sauceInside = true;
return this;
} @Override
public Calzone build() {
return new Calzone(this);
} @Override
protected Builder self() {
return this;
}
} private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
注意,这里的
Pizza.Builder
是类属性,被static
修饰的,并且泛型参数,是一个 递归 的泛型参数,继承本身。和返回自身的抽象方法self
,搭配一起,可以链式的调用下去,不需要进行类型的转换,这样做的原因是,java
不直接支持 自类型 [3],可以模拟自类型 [4]。如果不使用模拟自类型的话,调用
addTopping
方法,返回的其实就是抽象类中的Builder
,这样就导致无法调用子类扩展方法,无法使用fluent APIS
。其中build
方法,使用了1.5
添加的 协变类型 ,它可以不用cast
转换,就直接使用具体的类型,否则子类接收父类,是需要强转的 。builder
模式另外一个小优点:builder
可以有多个 可变参数,因为,可以将多个可变参数,放到各自对应的方法中[5]。另外build
可以将多个参数合并到一个字段上,就如上面代码中addTopping
的那样[6]。builder
模式是非常灵活的。一个单一的builder
多次调用,可以创建出不同的对象[7]。builder
的参数,可以在调用build
方法的时候进行细微调整,以便修改创建出的对象[7:1]。builder
模式还可以自动的填充object
域的字段在创建对象的时候。比如为每个新创建的对象设置编号,只需要在builder
中维护一个类变量即可。builder
模式也是有缺点的。为了创建一个对象,我们首先需要创建它的builder
对象。虽然,创建builder
对象的开销,在实践中不是很明显,但是在对性能要求很严格的场景下,这种开销能会成为一个问题。同时,builder
模式是非常冗杂的,对于比 重叠构造器 ,所以,builder
模式应该仅仅被用在构造器参数足够多的情况下,比如三个、四个或者更多,只有这样,使用builder
模式才是值得的。但是,你要时刻记住,类在将来可能会添加新的参数,如果你一开始使用了构造器或者静态工厂方法,随着类的变化,类的属性参数变得足够多,这时候你再切换到builder
模式,那么一开始的构造器和静态工厂方法就会被废弃,这些废弃的方法看起来很凸出,你还不能删除它们,需要保存兼容性。因此,一般一开始就选择builder
模式是一个不错的选择。总结,
builder
模式是一个好的选择,当设计一个类的时候,该类的构造器参数或者静态工厂参数不止几个参数,尤其是许多参数是可选的或者同一个类型(可变参数)。这样设计的类,客户端代码,与静态工厂方法和重叠构造器比起来更加容易阅读和编写,和Javabeans
模式比起来更加安全。
不一致的意思:正常对象的创建应该是一个完整的过程,这个过程控制在构造器中,可以看做是一个 原子性 的操作。它在对象创建出来以后,对象的各项属性已经被正确的初始化。但是 Javabean 模式,天生的背弃了这个原则,它的创建对象,不是一个 原子性 的操作,在构造器执行完毕以后,还有一些列的属性赋值,在这期间任何引用该对象的地方,都将获得一个不正确的对象,直到对象创建完毕。可以参考下 JavaBean disadvantage - inconsistent during construction 这里还提到了重复错误对象的创建。 ↩︎
我理解为构造器所在类的不可变属性,在
builder
中的检查类似于前台页面字段的合法性检查,最后后台(Object
)都要再次检查一遍。 ↩︎自类型 。在支持自类型的语言中,
this
或者self
的语义,谁调用该方法,则this
代表谁。但是在java
中,方法中的this
指代的是定义该方法的类型,与调用无关,导致无法很好的使用fluent API
。可参考 java 的 self 类型。你可以验证下,打印控制台看,类型确实是调用它的类型,但是你等号左边用这个类型去接收,会提示你发现父类型,不能赋值给子类型,不知道java
在这里面做了什么。 ↩︎模拟自类型,这里在抽象类中,使用泛型指定,避免使用指定的类型,导致
this
被绑定为具体的。 ↩︎构造器在创建对象的时候,构造器和普通方法一样,只能接受一个 可变参数 。但是
builder
模式,可以多次调用不同的方法,添加 可变参数,直到所有的可变参数全部添加完毕,再build
创建对象。 ↩︎同样的,构造器无法做的原因是,构造器一经调用,对象就会被创建,也就是创建对象的过程中,只可以调用一次构造器。但是
builder
模式可以多次调用方法,设置参数,直到最后全部添加完毕,调用build
创建对象。 ↩︎还是因为
builder
模式,只有在调用build
方法,对象才会被创建,在创建之前,可以在调用builder
模式的方法,修改参数,创建出不同的对象。 可以参考下StackOverflow
的回答: A single builder can be used repeatedly to build multiple objects ↩︎ ↩︎
effective java 3th item2:考虑 builder 模式,当构造器参数过多的时候的更多相关文章
- effective java 3th 序
正本基本是自己翻译,翻译绝对有错误,就是这么自信,看的时候,自己注意下,如果感觉有语句不通,那么可能就是我翻译的出现了问题,可以自己翻找原文对比下. 其中自己的见解,我写在脚注中. 在 1997 年, ...
- Effective Java 02 Consider a builder when faced with many constructor parameters
Advantage It simulates named optional parameters which is easily used to client API. Detect the inva ...
- effective java 3th item1:考虑静态工厂方法代替构造器
传统的方式获取一个类的实例,是通过提供一个 public 构造器.这里有技巧,每一个程序员应该记住.一个类可以对外提供一个 public 的 静态工厂方法 ,该方法只是一个朴素的静态方法,不需要有太多 ...
- (零)引言——关于effective Java 3th
去年4月份那时候,读过本书的第二版本,那时候寻思着好好读完,但是事与愿违,没有读完! 现在起,寻思着再次开始读吧: 现在第三版也出版了,还有第二版的翻译问题,遂决定读第三版的英文版吧: PDF版本可以 ...
- java的设计模式 - Builder模式
Builder 模式的目的? 构造对象的方式过于复杂,不如将之抽离出来.比如,构造器参数过多 这样说也有点抽象,举个例子吧. 举个例子 比如 非常热门的消息队列RabbitMQ 的 AMQP.Basi ...
- Effective Java Index
Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...
- Effective Java学习笔记
创建和销毁对象 第一条:考虑用静态工厂方法替代构造器 For example: public static Boolean valueOf(boolean b){ return b ? Boolean ...
- Effective Java 读书笔记
创建和销毁对象 >考虑用静态工厂方法替代构造器. 优点: ●优势在于有名称. ●不必再每次调用他们的时候都创建一个新的对象. ●可以返回原返回类型的任何子类型的对象. ●在创建参数化类型实例的时 ...
- Builder模式的思考(Effective Java)
<Effective Java>(第2版)中第二条中提到:遇到多个构造器参数时要考虑用构建器.在复习static关键字和内部类时回头看了一下,这才明白了为什么要用静态内部类来做处理,这里记 ...
随机推荐
- .net RabbitMQ 介绍、安装、运行
RabbitMQ介绍 什么是MQ Message Queue(简称:MQ),消息队列 顾名思义将内容存入到队列中,存入取出的原则是先进先出.后进后出. 其主要用途:不同进程Process/线程Thre ...
- 8 NLP-自然语言处理Demo
1 NLP(自然语言处理) 1.1相似度 相似度和距离之间关系: 1.文本相似度: 1) 语义相似.但字面不相似: 老王的个人简介 铁王人物介绍 2) 字面相似.但是语义不相似: 我吃饱饭了 我吃不饱 ...
- java常见面试题目(一)
在大四实习阶段,秋招的时候,面试了很多家公司,总结常见的java面试题目:(答案可以自己百度) 1.你所用oracle的版本号是多少? 2.tomcat修改8080端口号的配置文件是哪个? 3.myb ...
- 初试kafka消息队列中间件二(采用java代码收发消息)
初试kafka消息队列中间件二(采用java代码收发消息) 上一篇 初试kafka消息队列中间件一 今天的案例主要是将采用命令行收发信息改成使用java代码实现,根据上一篇的接着写: 先启动Zooke ...
- 08_代码块丶继承和final
Day07笔记 课程内容 1.封装 2.静态 3.工具类 4.Arrays工具类 封装 概述 1.封装:隐藏事物的属性和实现细节,对外提供公共的访问方式 2.封装的好处: 隐藏了事物的实现细节 提高了 ...
- 自定义markdown代码高亮显示-cnblog
这个代码高亮..一点儿都不高亮...... cnblog里已经有闻道先者贴出代码了, https://www.cnblogs.com/liutongqing/p/7745413.html 效果大概是这 ...
- 3月1日 大型网站系统与Java中间件实践 读后感
第二章:大型网站以及架构演进过程 db和应用服务器在一台机器上 数据库与应用分离 服务器走向集群,负载均衡,session问题 读写分离:数据复制,数据源的选择,搜索引擎其实就是一个读库,缓存(数据缓 ...
- 简单认识Nginx---负载均衡
中大型项目都会考虑到分布式,前面几篇文章着重介绍了数据处理的技术集群.今天来研究一下关于服务器的负载均衡–Nginx.他除了静态资源的处理外还有可以决定将请求置于那台服务上. Nginx的安装 点我下 ...
- 洛谷 P2157 [SDOI2009]学校食堂
题意简述 每个人有一个口味,食堂每次只能为一个人做菜 做每道菜所需的时间是和前一道菜有关的,若前一道菜的对应的口味是a,这一道为b,则做这道菜所需的时间为a 异或 b 每个人都有一个容忍度,最多允许紧 ...
- Windows Server 2008利用NTFS管理数据
今天我们学习关于NTFS管理数据 以下是学习的内容NTFS分区和FAT32分区的区别,如何将FAT32分区转化成NTFS分区,FAT 32 不支持大于4G ,NTFS权限设置 ,EFS加密 ,文件夹的 ...