1、 前言
在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”要如何应对这样不断变化的需求?理想的状态下,应该把工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。
2、应对不断变化的需求
编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,必须实现一个从列表中筛选绿苹 果的功能。
2.1 小试牛刀,筛选苹果
假设有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。可能想要选出所 有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念,可能会写这样一个方法filterGreenApples:
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条件来匹配红苹果。然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
2.2 再展身手:把颜色作为参数
一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) { List<Apple> result = new ArrayList<Apple>(); for (Apple apple : inventory) { if (apple.getColor().equals(color)) { result.add(apple); } } return result; } 复制代码
现在,只要像下面这样调用方法,农民朋友就会满意了:
List<Apple> greenApples = filterApplesByColor(inventory, "green"); List<Apple> redApples = filterApplesByColor(inventory, "red"); ... 复制代码
让我们把例子再弄得复杂一点儿。这位农民又跑回来说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”
作为软件工程师,早就想到农民可能会要改变重量,于是写了下面的方法,用另一个参数来应对不同的重量
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) { List<Apple> result = new ArrayList<Apple>(); For(Apple apple:inventory){ if (apple.getWeight() > weight) { result.add(apple); } } return result; } 复制代码
解决方案可以,但是请注意,复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿不好,因为它打破了DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则。如果想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。
可以将颜色和重量结合为一个方法,称为filter。不过就算这样,还是需要一种方式来区分想要筛选哪个属性。可以加上一个标志来区分对颜色和重量的查询。
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> greenApples = filterApples(inventory, "green", 0, true);、 List<Apple> heavyApples = filterApples(inventory, "", 150, false); … 复制代码
4、行为参数化
行为参数化就是可以帮助处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被程序的其他部分调用,这意味着可以推迟这块代码的执行例如,可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对的选择标准建模:考虑的是苹果,需要根据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方法迭代集合的逻辑与要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
5、根据抽象条件筛选
利用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方法的对象来传递布尔表达式的。
6、对付啰嗦
人们都不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给filterApples方法的时候,不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次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()); } } public class FilteringApples { public static void main(String... args) { List<Apple> inventory = Arrays.asList(new Apple(80, "green"),new Apple(155, "green"), new Apple(120, "red")); List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate()); List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate()); } 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; } } 复制代码
费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以同时声明和实例化一个类。它可以帮助进一步改善代码,让它变得更简洁。但这也不完全令人满意。
6.1 匿名类
匿名类和熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许同时声明并实例化一个类。换句话说,它允许随用随建。
6.2 使用匿名类
下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { public boolean test(Apple apple){ return "red".equals(apple.getColor()); } }); 复制代码
但匿名类还是不够好。它往往很笨重,因为它占用了很多空间,很多模板式代码
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { public boolean test(Apple a){ return "red".equals(a.getColor()); } }); 复制代码
6.3、使用Lambda 表达式
上面的代码在Java8里可以用Lambda表达式重写为下面的样子:
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor())); 复制代码
不得不承认这代码看上去比先前干净很多,因为它看起来更像问题陈述本身了,从下图可以看到几种方法的对比:
7、将List 类型抽象化
在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:
public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p){ 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); 复制代码
现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!
8、总结
跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:
- 使用Java 8,可以减少冗长的代码,让代码更易于理解
- 通过方法引用和Stream API,代码会变得更直观
利用Lambda表达式、方法引用以及Stream改善程序代码的可读性:
- 重构代码,用Lambda表达式取代匿名类
- 用方法引用重构Lambda表达式
- 用Stream API重构命令式的数据处理