[Effective Java Distilled] Item 2 当构造方法中有多个参数时,考虑建造者模式(一)

2014-11-24 11:20:11 · 作者: · 浏览: 15
关于Effective Java Distilled:
《Effective Java》这本书我断断续续的读了近两遍,里面的内容挺有深度,对提高工程代码质量也非常有帮助。我打算慢慢的整理出来一个系列,之所以命名为Effective Java Distilled,也是想将本书的精华尽可能的整理出来,方便复习查阅使用。毕竟自己的记忆力也很有限,很多东西经常忘记。当然,如果能给大家带来什么帮助,那就更好了。
本文提纲:
参数过多对静态工厂方法和构造方法的影响
JavaBean模式及其弊端
建造者模式的运用
泛化建造者模式
实际工程中往往有一些实体类,这些实体类通常都会拥有数量不等的域用来保存各种相关状态。有一些状态必须有值,而有一些则是可选状态。对于这类实体的实例化,通常都通过一个包含了所有必选状态的构造方法来完成,同时对其他可选状态提供一些setters方法,用于在初始化后进行可选状态的设置。这种模式已经逐渐成为一种约定,在很多框架中都会被使用。比如Hibernate这种ORM框架,从 数据库表中获取记录时,将记录通过这种形式转换成 Java对象。
一旦习惯这种方式,其实也不会觉得此方式有什么不合适的地方。但是还需要考虑没有框架支持的情况,如果需要通过自己编写代码来完成很多个对象的实例化,以上这种构造方法结合setters的初始化方式无疑还有很大的提升空间。
参数过多对静态工厂方法和构造方法的影响
首先,关于静态工厂方法的概念,可以参见Item 1中的内容。
参数过多,特别是可选参数过多的情况,对静态工厂方法和构造方法都有不利影响。
比如现在有5个参数,其中a,b是必选参数,c,d,e是可选参数,那么为了创建此对象,在不考虑使用setters的情况下,需要的构造方法(静态工厂方法)的数量为:1 + 3 + 3 + 1 = 8个,具体如下:
只带有必选参数的数量:1,即constructor(a, b)
带有一个可选参数的数量:3,即constructor(a, b, c) constructor(a, b, d) 和 constructor(a, b, e)
带有两个可选参数的数量:3,即constructor(a, b, c, d) constructor(a, b, c, e) 和 constructor(a,b, d, e)
带有全部可选参数的数量:1,即constructor(a, b, c, d, e)
Telescoping Constructor模式
以上这种声明构造方法(或者静态工厂方法)的模式被称为Telescoping Constructor模式,即先带有必要参数,然后逐渐增加可选参数的数量,直到拥有全部参数。
这种模式能够起作用,但是弊端也不少,首当其冲的就是构造方法规模过大。当仅有5个参数,其中3个可选的情况下,就需要要多达8个构造方法(或者静态工厂方法),那么当可选参数的数量继续增加时,需要的构造方法的数量会呈现爆发式增长的趋势。另外,当可选参数的类型都相同时,容易将参数的位置弄错。而这种错误可谓是防不胜防,因为类型一样,编译的时候不会出现错误,而在运行时则会出现各种莫名其妙的问题。
JavaBean模式及其弊端
主要思想就是只提供一个无参构造方法,然后通过setters方法来设置必要的域和可选的域。
它的主要弊端包括:
代码冗长,当需要设置的域很多时,更加明显
这是因为每一个field都需要显式调用它的setter方法。
在并发环境中可能出现的不一致性
正是由于每个field都需要调用setter方法来设置其值,所以在不采用同步机制的情况下,无法保证对所有的fields的设置为原子操作,当然,可以通过各种同步机制来保证设置操作的原子性,但是这又会牺牲一部分性能。
无法将该实体类声明为不可变类(Immutable Class)
这是因为在类中引入了setters,这违背了不可变类的基本要求,即在对象创建之后,对象中的任何值都不再可变。可变类在单线程环境中几乎没有风险,但是在并发环境下,可能存在各种风险。这个在后续并发相关的Items中会讨论。
setters方法可能破坏不变性约束(Invariants)
如果实体类中的几个域之间有不变性约束关系,使用setters方法来赋值的时候,可能会造成该约束破坏,从而给程序运行带来隐患。不变性约束的遵循在并发环境中十分重要,此概念在后序并发相关的Items中会讨论到。
建造者模式的运用
无论是Telescoping Constructor模式还是JavaBean模式,使用它们的原因很大程度上是因为Java仅仅支持定位参数(Positional Parameters),而不支持命名参数(Named Parameters)。在其他语言,比如 Python中,这两种模式都被支持,典型的Python方法签名:def method(*args, **kwargs) 非常明显的体现了这一点,其中args代表了位置参数,而kwargs则是命名参数。很可惜,Java没有内置的语言特性能够支持命名参数,但是通过使用建造者模式,可以模仿这一行为:
直接引用Effective Java中关于营养成分的例子:
[java]
// 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 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)