java杂谈之浅谈行为参数化

1、 前言

在软件工程中,一个众所周知的问题就是,不管做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存的。这位农民可能想有一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”又过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”要如何应对这样不断变化的需求?理想的状态下,应该把工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。

2、应对不断变化的需求

编写能够应对变化的需求的代码并不容易。让我们来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,必须实现一个从列表中筛选绿苹
果的功能。

2.1 小试牛刀,筛选苹果

假设有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。可能想要选出所 有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念,可能会写这样一个方法filterGreenApples:

 public static List filterGreenApples(List inventory) {List result = new ArrayList<>();for (Apple apple : inventory) {if ("green".equals(apple.getColor())) {result.add(apple);}}return (result);
}

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

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

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

public static List filterApplesByColor(List inventory, String color) {List result = new ArrayList();for (Apple apple : inventory) {if (apple.getColor().equals(color)) {result.add(apple);}}return result;
}

现在,只要像下面这样调用方法,农民朋友就会满意了:

List greenApples = filterApplesByColor(inventory, "green");
List redApples = filterApplesByColor(inventory, "red");
...

让我们把例子再弄得复杂一点儿。这位农民又跑回来说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”

作为软件工程师,早就想到农民可能会要改变重量,于是写了下面的方法,用另一个参数来应对不同的重量

public static List filterApplesByWeight(List inventory, int weight) {List result = new ArrayList();For(Apple apple:inventory){if (apple.getWeight() > weight) {result.add(apple);}}return result;
}

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

可以将颜色和重量结合为一个方法,称为filter。不过就算这样,还是需要一种方式来区分想要筛选哪个属性。可以加上一个标志来区分对颜色和重量的查询。

3、对能想到的每个属性做筛选

一种把所有属性结合起来的笨拙尝试如下所示:

public static List filterApples(List inventory, String color, int weight, boolean flag) {List result = new ArrayList();for (Apple apple : inventory) {if ((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {result.add(apple);}}return result;
}

可以这么用(但真的很笨拙):

List greenApples = filterApples(inventory, "green", 0, true);、
List 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 filterApples(List inventory,ApplePredicate p){List 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 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 inventory = Arrays.asList(new Apple(80, "green"),new Apple(155, "green"), new Apple(120, "red"));List heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());List greenApples = filterApples(inventory, new AppleGreenColorPredicate());}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;}
}

费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以同时声明和实例化一个类。它可以帮助进一步改善代码,让它变得更简洁。但这也不完全令人满意。

6.1 匿名类

匿名类和熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许同时声明并实例化一个类。换句话说,它允许随用随建。

6.2 使用匿名类

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

List redApples = filterApples(inventory, new ApplePredicate() {public boolean test(Apple apple){return "red".equals(apple.getColor());}
});

但匿名类还是不够好。它往往很笨重,因为它占用了很多空间,很多模板式代码

List redApples = filterApples(inventory, new ApplePredicate() {public boolean test(Apple a){return "red".equals(a.getColor());}
});

6.3、使用Lambda 表达式

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

List result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

不得不承认这代码看上去比先前干净很多,因为它看起来更像问题陈述本身了,从下图可以看到几种方法的对比:

7、将List 类型抽象化

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

public interface Predicate{boolean test(T t);
}public static  List filter(List list, Predicate p){List result = new ArrayList<>();for(T e: list){if(p.test(e)){result.add(e);}}return result;
}

现在可以把filter方法用在香蕉、桔子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:

List redApples =filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));List evenNumbers =filter(numbers, (Integer i) -> i % 2 == 0);

现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!

8、总结

跟之前的版本相比较,Java 8的新特性也可以帮助提升代码的可读性:

  • 使用Java 8,可以减少冗长的代码,让代码更易于理解
  • 通过方法引用和Stream API,代码会变得更直观

利用Lambda表达式、方法引用以及Stream改善程序代码的可读性:

  • 重构代码,用Lambda表达式取代匿名类
  • 用方法引用重构Lambda表达式
  • 用Stream API重构命令式的数据处理


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部