Effective Java之builder模式

Posted by BY Blog on December 26, 2018

effective java之builder模式

在实际业务中,往往会遇到一个类包含很多成员变量的情况,因此在实例化该类时,要么需要包含不同参数的构造函数,要么需要在默认构造函数的基础上调用多个setter方法为该实例的各成员赋值。

针对第一种情况,当参数的个数很多,构造函数的参数相应会很多,这样将导致在客户端调用的时候 构造函数参数表意不明的情况;对于第二种情况,是在创建实例后调用setter函数对成员变量赋值,虽然在客户端上可读性变强,但在多线程下,将不能保证各个各setter操作的原子性。

这里对第一、第二种情况分别进行说明,最后介绍Builder模式,及lombok的@Builder大法。

1. 使用多个构造函数
@Data
public class Builder {
    private String param1;
    private String param2;
    private String param3;
    private String param4;

    public Builder() {}
    
    public Builder(String param1) {
        this.param1 = param1;
    }

    public Builder(String param1, String param2) {
        this.param1 = param1;
        this.param2 = param2;
    }

    // 三个参数的构造函数
    // 四个参数的构造函数
}

// 调用时
public static void main(String[] args) {
    Builder builder = new Builder("xixi");
    // 其他生成实例方式
    Builder builderOne = new Builder("xixi","haha","xixi", "haha");
}

以上面这个Builder类为例,它包含四个参数,且每个参数都是String类型,所以在这里写了五个构造函数,适应对不同成员变量赋值的情形。但是,在这种情况下存在以下明显的缺点:

  • 含一个参数的构造函数只能对param1(其中一个参数赋值),不能对任意参数赋值;
  • 含两个参数的构造函数只能对其中两个成员变量赋值,不能对任意两个成员变量赋值,…;
  • 在参数很多的时候,根本分不清每个参数的意义(表意不明),客户端也不方便调用,或者调用的时候极易出现赋值错误的情况。
2. 使用setter函数
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Builder {
    private String param1;
    private String param2;
    private String param3;
    private String param4;
}

    // 调用时
    public static void main(String[] args) {
        Builder builder = new Builder();
        builder.setParam1("xixi");
        builder.setParam2("haha");
        builder.setParam2("xixi");
        builder.setParam2("haha");
    }

上述的这种方式,虽然可读性变强,但随着参数个数越多,调用的setter函数也就越多,看着不太优雅;且这样是线程不安全的,存在多个线程同时修改一个实例参数的情况。

3. Builder模式的原理
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class OuterClass {
    // 成员变量
    private String param1;
    private String param2;
    private String param3;
    private String param4;
    // 1.内部静态类Builder
    public static class Builder {
        // 2.Builder类有与OuterClass相同的成员变量
        private String param1;
        private String param2;
        private String param3;
        private String param4;

        // 3.Builder默认构造函数
        public Builder() {}
        // 还可以有其他构造函数
        public Builder(String param1, String param2) {
            this.param1 = param1;
            this.param2 = param2;
            // 对其他参数设默认值
            this.param3 = "xixi";
            this.param4 = "haha";
        }
        
        // 4.对不同的成员变量赋值的函数,并返回Builder对象实例
        public Builder param1(String param1) {
            this.param1 = param1;
            return this;
        }

        public Builder param2(String param2) {
            this.param2 = param2;
            return this;
        }

        public Builder param3(String param3) {
            this.param3 = param3;
            return this;
        }

        public Builder param4(String param4) {
            this.param4 = param4;
            return this;
        }

        // 5.build函数返回一个OuterClass实例
        public OuterClass build() {
            return new OuterClass(this);
        }
    }
    
    // 6.OuterClass的构造函数,以内部静态类Builder为参数
    public OuterClass(Builder builder) {
        this.param1 = builder.param1;
        this.param2 = builder.param2;
        this.param3 = builder.param3;
        this.param4 = builder.param4;
    }
}

从上面看出,Builder模式的几个关键点在于:

  • 拥有一个静态内部类Builder,其包含于OuterClass相同的成员变量;
  • 内部类包含构造函数;
  • 内部类拥有可以设置其各成员变量的函数,且这些函数返回内部类实例;
  • 内部类包含build函数,返回一个OuterClass实例;
  • OuterClass包含一个构造函数,其参数为内部类实例。

生成OuterClass实例

	// 调用一下OuterClass
    public static void main(String[] args) {
        OuterClass bean = new OuterClass.Builder().param1("xixi")
                .param2("haha")
                .param3("xxxxx")
                .param4("hhhhhhh")
                .build();

        // 输出:OuterClass(param1=xixi, param2=haha, param3=xxxxx, param4=hhhhhhh)
        System.out.println(bean.toString());

        OuterClass bean2 = new OuterClass.Builder("xixi", "haha")
                .param3("xxxxxxxxxxxx")
                .build();
        // 输出: OuterClass(param1=xixi, param2=haha, param3=xxxxxxxxxxxx, param4=haha)
        System.out.println(bean2.toString());
    }

这样一来,我们就可以通过上面的方法来生成OuterClass实例,且可以根据需要随意的设置其中的参数,解决了

12 节的问题。但是Builder模式同样有不足:

  • 为了创建对象,必须先创建它的构建器(存在开销);
  • 代码变得更冗长了。

但是,在参数很多或者字段扩张厉害的情况下,在一开始使用构建器却是不错的选择。

4. lombok @Builder注解的使用

如果要使用构建器,我们不需要自己去编写额外的代码,lombok的@Builder注解能帮我们实现这个功能。其使用如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class OuterClass {
    private String param1;
    private String param2;
    private String param3;
    private String param4;

    // 调用一下OuterClass
    public static void main(String[] args) {
        OuterClass bean = OuterClass.builder()
                .param1("xixi")
                .param2("haha")
                .param3("xxxxxxxx")
                .build();

        // 输出:OuterClass(param1=xixi, param2=haha, param3=xxxxxxxx, param4=null)
        System.out.println(bean);
    }
参考链接

lombok大法的一些使用:https://www.jianshu.com/p/2ea9ff98f7d6

github完整代码示例:https://github.com/HorizonLiu/effective.java.learning