转载

Effective Java学习笔记(二)Builder模式

写在前面

本文对应原书条目2,主要探讨的是在一个类的成员变量较多时,如何用一种可读性和可扩展性都更好的方式构建对象。通过 可伸缩构造方法模式 - JavaBeans模式 - Builder模式 的探索,我们可以看到不同模式的利弊,以及为什么大部分情况下Builder模式最适用。本文的示例代码均来自原书第3版,如有版权问题,请联系我删除。

可伸缩构造方法模式

当一个类的成员变量过多时,程序员们最先想到了这个方法。这种模式就像望远镜一样,你可以按需伸展它,直到你需要的长度。首先提供一个仅含必选参数的构造方法,然后提供含一个可选参数的构造方法,接着提供更多一个可选参数的构造方法,直到涵盖所有参数为止。正如以下示例所示。

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
    private final int servingSize;  // (mL)            required
    private final int servings;     // (per container) required
    private final int calories;     // (per serving)   optional
    private final int fat;          // (g/serving)     optional
    private final int sodium;       // (mg/serving)    optional
    private final int carbohydrate; // (g/serving)     optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings,
           int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }
}

这种方式在参数规模较小时还适用,但是当规模增大以后,需要写大量的构造方法,且在调用时容易混淆,很容易把同类型参数的顺序搞错。这种模式的客户端代码(这里客户端的概念是调用上述API的程序)可读性很差,且不便于debug。

JavaBeans模式

JavaBeans模式也是一种传统的构造对象的方式。它会提供一个无参的构造方法(当然我觉得包含必要参数也是可以的)来构造对象,然后对所有参数提供setter方法来进行设置。这种方式写成的API虽然冗长,但是易于理解,而且对应的客户端代码可读性也很强。

但是这种方式有一个很严重的缺陷,就是将对象的构造过程分割成了多次调用,使得在构造过程中对象可能处于 不一致 状态。

如何理解这个 不一致 呢?我的理解是每一次构建对象都不能保证有齐全的参数。比如说有一个类,有A和B两个成员变量,如果采用JavaBeans模式,那么有可能在一个地方set了A,没有set B,而另一个地方set了B没有set A。但是使用这个对象的客户端代码不知道它到底被set了哪些属性,这样就不能冒然get了。

另外一个问题是,当一个对象被多线程共享,把参数的设置权限这样放开出去也有相当大的风险,这个对象的内部基本上就会完全乱套了。

因为不一致问题,你无法用JavaBeans模式来构建不可变对象,而且还需要自行保证线程安全性。

Builder模式

最后来说说我们的主角—— Builder模式 。Builder模式兼具可伸缩构造方法模式的安全性和JavaBeans模式的可读性。这种模式通常在一个类的内部加入一个静态成员类Builder,用Builder打造出一个封闭的构造区域,在这个构造区域内你可以设置必需的参数,然后像JavaBeans模式那样任意设置可选参数(这个过程是链式调用的,非常畅快),最后调用build方法拿到要构建的对象。下面是采用Builder模式的NutritionFacts类的写法:

// 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 sodium        = 0;
        private int carbohydrate  = 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) { 
           fat = val;           
           return this;
        }

        public Builder sodium(int val) { 
           sodium = val;        
           return this; 
        }

        public Builder carbohydrate(int val) { 
           carbohydrate = val;  
           return this; 
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

可以看到这个类是一个不可变类,示例代码中省略了参数有效性的检查。实际的检查可以放在Builder类的构造方法和build方法调用的NutritionFacts构造方法中。

现在要构造NutritionFacts的过程变得非常简单易读,下面是实际的用法示例:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

Builder模式适用于 类层次结构 。抽象类可以有抽象类的Builder,继承它的具体的类也有自己的Builder。考虑到篇幅我就不把相关的示例代码贴出来了。感兴趣的朋友可直接阅读原书。

总结

Builder模式具有良好的可伸缩性和可读性,也能保证一致性。因为Builder是类的静态内部类,所以可重复使用来构建多个对象,而且可以在每次创建时添加一下自动填充的属性,比如序列号。

Builder模式也有缺点。一个是在创建对象之前必须先创建相应的builder,这在对性能要求很严苛的情况下会有问题(不过一般也不太会有这么极致的要求吧......)。还有一个问题是,Builder模式还是比较冗长的,所以建议只有在参数数量较多(四个以上)时再使用。

不过也要注意,如果在设计类的起初就预料到类的规模在将来会变得很庞大,那最好一开始就采用Builder模式,省得后面再改麻烦。

最后的最后,lombok的@Builder注解是个好东西啊,如果你渴望Builder模式的便利,又懒得自己写那么多重复代码,@Builder绝对会是你的不二之选:)

声明

本文仅用于学习交流,请勿用于商业用途。转载请注明出处,谢谢。

参考资料

  1. 《Effective Java(第3版)》
  2. Java实例变量和类变量 https://blog.csdn.net/itmyhom...
  3. 【Effective Java】理解 - 在构造过程中JavaBeans可能处于不一致的状态 https://blog.csdn.net/digi352...
原文  https://segmentfault.com/a/1190000022297730
正文到此结束
Loading...