行为参数化
总结:
- 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
- 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
- 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
- Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。
举个例子,应对需求的不断变化,现在一位农民想要从农场库存中筛选出绿苹果,代码如下:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<Apple>();
for(Apple apple: inventory){
if( "green".equals(apple.getColor() ) {
result.add(apple);
}
}
return result;
}
但是现在农民改主意了,他还想要筛选红苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory,
String color) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory){
if ( apple.getColor().equals(color) ) {
result.add(apple);
}
}
return result;
}
现在,只要像下面这样调用方法,农民就会满意了:
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
太简单了对吧?让我们把例子再फ得复杂一点儿。这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”
作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:
public static List<Apple> filterApplesByWeight(List<Apple> inventory,
int weight) {
List<Apple> result = new ArrayList<Apple>();
For (Apple apple: inventory){
if ( apple.getWeight() > weight ){
result.add(apple);
}
}
return result;
}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人ܾ失望,因为它打破了DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。
一种把所有属性结合起来的笨拙尝试如下所示:
public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight) ){
result.add(apple);
}
}
return result;
}
这个解决方案再差不过了。首先,客户端代码看上去遭透了。true和false是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。到目前为止,你已经给filterApples方法加上了值(比如String、Integer或boolean)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。接下来就是展示行为参数化以实现这种灵活性。
定义一个接口,使用ApplePredicate的多个实现
public interface ApplePredicate{
boolean test (Apple apple);
}
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
要在我们的例子中实现这一点,你要给filterApples方法添加一个参数,让它接受ApplePredicate对象。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples =
filterApples(inventory, new AppleRedAndHeavyPredicate());
filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法的行为参数化了!
但是,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。真是很啰嗦,很费时间!
Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类。它可以帮助你进一步改ؒ代码,让它变得更简洁。
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。所以Java 8引入Lambda表达式——一种更简洁的传递代码的方式——解决了这个问题。
上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
在通往抽象的道路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题:
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
现在你可以把filter方法用在香蕉、橘子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
以上就是对行为参数化的理解。