Welcome to my website, have a nice day!
Dream it, Do it, Make it!

Java有效编程:使用构建器来处理构造函数有多个参数的情形

1. 问题引入

在Java中,会碰到这种情形:以一个类为例,它表示包装食品上的营养标签。这个标签包含必需字段,如:净含量、毛重和每单位份量的卡路里,可选的字段,如:总脂肪、饱和脂肪、反式脂肪、胆固醇、钠……。

如何对这种有大量可选参数的构造函数进行更好的扩展呢?

2. 问题解析

2.1 可伸缩构造函数

传统的处理方式是使用可伸缩构造函数,在这种模式中,只向构造函数提供必需的参数。即,向第一个构造函数提供单个可选参数,向第二个构造函数提供两个可选参数,以此类推,最后一个构造函数是具有所有可选参数的。

类似下面的代码(篇幅原因,只展示4个可选字段的情况):

// 伸缩构造模式-不能很好的扩展
public class NutritionFacts {
    private final int servingSize; // (mL) 必须字段
    private final int servings; // (per container) 必须字段
    private final int calories; // (per serving) 可选
    private final int fat; // (g/serving) 可选
    private final int sodium; // (mg/serving) 可选
    private final int carbohydrate; // (g/serving) 可选

    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;

这种方式虽然可行,但是当有很多参数时,编写客户端代码是很困难的,而且读起来更困难。阅读代码的人想知道所有这些值是什么意思,必须仔细清点参数。相同类型参数的长序列会导致细微的错误。如果客户端不小心倒转了两个这样的参数,编译器不会报错,但是程序会在运行时出错。

2.2 JavaBean模式

这种方式,通过调用无参构造来创建对象,然后调用setter 方法来设置每个所需的参数。

// JavaBeans模式 - 容易导致程序构建的不一致性,让类变成可变的
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // Required; no default value
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;
    public NutritionFacts() { }
    // Setters
    public void setServingSize(int val) { servingSize = val; }
    public void setServings(int val) { servings = val; }
    public void setCalories(int val) { calories = val; }
    public void setFat(int val) { fat = val; }
    public void setSodium(int val) { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}

JavaBean让创建实例很容易,虽然有点冗长,单代码容易阅读,如下:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

不幸的是,JavaBean 模式本身有严重的缺点。因为构建是在多个调用之间进行的,所以 JavaBean 可能在构建的过程中处于不一致的状态。该类不能仅通过检查构造函数参数的有效性来强制一致性。在不一致的状态下尝试使用对象可能会导致错误的发生,而包含这些错误的代码很难调试。一个相关的缺点是,JavaBean 模式排除了使类不可变的可能性,并且需要程序员额外的努力来确保线程安全。

2.3 构建器

第三种选择-构建器,它结合了可伸缩构造函数模式的安全性和 JavaBean模式的可读性。

它是建造者模式的一种形式。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数(或静态工厂),并获得一个 builder对象。然后,客户端在构建器对象上调用像setter这样的方法来设置每个感兴趣的可选参数。最后,客户端调用一个无参数的构建方法来生成对象,这通常是不可变的。

构建器通常是它构建的类的静态成员类。

使用示例:

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 {
        // 必要参数
        private final int servingSize;
        private final int servings;
        // 可选参数 - 初始化为默认值
        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;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

NutritionFacts 类是不可变的,所有参数默认值都在一个位置。构建器的setter方法返回构建器本身,这样就可以链式调用,从而得到一个流畅的 API。

客户端使用:

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

参考:

Item 2: Consider a builder when faced with many constructor parameters(当构造函数有多个参数时,考虑改用构建器)

赞(0)
未经允许禁止转载:Ddmit » Java有效编程:使用构建器来处理构造函数有多个参数的情形

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址