通过行为参数化传递代码
行为参数化是可以帮助你处理频繁变更的需求的一种软件开发模式。它意味着拿出一个代码块,将它准备好却不去执行它,这个代码块可以被程序的其他部分调用。
将代码块作为参数传递给另一个方法,稍后再去执行它。这样这个方法就基于那块代码被参数化了。打个比方,可能会这样处理一个集合:
- 可以对列表中的每个元素做 “某件事”
- 可以再列表处理完后做 “另一件事”
- 遇到错误时可以做 “另外一件事”
这样一个方法便可以接受不同的新行为作为参数去执行。
行为参数化
举个栗子:假设要求你对苹果的不同属性做筛选,比如大小、形状、产地、重量等,写好多个重复的filter方法或者一个巨大的非常复杂的方法都不是好办法。为此需要一种更好的方法,来把苹果的选择标准告诉filterApples方法。更高层次的抽象这个问题,对选择标准进行建模:苹果需要根据Apple的某些属性来返回一个boolean值,这需要一个返回boolean值的函数,我们把它称为谓词。
现在我们定义一个接口来对选择标准建模:
public interface ApplePredicate(){
boolean test(Apple apple);
}
现在就可以用ApplePredicate的多个实现代表不同的选择标准了:
static class AppleWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
static class AppleColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
static class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
这些标准可以看做filter方法的不同行为。上面做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来,然后运行的时候选择一个算法,这里的算法族就是applePredicate
,不同的策略就AppleHeavyWeightPredicate
和AppleGreenColorPredicate
。
下面的问题是,要怎么利用applePredicate的不同实现。需要让filterApples
方法接受applePredicate
对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。
为此需要给filterApples方法添加一个参数,让它能接受ApplePredicate对象。这样在软件工程上带来的好处就是:将filterApples方法迭代集合的逻辑与你要应用到集合中每个也元素的行为(这里是谓词)区分开了。
由此便可以修改ApplePredicate方法为:
public static List filterApples(List inventory,
ApplePredicate p){
List result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
现在便可以创建不同的ApplePredicat
e对象,并将它们传递给filterApples
方法。filterApples
方法的行为取决于你通过ApplePredicate对象传递的代码。实现了行为参数化。
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List redAndHeavyApples =
filterApples(inventory, new AppleRedAndHeavyPredicate());
对付啰嗦
虽然已经可以把行为抽象出来让代码适应需求的变化了,但是这个过程很啰嗦,因为当要把新的行为传递给方法的时候,可能需要声明很多实现接口的类来实例化好几个只要实例化一次的类(划重点,这边说的是对于只要实例化一次的类)。这样又啰嗦又费时间。
对于这个问题也有对应的解决办法,Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类,使代码更为简洁。
匿名类
匿名类和Java局部类差不多,但匿名类没有名字。它允许你同时声明并实例化一个类(随用随建)。
用匿名类实现的ApplePredicate对象:
List redApples= filterApples(inventory, new ApplePredicate(){
public boolean test(Apple apple){
return "red".euqls(apple.getColor());
}
});
但是这也并不完全令人满意。第一,它代码占用很多行。第二,它用起来容易让人费解。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,让仍不能让人满意。更好的方式是通过Lambda表达式让代码更易读。
使用Lambda表达式
上面的代码在Java8里可以用Lambda表达式重写为下面的样子:
List result =
filterApples(inventory, (Apple apple) -> "red".equals(getColor()));
这代码看上去比先前干净很多。因为它看起来更像问题陈述本身了。
将List类型抽象化
目前的filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:
public interface Predicate{
boolean test(T t);
}
public static List filter(List inventory, ApplePredicate p){
List result = new ArrayList<>();
for(T e : list ){
if(p.test(e)){
result.add(e);
}
}
return result;
}
现在可以把filter方法用在其他列表上了。
真实的例子
下面将通过两个例子来巩固传递代码的思想。
用Comparator来排序
想要根据不同的属性使用一种方法来表示和使用不同的排序行为来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:
public interface Comparator{
public int compare(T o1,T o2);
}
因此可以随时创建Comparator的实现,用sort方法表现出不同的行为。
按照重量升序对库存排序:
inventory.sort(new Comparator(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
如果用Lambda表达式,它看起来会是这样:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
(这段代码并没有告诉你如何同时遍历列表中的两个元素,不要纠结遍历问题,下一章会详细的讲解如何编写和使用Lambda表达式。)
用Runnable执行代码块
线程就像是轻量级的进程:它们自己执行一个代码块。多个线程可能会运行不同的代码。为此需要一种方式来代表要执行的一段代码。在Java里,可以使用Runnable
接口表示一个要执行的代码块:
public interface Runnabke{
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"));