java当类构造器参数过多的时候,考虑 builder 模式

导读:本篇文章讲解 java当类构造器参数过多的时候,考虑 builder 模式,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

静态工厂方法和构造器都有一个限制:当有许多参数的时候,它们不能很好的扩展。

比如试想下如下场景:考虑使用一个类表示食品包装袋上的营养成分标签。这些标签只有几个是必须的——每份的含量、每罐的含量、每份的卡路里,除了这几个必选的,还有超过 20 个可选的标签——总脂肪量、饱和脂肪量等等。对于这些可选的标签,大部分产品一般都只有几个标签的有值,不是每一个标签都用到。

1.(telescoping constructor)重叠构造器模式

对于这种情况,你应该选择哪种构造器或者静态工厂方法。一般程序员的习惯是采用 (telescoping constructor)重叠构造器模式。在这种模式中,提供一个包含必选参数的构造器,再提供其他一些列包含可选参数的构造器,第一个包含一个可以参数、第二个包含两个可选参数,以此类推下去,直到包含所有的可选参数。

示例代码:

// 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;
	}
}

当你想创建一个实例的时候,你只需要找包含你需要的并且是最短参数列表的构造器即可。

这里有一些问题,比如看下面的代码:

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

其中,第 1,2 个可选参数,我们是不需要的,但是程序中没有提供直接赋值第 3,4个可选参数的构造器,因此,我们只能选择包含了 1,2,3,4 个参数的构造器。这里面要求了许多你不想设置的参数,但是你又被迫的设置它们,在这里,传入对应的属性的默认值 0。并且这种模式,随着参数的增加,将变得越来越难以忍受,无论是编写程序的人,还是调用程序的人。

总而言之,(telescoping constructor)重叠构造器模式,可以使用,但是它对客户端来说,很不友好,写和读都是一件困难的事情。它们很难搞懂那些参数对应的到底是什么属性,必须好好的比对构造器代码。并且当参数很多的时候,很容易出 bug,如果使用的时候,无意间颠倒了两个参数的位置,编译器是不会出现警告的,因为这里的类型一样,都是 int ,直到运行的时候才会暴露出。

2.Javabeans 模式
我们还有一种选择,使用 Javabeans 模式 。

在此模式中,我们提供一个 无参构造器 创建实例,然后利用 setXXX 方法,设置每一个必须的属性和每一个需要的可选属性。

示例代码:

// 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; // 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; }
	}

Javabeans 模式,没有 重叠构造器模式 的缺点,对于冗长的参数,使用它创建对象,会很容易,同时读起来也是容易。正如下面看到的,我们可以清晰的看到,每一个属性的值。

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

不幸运的是,Javabeans模式 本身有着严重的缺点:因为,创建对象被分割为多个步骤,先是利用无参构造器创建对象,然后再依次设置属性。这导致一个问题: Javabean 在其创建过程中,可能处于不一致[1]的状态。 类不能通过检查构造器的参数,来保证对象的一致性。

另外一个缺点是,将创建一个可变的类的难度提高了好几个级别,因为有 setXXX 方法的存在。

可以通过一些手段来减少不一致的问题,通过一些手段 冻结 对象,在对象被创建完成之前。并且不允许使用该对象,直到 解冻 。但是这种方式非常笨拙,在实践中很少使用。因为,编译器无法确认程序员在使用一个对象之前,该对象是否已经 解冻 。

3.Builder 模式
幸运的是,这里还有一种方法 Builder 模式,兼顾 重叠构造器 的安全以及 Javabean模式 的可读性。

客户端先通过调用构造器或者静态工厂方法,传入必须的参数,获得一个 builder 对象,代替直接获取目标对象。然后客户端在该 builder 对象上调用 setXXX 方法,为每一个感兴趣的可选属性赋值,最后客户端调用一个 无参构造器 生成最终的目标对象,该对象一般是不可变的。其中 Builder 类是目标类的静态内部类

示例代码:

	// 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;
		
		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;
		}
	}


其中 NutritionFacts 类为不可变类,类的成员变量全部被 final 修饰,参数的默认值被放在一个地方。Builder 类 setXXX 方法返回 Builder 本身,这种写法,可以将设置变成一个链,一直点下去(fluent APIs):

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

这样的客户端代码,容易编写,更容易阅读。

示例代码中,为了简洁,省去了有效性的检查。一般,为了尽快的检查到非法参数,我们在 builder 的构造器和方法中,对其参数进行检查。

还需要检查 build 方法中调用的构造器的多个不可变参数[2]。这次检查延迟到 object 中,为了确保这些不可变参数不受到攻击,在 builder 将属性复制到 object 中的时候,再做一次检查。如果检验失败,则抛出 IllegalArgumentException 异常,异常信息中提示哪些参数不合法。

Bulider 模式很适合类的层次结构。可以使用一个 builder 的平行结构,即每一个 builder 嵌套在一个对应的类中,抽象类中有抽象的 builder ,具体类中有具体的 builder 。像下面的代码所示:

	// Builder pattern for class hierarchies
	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
	    }
	}

	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;
	    }
	}

	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;
	    }
	}


注意,这里的 Pizza.Builder 是类属性,被 static 修饰的,并且泛型参数,是一个 递归 的泛型参数,继承本身。和返回自身的抽象方法 self ,搭配一起,可以链式的调用下去,不需要进行类型的转换,这样做的原因是,java 不直接支持 自类型 [3],可以模拟自类型 [4]。

如果不使用模拟自类型的话,调用 addTopping方法,返回的其实就是抽象类中的 Builder ,这样就导致无法调用子类扩展方法,无法使用 fluent APIS。其中 build 方法,使用了 1.5 添加的 协变类型 ,它可以不用 cast 转换,就直接使用具体的类型,否则子类接收父类,是需要强转的 。

builder 模式另外一个小优点:builder 可以有多个 可变参数,因为,可以将多个可变参数,放到各自对应的方法中[5]。另外 build 可以将多个参数合并到一个字段上,就如上面代码中 addTopping 的那样[6]。

builder 模式是非常灵活的。一个单一的 builder 多次调用,可以创建出不同的对象[7]。builder 的参数,可以在调用 build 方法的时候进行细微调整,以便修改创建出的对象[7:1]。builder 模式还可以自动的填充 object域的字段在创建对象的时候。比如为每个新创建的对象设置编号,只需要在 builder 中维护一个类变量即可。

builder 模式也是有缺点的。为了创建一个对象,我们首先需要创建它的 builder 对象。虽然,创建 builder 对象的开销,在实践中不是很明显,但是在对性能要求很严格的场景下,这种开销能会成为一个问题。同时,builder 模式是非常冗杂的,对于比 重叠构造器 ,所以,builder 模式应该仅仅被用在构造器参数足够多的情况下,比如三个、四个或者更多,只有这样,使用 builder 模式才是值得的。但是,你要时刻记住,类在将来可能会添加新的参数,如果你一开始使用了构造器或者静态工厂方法,随着类的变化,类的属性参数变得足够多,这时候你再切换到 builder 模式,那么一开始的构造器和静态工厂方法就会被废弃,这些废弃的方法看起来很凸出,你还不能删除它们,需要保存兼容性。因此,一般一开始就选择 builder 模式是一个不错的选择。

总结,builder 模式是一个好的选择,当设计一个类的时候,该类的构造器参数或者静态工厂参数不止几个参数,尤其是许多参数是可选的或者同一个类型(可变参数)。这样设计的类,客户端代码,与静态工厂方法和重叠构造器比起来更加容易阅读和编写,和 Javabeans 模式比起来更加安全。

参考:
考虑 builder 模式,当构造器参数过多的时候

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/80352.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!