-
应对不断变化的需求
-
行为参数化
-
匿名类
-
Lambda表达式预览
-
口真实示例: Comparator, Runnable和GUI
在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你: “其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道: "要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果你要处理一个集合,可能会写一个方法:
-
可以对列表中的每个元素做“某件事”
-
可以在列表处理完后做“另一件事”
-
遇到错误时可以做“另外一件事”
行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包,奶酪、葡萄酒什么的。这相当于调用一个goAndBuy方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了;去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件的发给他,当他收到之后就可以按照指示行事了。
你现在做的事情就更高级一些了,相当于一个方法:go,它可以接受不同的新行为作为参数,然后去执行。
这一章首先会给你讲解一个例子,说明如何对你的代码加以改进,从而更灵活地适应不断变化的需求。在此基础之上,我们将展示如何把行为参数化用在几个真实的例子上。比如,你可能已经用过了行为参数化模式--使用JavaAPI中现有的类和接口,对List进行排序,筛选文件名或告诉一个Thread去执行代码块,甚或是处理GUI事件。你很快会发现,在Java中使用这种模式十分啰嗦。Java 8中的Lambda解决了代码啰嗦的问题。我们会在第3章中向你展示如何构建Lambda表达式、其使用场合,以及如何利用它让代码更简洁。
2.1 应对不断变化的需求
编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序面言,你必须实现一个从列表中筛选绿苹果的功能。听起来很简单吧?
2.1.1 初试牛刀;筛选绿苹果
第一个解决方案可能是下面这样的:
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;
}
突出显示的行就是筛选缘苹果所需的条件。但是现在农民改主意了,他还想要筛选红苹果。
称该怎么做呢?简单的解决办法就是复制这个方法,把名字改成fiiterRedApples,然后更改i条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
2.1.2 再展身手:把颜色作为参数
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterGreenApples (List <Apple > inventory,String color) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple : inventory) {
if ("green".equals(apple.getColor().equals(color)) ){
result.add(apple);
}
}
return result;
}
现在,只要像下面这样调用方法,农民朋友就会满意了
List <Apple> greenApples = filterAppByColor(inventory,"green");
List <Apple> redApples = filterAppByColor(inventory,"red");
太简单了对吧?让我们把例子弄得复杂一点儿,这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹采放太好了,重的平来一般是重量大下150克。”
作为软件工程够,你早就想跳农民可能会要改变重量,于是你写了下面的方法,用另一个参数应对不同的重量:
public static List<Apple> filterAppByWeight (List <Apple > inventory,
int weight) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple : inventory) {
if ("green".equals(apple.getWeight()) ){
result.add(apple);
}
}
return result;
}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了DRY (Don't Repeat Yourself,
)的软件工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。你可以将颜色和重量结合为一个方法,称为filter。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!我们很快会解释为什么)。
2.1.3 第三次尝试:对你能想到的每个属性做筛选
一种把所有属性结合起来的笨拙尝试如下所示:
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;
}
List<Apple> heavyApples =filterApples(inventory, 150,false);
这个解决方案再差不过了。首先,客户端代码看上去糟透了。true和false是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。到目前为止,你已经给filterApples方法加上了值(比如string、Integer或boolean)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。在下一节中,我们会介绍了如何利用行为参数化实现这种灵活性。
2.2 行为参数化
你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据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().equals(apple.getColor()));
}
}
你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”"相关,它让你定义一族算法,把它们封装起来(称为“策略”)。然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreen- ColorPredicate.
但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受 ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
要在我们的例子中实现这一点,你要给filterApples方法添加一个参数,让它接受 ApplePredicate对象。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
第四次尝试:根据抽象条件筛选
利用ApplePredicate改过之后,filter方法看起来是这样的:
public statie 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
1.传递代码/行为
这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:
public class ApplckedAndHeavyPredicate implements ApplePredicate(
publie boolean test (Apple apple){
return "red",equals(apple-getColor())&&apple.getweight ()>150;}
List<Apple> redAndHeavyApples =
filterApples (inventory, new AppleRedAndHeavyPredicate());
你已经做成了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法的行为参数化了! 请注意,在上一个例子中,唯一重要的代码是test方法的实现,如图2-2所示;正是它定义 了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象, 所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因 为你是通过一个实现了test方法的对象来传递布尔表达式的。你将在2.3节(第3章中有更详细的 内容)中看到,通过使用Lambda,你可以直接把表达式"red".equals (apple.getColor()) &&apple.getweight () > 150传递给filterApples方法,而无需定义多个ApplePredicate 类,从而去掉不必要的代码。
2.多种行为,一个参数
正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。
这就是行为参数化是一个有用的概念的原因,你应该把它放进你的工具箱里,用来编写灵活的API.
2.3 对付啰嗦
2.3.2匿名类
匿名类和你熟悉的java局部类(块中定义的类)差不多,但匿名类没有名称。它允许声明并实例化一个类。换句话说,它允许你随用随建。
2.3.2 第五次尝试: 使用匿名类
2.3.3 第六次尝试: 使用Lambda表达式
上面的代码在Java8里可以用Lambda表达式重写下面的样子:
List<Apple> result = filterApples(inventory,(Apple apple)-> "red".equals(apple.getColor()));
不得不承认这代码看上去比先前干净很多,这很好,因为看起来更像问题陈述本身了。
2.4.1 用Comparator来排序
2.4.2 用Runnable执行代码
Thread t = new Thread(new Runnable() {
public void run(){
system.out.println("hellow world");
});
用Lambda表达式的话,看起来是这样:
Thread t = new Thread(() -> System.out.println("hellow world"));
2.5 小结
-
行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
-
行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
-
传递代码,就是将新行为作为参数传递给方法。但在Java8之前这实现起来很啰嗦。为接
-
声明许多只用一次的实体类而造成的啰嗦代码,在Java8之前可以用匿名类来减少。 Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。