构造方法参数过多的解决方法

提出问题:

例如:食品营养成分标签

  1. 必需的属性——每次建议的摄入量,每罐的份量和每份卡路里,
  2. 以及超过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

posted on 2022-10-27 14:47  weldonh  阅读(479)  评论(0编辑  收藏  举报

导航