通过行为参数化传递代码
在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天农民又回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,你要处理一个集合,可能会写一个方法:
- 可以对列表中的每个元素做“某件事”;
- 可以在列表处理完后做“另一件事”;
- 遇到错误时可以做“另外一件事”。
行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、香烟、葡萄酒什么的。这相当于调用一个 goAndBuy 方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go,它可以接受不同的新行为作为参数,然后去执行。
1. 应对不断变化的需求
编写能够应对变化的需求的代码并不容易。让我们来看一个例子,就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。
/**
* 苹果类
*/
public class Apple {
private int weight;
private String color;
public Apple() {
}
public Apple(int weight, String color) {
this.weight = weight;
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Apple{" +
"weight=" + weight +
", color='" + color + '\'' +
'}';
}
}
1.1 初试牛刀:筛选绿苹果
第一个解决方案可能是下面这样的:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
上面就是筛选出绿色的苹果的实现。但是现在农民改主意了,他还想要筛选红苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成 filterRedApples,然后更改if 条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、红色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
1.2 再展身手:把颜色作为参数
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (color.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
现在,只要像下面这样调用方法,农民朋友就会满意了:
List<Apple> redApples = filterApplesByColor(inventory, "red");
List<Apple> greenApples = filterApplesByColor(inventory, "green");
让我们把例子再फ得复杂一点儿。这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”
作为软件工程师,你就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (weight == apple.getWeight()) {
result.add(apple)
}
}
return result;
}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了 DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。
你可以将颜色和重量结合为一个方法,称为 filter。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询。
1.3 第三次尝试:对你能先到的每个属性做筛选
一种把所有属性都结合起来的笨拙方式如下:
public static List<Apple> filerApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if ((flag && color.equals(apple.getColor()))
|| (!flag && weight == apple.getWeight())) {
result.add(apple);
}
}
return result;
}
你可以这么用(但真的很笨拙):
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
这个解决方案再差不过了。首先,客户端代码看上去ጁ透了。true 和 false 是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿ᓣ的重苹果,又该怎么办?你会有好多个重复的 filter 方法,或一个巨大的非常复杂的方法。到目前为止,你已经给 filterApples 方法加上了值(比如 String、Integer 或 boolean)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的 filterApples 方法。
2. 行为参数化
你需要一种比添加很多参数更好的方法来应对变化的需求,让我们站在更高的高度进行抽象化,一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个 boolean 值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选出标准建模:
public interface ApplePredicate {
boolean test(Apple apple);
}
现在你就可以用ApplePredicate的多个实现代表不同的选择标准了:
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
你可以把这些标准看作 filter 方法的不同行为。你ѷ做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是 ApplePredicate,不同的策略就是 AppleHeavyWeightPredicate 和 AppleGreenColorPredicate。
但是,该怎么利用 ApplePredicate 的不同实现呢?你需要 filterApples 方法接受
ApplePredicate 对象,对 Apple 做条件测试。这就是行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。
要在我们的例子中实现这一点,你要给 filterApples 方法添加一个参数,让它接受
ApplePredicate 对象。这在软件工程上有很大好处:现在你把 filterApples 方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
2.1 第四次尝试:根据抽象条件筛选
利用 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;
}
2.1.1 传递代码行为
这里值得停下来小小地ࣻ庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的 ApplePredicate 对象,并将它们传递给 filterApples 方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现 ApplePredicate 就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
你已经做成了一件很酷的事:filterApples 方法的行为取决于你通过 ApplePredicate 对象传递的代码。换句话说,你把 filterApples 方法的行为参数化了!
2.1.2 多种行为,一个参数
正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如图所示:
你已经看到,可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。让我们来看看可以怎样改进。
3. 对付啰嗦
我们都知道,人们都不愿意用那些麻烦的功能或概念。目前,当要把新的行为传递给 filterApples 方法的时候,你不得不声明好几个实现 ApplePredicate 接口的类,然后实例化好几个只会提到一次的 ApplePredicate 对象,这真是很啰嗦,很费时间。
3.1 第五次尝试:使用匿名类
匿名类和你熟悉的 Java 内部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。下面我们展示一下如何使用匿名类实现 ApplePredicate 接口,重新实现筛选的例子:
List<Apple> greenColorApples = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
});
但是匿名类还是不够好,因为它很笨重,往往要写很多多余的代码。整体啰嗦了就不好:因为它让人不愿意使用它。
3.2 第六次尝试:使用 Lambda 表达式
List<Apple> grrenColorApples = filterApples(inventory,
(Apple apple) -> "green".equals(apple.getColor()));
不得不承认这代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。我们现在已经解决了啰嗦的问题。
3.3 第七次尝试:将 List 类型抽象化
在通往抽象的路上,我们还可以更进一步。目前,filterApples 方法还只适用于 Apple。 你还可以将 List 类型抽象化,从而超越你眼前要处理的问题:
public static <T> List<T> filterApples(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
result.add(t);
}
}
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);
4. 真实的例子
你现在已经看到,行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这 种模式可以把一个行为(一段代码)装起来,并通过传递和使用创建的行为(例如对 Apple 的 不同谓词)将方法的行为参数化。前面提到过,这种做法类似于略设计模式。你可能已经在实 践中用过这个模式了。Java API 中的很多方法都可以用不同的行为来参数化。这些方法往往与匿 名类一起使用。我们会展示三个例子,这应该能帮助你传递代码的思想了:用一个 Comparator 排序,用Runnable 执行一个代码块。
4.1 用 Comparator 来排序
对集合进行排序是一个常见的编程任务。比如,你的那位农民想要根据苹果的重量对库 存进行排序,或者他可能改了主意,希望你根据对苹果进行排序。听起来有点儿熟?是的, 你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。 sort的行为 可以用java.util.Comparator对象来参数化,它的接口如下:
// java.util.Comparator
public interface Comparator<T> {
public int compare(T o1, T o2);
}
因此,你可以随时创建 Comparator 的实现,用 sort 方法表现出不同的行为。比如,你可以 使用匿名类,按照重量升序对库存排序:
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
如果农民改了主意,你可以随时创建一个Comparator来满足他的新要求,并把它传递给 sort方法。而如何进行排序这一内部细节都被抽象了。用Lambda表达式的话,看起来就是 这样:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
4.2 用 Runnable 执行代码
线程就像是轻量级的进程:它们自己执行一个代码块。但是,怎么才能告诉线程要执行哪块 代码呢?多个线程可能会运行不同的代码。我们需要一种方式来代表稍候执行的一段代码。在 Java 里,你可以使用 Runnable 接口表示一个要执行的代码块。请注意,代码不会返回任何结果 (即void):
// java.lang.Runnable
public interface Runnable{
public void run();
}
你可以像下面这样,使用这个接口创建执行不同行为的线程:
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
用Lambda表达式的话,看起来是这样:
Thread t = new Thread(() -> System.out.println("Hello world"));
5. 总结
在本篇文章中我们应该理解的关键概念有:
- 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,成不 同行为的能力;
- 行为参数化可让代码更好地适应不断变化的要求,轻未来的工作量;
- 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接 口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来少;
- Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。