构造方法参数过多的解决方法
提出问题:
例如:食品营养成分标签
- 必需的属性——每次建议的摄入量,每罐的份量和每份卡路里,
- 以及超过20个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。
大多数产品只有这些可选字段中的少数, 且具有非零值。
应该为这样的类编写什么样的构造方法或静态工厂?
1. 可伸缩构造方法模式(Telescoping constructor pattern)
在这种模式中,
首先提供一个只有必需参数的构造方法,
接着提供增加了一个可选参数的构造函数,
然后提供增加了两个可选参数的构造函数,等等,
最终在构造函数中包含所有必需和可选参数。
以下就是它在实践中的样子。为了简便起⻅,只显示了四个可选属性:
// 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;
}
/**
可以使用,包含所有要设置的参数的,最短参数列表的,构造方法来创建对象
**/
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
缺点:
1、参数扩展性差,随着参数数量的增加,代码维护成本大大增加;
2、增加使用复杂性,难以理解,参数过多时必须仔细对比参数顺序。
2. JavaBeans 模式
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数:
// JavaBeans Pattern - allows inconsistency, mandates mutability (pages 11-12)
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; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
优点:
1、可读性强,易于理解。
缺点:
2、破坏了类的不可变性,需要程序员增加工作以确保线程安全。
不可变类简单来说是它的实例不能被修改的类。包含在每个实例中的所有信息在对象的生命周期中是固定的。
3. Builder Pattern
客户端不直接构造所需的对象,而是调用一个包含所有必需参数的构造 方法 (或静态工厂) 得到获得一个 builder 对象。
然后,客户端调用 builder 对象的与 setter 相似方法来设置你 想设置的可选参数。
最后,客户端调用 builder 对象的一个无参的 build 方法来生成对象,该对象通常是不可变的。
Builder 通常是它所构建的类的一个静态成员类。
// Builder Pattern (Page 13)
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;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
Builder 模式结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。
4. Builder pattern for class hierarchies(builder 模式在类的继承体系中的应用)
Builder 模式非常适合在类的集成体系中的应用。
例如,考虑代表各种比萨饼的基类:
// Builder pattern for class hierarchies
// Note that the underlying "simulated self-type" idiom allows for arbitrary fluid hierarchies, not just builders
public abstract class Pizza {
//浇料: 火腿、蘑菇、洋葱、胡椒粉、香肠
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// Subclasses must override this method to return "this"
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // See Item 50
}
}
两个具体的 Pizza 的子类:
其中一个代表标准的纽约⻛格的披萨(NyPizza),有一个所需的披萨尺寸参数;
另一个是半圆形烤乳酪披萨(Calzone),指定酱汁是添加到馅饼里面还是外面。
// Subclass with hierarchical builder (Page 15)
public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override
public String toString() {
return "New York Pizza with " + toppings;
}
}
// Subclass with hierarchical builder (Page 15)
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
@Override public String toString() {
return String.format("Calzone with %s and sauce on the %s",
toppings, sauceInside ? "inside" : "outside");
}
}
调用示例:
// Using the hierarchical builder (Page 16)
public class PizzaTest {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside()
.build();
System.out.println(pizza);
System.out.println(calzone);
}
}
5.lombok Builder
lombok中提供的Builder注解效果:
Before:
@Builder
class Example<T> {
private T foo;
private final String bar;
}
After:
class Example<T> {
private T foo;
private final String bar;
private Example(T foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public static <T> ExampleBuilder<T> builder() {
return new ExampleBuilder<T>();
}
public static class ExampleBuilder<T> {
private T foo;
private String bar;
private ExampleBuilder() {}
public ExampleBuilder foo(T foo) {
this.foo = foo;
return this;
}
public ExampleBuilder bar(String bar) {
this.bar = bar;
return this;
}
@java.lang.Override public String toString() {
return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
}
public Example build() {
return new Example(foo, bar);
}
}
}
总结:
builder 模式客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 模式比 JavaBeans 更安全。
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。builder 模式比伸缩构造方法模式更冗⻓, 因此只有在有足够多的参数时才值得使用它。
对于以后可能添加更对参数的情况,最好从一开始就使用builder模式。
总而言之,当设计类的构造方法或静态工厂的参数超过几个时(比如四个),Builder 模式是一个不错的选择,
特别是许多参数是可选的或参数类型相同。
参考资料:
Effective Java, Third Edition