应对不断变化的需求

编写能够应对变化的需求的代码并不容易。
来看一个例子,然后逐步改进这个例子,以展示一些让代码更灵活的最佳做法。
就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。
初试牛刀: 筛选绿苹果
第一个解决方案可能是下面这样的:

  1. public static List<Apple> filterGreenApples (List<Apple> inventory) {
  2. List<Apple> result = new ArrayList<Apple>() ;//累积苹果的列表
  3. for (Apple apple: inventory) {
  4. if( "green" . equals (apple . getColor() )){
  5. //仅仅选出绿苹果
  6. result. add(apple) ;
  7. }
  8. }
  9. return result;
  10. }

但是现在农民改主意了,他还想要筛选红苹果。
该怎么做呢?
简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。
然而,要是农民想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。
一个良好的原则是在编写类似的代码之后,尝试将其抽象化。

再展身手:把颜色作为参数

一种做法是给方法加一个参数,把颜色变成参数,这样就能灵活地适应变化了:

  1. public static List<App1e> filterApplesByColor (List<App1e> inventory, String color) {
  2. List <App1e> result = new ArrayList<Apple>() ;
  3. for (Apple apple: inventory) {
  4. if ( apple. getColor() . equals(color) ) {
  5. result. add(apple) ;
  6. }
  7. }
  8. return result;
  9. }

让我们把例子再弄得复杂一点。
这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是 重量大于150克。”
作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:

  1. public static List<Apple> filterAppl esByWeight (List<App1e> inventory,int weight){
  2. List<Apple> result = new ArrayList<App1e>();
  3. for (Apple apple: inventory) {
  4. if ( apple.getweight() > weight ) {
  5. result. add (app1e) ;
  6. }
  7. }
  8. return result;
  9. }

解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。
这有点儿令人失望,因为它打破了DRY ( Don’t Repeat Yourself,不要重复自己)的软件工程原则。
如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。
从工程工作量的角度来看,这代价太大了。

行为参数化

需要一种比添加很多参数更好的方法来应对变化的需求。
后退一步来看看更高层次的抽象。
一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗? )
来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。
让我们定义一个接口来对选择标准建模:

  1. public interface ApplePredicate{
  2. boolean test (Apple app1e) ;
  3. }

现在你就可以用ApplePredicate的多个实现代表不同的选择标准了,比如

  1. public class App1 eHeavyWeight Predicate implements ApplePredicate{ <
  2. public boolean test (Apple apple) {
  3. //仅仅选出重的苹果
  4. return apple. getWeight() > 150;
  5. }
  6. }
  7. public class App1 eGreenColorPredicate impl ements ApplePredicate{
  8. public boolean test (Apple apple) {
  9. //仅仅选出绿苹果
  10. return "green" . equals (apple. getColor() ) ;
  11. }
  12. }

![(GT0EZO`NNREGE3QCH3}CG.png
刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。
在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
但是,该怎么利用ApplePredicate的不同实现呢?
你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。
这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。

第三次尝试:根据抽象条件筛选

利用ApplePredicate改过之后,filter 方法看起来是这样的:

  1. public static List<Apple> filterApples (List<App1e> inventory, ApplePredicate p) {
  2. List<Apple> result = new ArrayList<>() ;
  3. for (Apple apple: inventory) {
  4. if(p. test (apple)){
  5. result. add(apple) ;
  6. //谓词对象封装了测试苹果的条件
  7. }
  8. }
  9. return result;
  10. }

传递代码/行为
这段代码比第一次尝试的时候灵活多了,读起来、用起来也更容易!
现在可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。
比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。
你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:

  1. public class App1 eRedAndHeavyPredicate impl ements ApplePredicate{
  2. public boolean test (App1e app1e) {
  3. return "red" . equals (apple. getColor())
  4. && apple. getWeight() > 150;
  5. }
  6. List<Apple> redAndHeavyApples = filterApples (inventory, new Appl eRedAndHeavyPredicate()) ;

filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。
换句话说,你把filterApples方法的行为参数化了!
请注意,在上一个例子中,唯一重要的代码是test方法的实现。
如图2-2所示,正是它定义了filterApples方法的新行为。
但令人遗憾的是,由于该filterApples方法只能接受对象,
所以你必须把代码包裹在ApplePredicate对象里。
你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。
DTN3G5CWOY{@{7WSZTPB56A.png
多种行为,一个参数
]}SHDG9UUD8W4090MK7IA)2.png

匿名类

我们都知道,人们都不愿意用那些很麻烦的功能或概念。
目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。
这很哕嗦,也很费时间。

第四次尝试:使用匿名类

下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

  1. List<App1e> redApples = filterApples(inventory, new ApplePredicate() { //直接内联参数化filterapples方法的行为
  2. public boolean test (Apple apple) {
  3. return "red" . equals (apple. getColor() ) ;
  4. }
  5. }) ;

但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。
![ALU(`}E58R52SY2C8$1_KA.png

第二,很多程序员觉得它用起来很让人费解。
比如,下面的测验展示了一个经典的Java谜题,它让大多数程序员都措手不及。
HLC84IKX0XXR1SF6S90M1MC.png
(MEWO6}VMVKB`4M`B4(KUFB.png

第六次尝试:使用Lambda表达式

上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:

  1. List<Apple> result =
  2. filterApples (inventory, (Apple app1e) -> "red" . equals (apple .getCo1or())) ;

不得不承认这代码看上去比先前干净很多。
这很好,因为它看起来更像问题陈述本身了。
我们现在已经解决了啰嗦的问题。
![7764~N%M]E]KYG%_PQ~G%Y.png

第六次尝试:将List类型抽象化

在通往抽象的路上,还可以更进一步。
目前,filterApples方 法还只适用于Apple。
你还可以将List类型抽象化,从而超越眼前要处理的问题:

  1. public interface Predicate<T>{
  2. boolean test(T t) ;
  3. //引入类型
  4. }
  5. public static <T> List<T> filter(List<T> list Predicate<T> p) {
  6. List<T> result = new ArrayList<>() ;
  7. for(T e: list) {
  8. if(p.test(e)) {
  9. result.add(e) ;
  10. }
  11. }
  12. return result;
  13. }

现在可以把filter方法用在香蕉、桔子、Integer或是string的列表上了。
现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的。