遇到多个构造器参数时要考虑用构建器

    静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。考虑用一个类表示包装食品外面显示的养分成份标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里,还有超过20个可选域:总脂肪量、饱和脂肪量、转化脂肪、胆固醇、钠等等。大多数产品都只有几个可选域中会有非零的值。 java

    对于这样的类,应该用哪一种构造器或者静态方法来编写呢?程序员一贯习惯采用telescoping constructor模式,在这种模式下,你提供一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,依此类推,最后一个构造器包含全部可选参数。下面有个示例,为了简单起见,它只显示四个可选域:
node

// 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; // 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) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}
当你想要建立实例的时候,就利用参数列表最短的构造器,但该列表中包含了要设置的全部参数:


NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);
    这个构造器调用一般须要许多你本不想设置的参数,但仍是不得不为它们传递值。在这种状况下,咱们给fat传递了一个值为0。若是"仅仅"是这6个参数,看起来还不算太糟,问题是随着参数数目的增长,它很快就失去了控制。


    一句话:telescoping constructor模式可行,可是当有许多参数的时候,客户端代码会很难编写,而且仍然较难以阅读。若是读者想知道那些值是什么意思,必须很仔细地数着这些参数来探个究竟。一长串相同的类型参数会致使一些微妙的错误。若是客户端不当心颠倒了这两种参数,编译器也不会出错,可是程序在运行时会出现错误的行为。
程序员

    遇到许多构造器参数的时候,还有第二种代替办法,即JavaBeans模式,在这种模式下,调用一个无参构造器来建立对象,而后调用setter方法来设置每一个必要的参数,以及每一个相关的可选参数:
安全

// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // " " " "
    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;
    }
}
    这种模式不具有telescoping constructor模式的任何缺点。说得明白一点,就是建立实例很容易,这样产生的代码读起来也很容易:


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

    遗憾的是,JavaBeans模式自身有着很严重的缺点。由于构造过程被分到了几个调用中,在构造过程当中JavaBean可能处于不一致的状态。类没法仅仅经过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会致使失败,这种失败与包含错误的代码截然不同,所以它调试起来十分困难。与此相关的另外一点不足在于,JavaBeans模式防止了把类作成不可变的可能(见第15条),这就须要程序员付出格外努力来确保它的线程安全。 性能

    当对象的构造完成,而且不容许在解冻以前使用时,经过手工"冻结"对象,能够弥补这些不足,可是这种方式十分笨拙,在实践中不多使用。此外,它甚至会在运行时致使错误,由于编译器没法确保程序员会在使用以前先在对象上调用freeze方法。
ui

    幸运的是,还有第三种替代方法,既能保证像telescoping constructor模式那样的安全性,也能保证像JavaBeans模式那么好的的可读性。这就是Builder模式[Gamma95,p.97]的一种形式。不直接生成想要的对象,而是让客户端利用全部必要的参数调用构造器(或者静态工厂),获得一个builder对象。而后客户端在builder对象上调用相似于setter的方法,来设置每一个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类(见第22条)。下面就是它的示例: this

// 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;
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.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 carbohydrate = 0;
        private int sodium = 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 carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}
注意NutritionFacts是不可变的,全部的默认参数值都单独放在一个地方。builder的setter方法返回builder自己,以即可以把调用连接起来。下面就是客户端代码:


NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
    这样的客户端代码很容易编写,更为重要的是,易于阅读。builder模式模拟了具名的可选参数,就像Ada和Python中的同样。


    builder像个构造器同样,能够在它的参数中强加约束条件。build方法能够检验这些约束条件。将参数从builder拷贝到对象中以后,并在对象域而不是builder域(见第39条)中对它们进行检验,这个很重要。若是违反了任何约束条件,build方法就应该抛出IllegalStateException(见第60条)。异常的详细方法应该显示出违反了哪一个约束条件。
spa

    对多个参数强加约束条件的另外一种方法是,用setter方法提供某个约束条件必须持有的全部参数。若是该约束条件没有获得知足,setter方法就会抛出IllegalArgumentException。这有个好处,就是一旦传递了无效的参数,当即就会发现约束条件失败,而不是等着调用build方法。
线程

    与构造器相比,builder的一点微略优点在于,builder能够有多个varargs参数。构造器就像方法同样,只能有一个varargs参数。由于builder利用单独的方法来设置每一个参数,你想要多少个varargs参数,它们就能够有多少个,直到每一个setter方法都有一个varargs参数。
设计

    Builder模式十分灵活,能够利用单个builder构建多个对象。builder的参数能够在建立对象期间进行调整,也能够随着不一样的对象而改变。builder能够自动填充某些域,例如每次建立对象时自动增长序列号。

    设置了参数的builder生成了一个很好的抽象工厂(Abstract Factory)[Gamma95,p.87]。换句话说,客户端能够将这样一个builder传给方法,使该方法可以为客户端建立一个或者多个对象。要使用这种用法,须要有个类型来表示builder。若是使用的是发行版本1.5或者更新的版本,一个单独的泛型(见第26条)就能知足全部的builder,不管它们在构建哪一种类型的对象:

// A builder for objects of type T
public interface Builder {
    public T build();
}
注意,能够声明NutritionFacts.Builder类来实现Builder


    带有Builder实例的方法一般利用有限制的通配符类型(bounded wildcard type,见第28条)来约束构建器的类型参数。例如,下面就是构建每一个节点的方法,它利用一个客户端提供的Builder实例构建树:

Tree buildTree(Builder nodeBuilder) { ... }
    Java中传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。这种用法隐含着许多问题。newInstance方法老是企图调用类的无参构造器,这个构造器甚至可能根本不存在。若是类没有能够访问的无参构造器,你也不会收到编译时错误。相反,客户端代码必须在运行时处理InstantiationException或者IllegalAccessException,这样既不雅观也不方便。newInstance方法还会传播由无参构造器抛出的任何异常,即便newInstance缺少相应的throws子句。换句话说,Class.newInstance破坏了编译时的异常检查。上面讲过的Builder接口弥补了这些不足。


    Builder模式的确也有它自身的不足。为了建立对象,必须先建立它的构建器。虽然建立构建器的开销在实践中可能不那么明显,可是在某些十分注重性能的状况下,可能就是个问题了。Builder模式还比telescoping constructor模式更加冗长,所以它只在有足够参数的时候才使用,好比4个或者更多个参数。可是记住,未来你可能须要添加参数。若是一开始就使用构造器或者静态工厂,等到类须要多个参数时才添加构建器,就会没法控制,那些过期的构造器或者静态工厂显得十分不协调。所以,一般最好一开始就使用构建器。

    简而言之,若是类的构造器或者静态工厂中具备多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与使用传统的telescoping constructor模式相比,使用Builder模式的客户端代码将更易于阅读和编写,builders也比JavaBeans更加安全。

相关文章
相关标签/搜索