Java编程技巧——构建器

Java编程技巧——构建器

遇到多个构造器参数时的最佳实践

  最近读《Effective Java》学到一些很好的编程技巧,和大家分享一下。

  首先考虑这样一个类,它用来表示食品包装外面显示营养成分的标签。这些标签中有几个是必须显示的:每份的含量、每罐的含量以及每份的卡路里,还有20余种其它可选标签(如脂肪、钠、糖等含量等)。

  对于这样的类,大多数程序员会采用重叠构造器的方式——提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数。它看起来有可能像这样(为了简单起见,只显示四个可选成员):

public class NutritionFacts {
    private final int servingSize;    //(mL)                required
    private final int servings;        //(per container)    required
    private final int calories;        //                    optional
    private final int fat;            //(g)                optional
    private final int sodium;        //(mg)                optional
    private final int carbohydrate;    //(g)                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) {
        super();
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
                + ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
    }

}

  这种方式创建实例的时候,就利用参数列表最短的构造器,但该列表包含了要设置的所有参数:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

  这种情况下通常需要许多你本不想设置的参数,但还是不得不为它们传递值,这个例子中fat传递了一个为0的初始值。如果参数仅此6个,情况还不算太糟糕,但是随着成员域的增多,这种情况很快就会失去控制。首先更多的参数构造器会让代码非常难以阅读,使用这个类需要非常仔细的探究每个参数具体是什么意思;其次一长串类型相同的参数会导致一些人为的微妙错误,比如客户端不小心颠倒了其中连个参数的位置,此时编译器不会报错,但是程序运行会有错误的显示。

  解决这个问题通常有一个仍然不算完美的方案。即使用一个无参的构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数,这种方式的代码看起来有可能像这样:

public class NutritionFacts {
    /** 每罐的容量 ml */
    private int servingSize     = -1;
    /** 每箱的数量 */
    private int servings         = -1; 
    /** 卡路里 */
    private int calories         = 0;
    /** 脂肪含量 */
    private int fat             = 0;
    /** 钠含量 */
    private int sodium             = 0;
    /** 糖含量 */
    private int carbohydrate     = 0; 

    public NutritionFacts() { }

    public void setServingSize(int servingSize)     { this.servingSize = servingSize; } 
    public void setServings(int servings)             { this.servings = servings; } 
    public void setCalories(int calories)             { this.calories = calories; } 
    public void setFat(int fat)                     { this.fat = fat; } 
    public void setSodium(int sodium)                 { this.sodium = sodium; } 
    public void setCarbohydrate(int carbohydrate)     { this.carbohydrate = carbohydrate; }

    @Override
    public String toString() {
        return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
                + ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
    }

    
}

 

  这种方式弥补了重叠构造器方式的不足,使得创建实例很容易,代码也很容易阅读:

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

 

  遗憾的是这种方式将构造过程分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。类无法仅仅通过校验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会导致失败,这种失败与包含错误的代码大相径庭,因此调试时十分困难。于此相关的另一点不足为这种模式阻止了把类做成不可变类的可能,这时需要程序员付出额外的努力来确保它线程安全。

  那么有没有有一种既能像重叠构造器那样安全(需要通过构造器参数有效性的检验),又能像第二种方式那样有很好的可读性呢?答案是肯定的。此方式是Builder模式的一种形式,不直接生成想要的对象,而是让客户端调用内联builder对象,然后在此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 {
        // Required parameters
        private final int servingSize;
        private final int servings;
        // Optional parameters
        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);
        }
    }

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

    @Override
    public String toString() {
        return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
                + ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
    }

}

 

  这里 NutritionFacts 是不可变的,所有默认参数值都单独放在一个地方。builder 的 具名的 setter 方法返回 builder 本身,以便可以进行类似链式操作的方式进行可选参数的赋值,客户端代码可以像这样:

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

 

  这样的客户端代码很容易编写,并且易于阅读。这里 builder 像一个构造器一样可以对其参数加强约束条件,build 方法可以检验这些约束条件。将参数从 builder 拷贝到对象中后,是在对象域而不是builder 域中对这些参数进行检验,这一点很重要。如果违法了任何约束条件,build方法就应该抛出异常(原书翻译指出此处应该抛出 IllegalStateException 异常,且异常信息应该显示出违反了那个约束条件)。

  今天先整理到这里,后续再完善总结

posted @ 2017-04-19 22:06 夕兮曦兮 阅读( ...) 评论( ...) 编辑 收藏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值