摘要:
当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数,也就是说,灵活性很差。那么,对于这样的类,我们应该如何创建对象呢?本文列举了三种解决办法:重叠构造器模式、JavaBeans模式和Builder模式,并通过具体实例对上述三种方法进行铺垫和对比,从而真正帮助读者理解Builder模式。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
当我们需要创建一个复杂的对象时,使用静态工厂或者构造器的方式就显得特别笨拙和丑陋,因为它们有个共同的局限性:它们都不能很好地扩展到大量的可选参数。考虑用一个Person类来描述一个人,除了姓名,性别,生日,邮箱等必要的属性外,还有很多可选的属性,比如:身高,学历,绰号,体重,通讯地址等等。对于这样的类,我们应该如何创建对象呢?无论是常见的重叠构造器模式还是JavaBeans模式,它们都不能很好地解决这类问题,而我们本文所着重阐述的Builder模式则正好是解决此类问题的利剑。为了更深入的了解Builder模式所带来的好处,我们先分别采用重叠构造器模式和JavaBeans模式来解决上述问题。
在这种模式下,我们提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器含有所有参数,如下所示:
public class Person {
private String name; // required
private String sex; // required
private Date date; // required
private String email; // required
private int height; // optional
private String edu; // optional
private String nickName; // optional
private int weight; // optional
private String addr; // optional
public Person(String name, String sex, Date date, String email) {
this(name, sex, date, email, 0);
}
public Person(String name, String sex, Date date, String email, int height) {
this(name, sex, date, email, height, null);
}
public Person(String name, String sex, Date date, String email, int height, String edu) {
this(name, sex, date, email, height, edu, null);
}
public Person(String name, String sex, Date date, String email, int height, String edu, String nickName) {
this(name, sex, date, email, height, edu, nickName, 0);
}
public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
weight) {
this(name, sex, date, email, height, edu, nickName, weight, null);
}
public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
weight, String addr) {
this.name = name;
this.sex = sex;
this.date = date;
this.email = email;
this.height = height;
this.edu = edu;
this.nickName = nickName;
this.weight = weight;
this.addr = addr;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", date=" + date +
", email='" + email + '\'' +
", height=" + height +
", edu='" + edu + '\'' +
", nickName='" + nickName + '\'' +
", weight=" + weight +
", addr='" + addr + '\'' +
'}';
}
}
使用这种模式创建对象时,存在一下几点不足:
public Person(String name, String sex, Date date, String email, int height, String edu, String nickName, int
weight) {
this(name, sex, date, email, height, edu, nickName, weight, null);
}
这时,我们可能转而求助于JavaBeans模式来避免这些问题,但是同来也会带来一些新的问题。同样的例子,若我们采用JavaBeans模式,那么代码将会是如下的样子:
public class Person {
private String name; // required
private String sex; // required
private Date date; // required
private String email; // required
private int height; // optional
private String edu; // optional
private String nickName; // optional
private int weight; // optional
private String addr; // optional
public void setName(String name) {
this.name = name;
}
public void setSex(String sex) {
this.sex = sex;
}
public void setDate(Date date) {
this.date = date;
}
public void setEmail(String email) {
this.email = email;
}
public void setHeight(int height) {
this.height = height;
}
public void setEdu(String edu) {
this.edu = edu;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public void setWeight(int weight) {
this.weight = weight;
}
public void setAddr(String addr) {
this.addr = addr;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", date=" + date +
", email='" + email + '\'' +
", height=" + height +
", edu='" + edu + '\'' +
", nickName='" + nickName + '\'' +
", weight=" + weight +
", addr='" + addr + '\'' +
'}';
}
}
这种方式虽然保证了灵活性,也不易出错,例如:
Person p2 = new Person();
p2.setName("livia");
p2.setSex("girl");
p2.setDate(new Date());
p2.setEmail("livia@tju.edu.cn");
p2.setHeight(163);
p2.setEdu("NCU");
p2.setNickName("pig");
p2.setWeight(100);
p2.setAddr("北京市");
System.out.println(p2);
但是其本身也存在这一些固有的缺点,比如:
Setter的存在妨碍了其成为不可变类的可能:这样,在并发环境下,我们就不得不考虑其线程安全性;
代码丑陋且对象易处于不一致状态:上面创建对象的方式也比较丑陋,同时由于对象的构造过程分为若干个函数调用,所以容易导致对象处于不一致状态。
使用Builder模式创建复杂对象,不但可以避免上述两种方式的缺点,而且还能兼顾们各自的优点。该模式的内涵是:不直接生成想要的对象,而是让客户端利用 所有必要的参数 构造一个Builder对象,然后在此基础上,调用类似于Setter的方法来设置每个可选参数,最后通过调用无参的build()方法来生成不可变对象。一般地,所属Builder是它所构建类的静态成员类。代码如下:
public class Person {
private final String name; // required
private final String sex; // required
private final Date date; // required
private final String email; // required
private final int height; // optional
private final String edu; // optional
private final String nickName; // optional
private final int weight; // optional
private final String addr; // optional
// 私有构造器,因此Person对象的创建必须依赖于Builder
private Person(Builder builder) {
this.name = builder.name;
this.sex = builder.sex;
this.date = builder.date;
this.email = builder.email;
this.height = builder.height;
this.edu = builder.edu;
this.nickName = builder.nickName;
this.weight = builder.weight;
this.addr = builder.addr;
}
public static class Builder{
private final String name; // required,使用final修饰
private final String sex; // required,使用final修饰
private final Date date; // required,使用final修饰
private final String email; // required,使用final修饰
private int height; // optional,不使用final修饰
private String edu; // optional,不使用final修饰
private String nickName; // optional,不使用final修饰
private int weight; // optional,不使用final修饰
private String addr; // optional,不使用final修饰
public Builder(String name, String sex, Date date, String email) {
this.name = name;
this.sex = sex;
this.date = date;
this.email = email;
}
// 返回Builder对象本身,链式调用
public Builder height(int height){
this.height = height;
return this;
}
// 返回Builder对象本身,链式调用
public Builder edu(String edu){
this.edu = edu;
return this;
}
// 返回Builder对象本身,链式调用
public Builder nickName(String nickName){
this.nickName = nickName;
return this;
}
// 返回Builder对象本身,链式调用
public Builder weight(int weight){
this.weight = weight;
return this;
}
// 返回Builder对象本身,链式调用
public Builder addr(String addr){
this.addr = addr;
return this;
}
// 通过Builder构建所需Person对象,并且每次都产生新的Person对象
public Person build(){
return new Person(this);
}
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", date=" + date +
", email='" + email + '\'' +
", height=" + height +
", edu='" + edu + '\'' +
", nickName='" + nickName + '\'' +
", weight=" + weight +
", addr='" + addr + '\'' +
'}';
}
}
我们可以通过下面的方式来创建一个Person对象:
Person.Builder builder = new Person.Builder("rico", "boy", new Date(), "rico@tju.edu.cn");
Person p1 = builder.height(173).addr("天津市").nickName("书呆子").build();
显而易见,使用这种方式创建对象不但灵活而且易于阅读,且不易出错。总的来说,这种模式具有以下特点:
Person类的构造方法是私有的: 也就是说,客户端不能直接创建User对象;
Person类是不可变的: 所有的属性都被final修饰,在构造方法中设置参数值,并且不对外提供setters方法;
Builder模式的高可读性: Builder模式使用了链式调用,可读性更佳。
Builder对象与目标对象的异同: Person与Builder拥有共同的属性,并且Builder内部类构造方法中只接收必传的参数,同时只有这些必传的参数使用了final修饰符。
我们知道,Person对象是不可变的,因此是线程安全的;但是,Builder对象并不具有线程安全性。因此,当我们需要对Person对象的参数强加约束条件时,我们应该可以对builder()方法中所创建出来的Person对象进行检验,即我们可以将builder()方法进行如下重写:
public Person build(){
Person person = new Person(this);
if (!"boy".equals(person.sex)){
throw new IllegalArgumentException("所注册用户必须为男性!");
}else{
return person;
}
}
需要特别注意的是,我们是对Person对象进行参数检查,而不是对Builder对象进行参数检查,因为Builder对象不是线程安全的,即下面的代码存在线程安全问题:
public Person build(){
if (!"boy".equals(this.sex)){
throw new IllegalArgumentException("所注册用户必须为男性!");
}else{
return new Person(this);
}
}
(1). Builder模式的应用场景
对象属性繁多,一般都具有5个或者5个以上的属性,特别是大多数参数都是可选的时候;
(2). Builder模式与重叠构造器模式及JavaBeans模式的对比
与重叠构造器模式相比,使用Builder模式的代码更易阅读和编写,并且也比JavaBeans模式更安全;
(3). 本文所述Builder模式与GOF经典Builder模式的关系
本文所谈的Builder模式可以看作是GOF经典Builder模式的简化版,其省略掉了Director,这样结构更加简单。特别地,在很多框架源码中,涉及到Builder模式的应用大多都不是经典GOF的Builder模式而是本文中所探讨的形式,比如Hibernate中国SessionFactory的创建等等。
由于GOF经典Builder模式在实践中较少使用,故本文不再赘述。
更多关于 Java内部类 的介绍,请移步至笔者 《 Java 内部类综述》一文。
更多关于 并发编程与线程安全问题 的介绍,请移步笔者 《Java并发编程学习笔记》专栏。