在筛选苹果的例子中,需要一种比添加很多参数更好的方法来应对变化的需求。后退一步来看看更高层次的抽象。一种可能的解决方案是对选择标准建模:你考虑的是苹果,需要根据Apple
的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean
值。我们把它称为谓词(即一个返回boolean
值的函数)。让我们定义一个接口来对选择标准建模:
public interface ApplePredicate {
boolean test(Apple apple);
}
现在就可以用ApplePredicate
的多个实现代表不同的选择标准了,比如:
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
方法迭代集合的逻辑与要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
利用ApplePredicate
改过之后,filter
方法看起来是这样的:
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
方法的行为参数化了!
请注意,在上一个例子中,唯一重要的代码是test
方法的实现,如下图所示;正是它定义了filterApples
方法的新行为。但令人遗憾的是,由于该filterApples
方法只能接受对象,所以必须把代码包裹在ApplePredicate
对象里。这种做法就类似于在内联“传递代码”,因为是通过一个实现了test
方法的对象来传递布尔表达式的。通过使用Lambda
,可以直接把表达式 "red".equals(apple.getcolor()) && apple.getWeight() > 150
传递给filterApples
方法,而无需定义多个ApplePredicate
类,从而去掉不必要的代码。
正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如x下图所示:
这就是说行为参数化是一个有用的概念的原因。应该把它放进工具箱里,用来编写灵活的API
。
测验:编写灵活的prettyPrintApple方法
编写一个prettyPrintApple
方法,它接受一个Apple
的List
,并可以对它参数化,以多种方式根据苹果生成一个String
输出(有点儿像多个可定制的toString
方法)。例如,可以告诉prettyPrintApple
方法,只打印每个苹果的重量。此外,可以让prettyPrintApple
方法分别打印每个苹果,然后说明它是重的还是轻的。解决方案和前面讨论的筛选的例子类似。
public static void prettyPrintApple(List<Apple> inventory, ???) {
for(Apple apple: inventory) {
String output = ???.???(apple);
System.out.println(output);
}
}
答案如下:
首先,需要一种表示接受Apple
并返回一个格式String
值的方法。前面在编写ApplePredicate
接口的时候,写过类似的东西:
public interface AppleFormatter {
String accept(Apple a);
}
现在就可以通过实现AppleFormatter
方法,来表示多种格式行为了:
public class AppleFancyFormatter implements AppleFormatter {
public String accept (Apple apple) {
String characteristic = apple.getWeight() > 150 ?"heavy" : "light";
return "A" + characteristic + "" + apple.getColor() + "apple";
}
}
public class AppleSimpleFormatter implements AppleFormatter {
public String accept(Apple apple) {
return "An apple of" + apple.getWeight() + "g";
}
}
最后,需要告诉prettyPrintApple
方法接受AppleFormatter
对象,并在内部使用它们。可以给prettyPrintApple
加上一个参数:
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter) {
for(Apple apple: inventory) {
String output = formatter.accept(apple);
System.out.println(output);
}
}
搞定啦!现在就可以给prettyPrintApple
方法传递多种行为了。为此,首先要实例化AppleFormatter
的实现,然后把它们作为参数传给prettyPrintApple
:
prettyPrintApple(inventory, new AppleFancyFormatter());
这将产生一个类似于下面的输出:
A light green apple
A heavy red apple
或者试试这个:
prettyPrintApple(inventory, new AppleSimpleFormatter());
这将产生一个类似于下面的输出:
An apple of 80g
An apple of 155g
把行为抽象出来,让代码适应需求的变化,但这个过程很啰嗦,因为需要声明很多只要实例化一次的类。让我们来看看可以怎样改进。