行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果你要处理一个集合,可能会写一个方法:
- 可以对列表中的每个元素做“某件事”
- 可以在列表处理完后做“另一件事”
- 遇到错误时可以做“另外一件事”
行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy
方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go
,它可以接受不同的新行为作为参数,然后去执行。
行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
行为参数化可以让代码更好地适应不断变化的要求,减轻未来的工作量。
传递代码,就是将新行为作为参数传递给方法。
Java API
包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI
处理。
行为参数化
以筛选苹果为例,对选择标准建模:需要根据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
方法的对象来传递布尔表达式的。
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素的应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如下图所示。
行为参数化步骤整理
下面编写一个prettyPrintApple
方法,它接受一个Apple
的List
,并可以对它参数化,以多种方式根据苹果生成一个String
输出。例如,你可以告诉prettyPrintApple
方法,只打印每个苹果的重量。此外,你可以让prettyPrintApple
方法分别打印每个苹果,然后说明它是重的还是轻的。
- 首先,你需要一直表示接受
Apple
并返回一个格式String
值的方法。
public interface AppleFormatter {
String accept(Apple a);
}
- 现在你就可以通过实现
AppleFormatter
方法,来表示多种格式行为了:
public class AppleFancyFormatter implements AppleFormatter {
public String accept(Apple a) {
String characteristic=a.getWeight() > 150 ? "heavy" : "light";
return "A "+characteristic+" "+a.getColor()+" apple";
}
}
public class AppleSimpleFormatter implements AppleFormatter {
public String accept(Apple a) {
return "An apple of "+a.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());
或者
prettyPrintApple(inventory,new AppleSimpleFormatter());
这样就可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。
使用Lambda表达式
接下来,通过使用Lambda
,你可以直接把表达式"red".equals(apple.getColor()) && apple.getWeight() > 150
传递给filterApples
方法,而无需定义多个ApplePredicate
类,从而去掉不必要的代码。
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()) && apple.getWeight() > 150);
这代码看上去比先前干净了很多,看起来也更像问题陈述本身了。
在通往抽象的路上,我们还可以更进一步。目前,filterApples
方法还只适用于Apple
。你还可以将List
类型抽象化,从而超越你眼前要处理的问题:
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter( List<T> list,Predicate<T> p){ //引入类型参数T
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);