很多博客文章上来就先抛出一个定义,我们不妨反过来问一句为什么要用构建者模式。
首先我们创建一个User
类,然后采用有参构造器的方式创建对象。
public class User {
private String id;
private String name;
private String gender;
private String address;
private Integer age;
private String phone;
//省略无参构造器,有参构造器,getter,setter方法...
}
public static void main(String[] args) throws Exception {
String id = UUID.randomUUID().toString().replaceAll("-", "");
User user = new User(id, "张三", "男", "广州天河", 20, "135461852xx");
}
我们通过有参构造器创建对象,并且赋值,看起来没什么问题,因为我们经常看到有人是这样写的。
事实上,如果User
对象里面有更多的字段,通过有参构造器去创建对象是很难一眼看出字段具体是什么意思,我们经常要看着User构造器
的代码,然后对照顺序才能看出字段的代表什么意思。
public User(String id, String name, String gender, String address, Integer age, String phone) {
this.id = id;
this.name = name;
this.gender = gender;
this.address = address;
this.age = age;
this.phone = phone;
}
比如通过上面这个,我们可以知道第一个参数是id,第二个参数是名字,第三个是性别…
缺点:
这显然不利于代码的维护性,对于不熟悉业务的新入职的员工,如果看到这种方式构建一个对象,估计要看上一会,有些项目我遇过一个构造器十几个参数的,更加离谱。而且一般老代码还不敢乱动他的这个构造器,一不小心你动了构造器里面的一个参数的顺序,直接GG;或者你在他原有的构造器后面加多一个参数,你会发现他很多地方都引用了这个有参构造器,你很多地方都要去修改,是真的恶心。
解决方法1-使用无参构造器,通过setter方法设置属性值
public static void main(String[] args) throws Exception {
User user = new User();
user.setId(UUID.randomUUID().toString().replaceAll("-", ""));
user.setName("张三");
user.setAge(20);
user.setGender("男");
user.setPhone("135461852xx");
user.setAddress("广州天河");
out.println(user);
}
上面这样,显然比直接用有参构造器要好很多,因为这样就可以创建对象和赋值分开进行,一眼就可以看出对什么属性值赋值,而且如果加一个字段,我们不需要再每一处都去修改,因为用的是无参构造器,是不是这样写就是万全之计呢?
也不是,因为这样创建对象和赋值是分开的,各个参数的初始化被放到了不同的方法中调用,这会导致严重的线程不安全问题(使用构造器则不会有这个问题),对象在一连串的set方法中,可能会出现状态不一致的情况,这是应该尽量避免的。
解决方法2-通过构建者模式,链式调用构建方法设置属性值
什么是链式编程,就是调用一个方法,返回值是他本身,可以继续调用下一个方法,返回又是他本身,如此调用下去,看上去就像一条链子一样。典型的例子可以看java8
新特性的Stream
流操作。我们可以使用构建者模式,也能达到这种效果,并且线程安全,而且能直观地看到属性值的意思。总得来说,既保证线程安全,也很具有代码的可读性。先看结果代码:
public static void main(String[] args) throws Exception {
String id = UUID.randomUUID().toString().replaceAll("-", "");
User user = UserBuilder.getInstance()
.newPojo()
.addId(id)
.addName("张三")
.addGender("男")
.addAge(20)
.addPhone("135461852xx")
.addAddress("广州天河")
.build();
}
怎么实现呢?其实很简单,我们只需要创建一个UserBuilder
类即可。代码如下:
public class UserBuilder {
private User user;
private UserBuilder() {
}
public static UserBuilder getInstance() {
return new UserBuilder();
}
public UserBuilder newPojo() {
this.user = new User();
//返回本身
return this;
}
public UserBuilder addId(String id) {
this.user.setId(id);
//返回本身
return this;
}
public UserBuilder addName(String name) {
this.user.setName(name);
return this;
}
public UserBuilder addGender(String gender) {
this.user.setGender(gender);
return this;
}
public UserBuilder addAge(Integer age) {
this.user.setAge(age);
return this;
}
public UserBuilder addAddress(String address) {
this.user.setAddress(address);
return this;
}
public UserBuilder addPhone(String phone) {
this.user.setPhone(phone);
return this;
}
public User build() {
return this.user;
}
}
那么是不是这种方式就是万全之计呢,就一定没有缺点吗?
缺点还是有的:
1.代码冗长。如果一个对象的属性很多,那我们在创建一个对象时,链式就会变得很长,但是这也没有办法,无论采用构造器还是builder模式都会很长。如果非要变得简洁一点,那就只有采用原型模式(克隆)等其他方式了。
2.会产生很多Builder
类。我们可以放在一个包下统一管理应该问题不大。
第二个缺点实际上可以使用Lombok
插件,然后在实体类上使用@Builder
注解,就不会产生过多的Builder
类了。但是有些公司的技术总监不太建议使用Lombok
,那就莫得办法了…
注意点:
有很多博客的示范代码,Builder
类的addXXX
方法会写成setXXX
方法,这是一个隐患。因为很多框架,对Setter方法比较敏感,往往会对Setter
方法做一些处理,所以Builder
类里的设置属性值方法尽量不要用setXXX
命名,防止出现一些不明原因的错误。
一般我们在项目中创建复杂的对象时,建议采用这种构建者模式创建对象。这样可以使代码可读性更好。
在java源码中,我们也可以看到构建者模式的应用。比如在StringBuilder
类中:
@Override
public StringBuilder append(CharSequence s) {
super.append(s);
return this;
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
@Override
public StringBuilder append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}
@Override
public StringBuilder append(char[] str) {
super.append(str);
return this;
}
StringBuilder
的append()
方法也是通过返回this
对象实现链式构建对象,人们经常说这个StringBuilder
类线程不安全是因为append()
方法没有用synchronized
修饰。StringBuffer
则用了synchronized
修饰,所以就是线程安全的。
还有Mybatis
框架中,构建SqlSessionFactory
对象是使用SqlSessionFactoryBuilder
类进行构建,构建者模式运用非常广泛,非常值得学习。更多的设计模式实战经验的分享,就关注java技术爱好者吧。
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!