教你用构建者(生成器)模式优雅地创建对象

为什么要用构建者模式

很多博客文章上来就先抛出一个定义,我们不妨反过来问一句为什么要用构建者模式。
首先我们创建一个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;
}

StringBuilderappend()方法也是通过返回this对象实现链式构建对象,人们经常说这个StringBuilder类线程不安全是因为append()方法没有用synchronized修饰。StringBuffer则用了synchronized修饰,所以就是线程安全的。
还有Mybatis框架中,构建SqlSessionFactory对象是使用SqlSessionFactoryBuilder类进行构建,构建者模式运用非常广泛,非常值得学习。更多的设计模式实战经验的分享,就关注java技术爱好者吧。
微信公众号二维码

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

你可能感兴趣的:(设计模式)