Java8新特性1:行为参数化

背景

在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。

案例

通过逐步改进案例,来熟悉什么是“行为参数化”。

需求:农民希望筛选出1. 绿色的苹果,2. 重量大于150的苹果 3….

方案1

可能的代码如下:

/**
* 根据颜色筛选苹果
* @param apples
* @param color
* @return
*/
public static List<Apple> filterByColor(List<Apple> apples, String color) {
List<Apple> res = new ArrayList<>();
for (Apple apple : apples) {
if(apple.getColor().equals(color)) {
res.add(apple);
}
}
return res;
}

/**
* 根据重量筛选苹果
* @param apples
* @param weight
* @return
*/
public static List<Apple> filterByWeight(List<Apple> apples, int weight) {
List<Apple> res = new ArrayList<>();
for (Apple apple : apples) {
if(apple.getWeight() > weight) {
res.add(apple);
}
}
return res;
}

农民通过调用来获得答案

filterByColor(apples, "green");
filterByWeight(apples, 150);

这样做打破了DRY(Don’t RepeatYourself,不要重复自己)的软件工程原则。这样做出现了大量的重复代码,当你想要修改某些逻辑,比如优化遍历语句时,你需要修改所有方法中的代码。

另一种方案是,把所有参数都整合到一个方法中去,签名如下:public static List<Apple> filterByColor(List<Apple> apples, String color, int weight)

这样做不仅笨拙,而且面对变化不能做出调整,如果有新的筛选条件,比如产地,大小,形状等怎么办?

方案2:行为参数化

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

public interface ApplePredicate {
boolean test(Apple apple);
}

现在我们就可以用ApplePredicate的多个实现代表不同的选择标准:

public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。

但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。

public static List<Apple> filterApples(List<Apple> apples, ApplePredicate p) {
List<Apple> res = new ArrayList<>();
for (Apple apple : apples) {
if(p.test(apple)) {
res.add(apple);
}
}
return res;
}

注:这样做有一点缺憾就是,我明明重要的只有一个test方法,但是filterApples只能接受对象,所以我们不得不把test方法封装在ApplePredicate对象中进行传递。后面会使用Lambda表达式进行简化。

方案2-1:使用匿名类改进

对于以上代码的调用我们要费很大劲,创建ApplePredicate的两个实现类,然后把这两个类实例化传入到filterApples方法中去。

filterApples(apples, new AppleGreenColorPredicate());

我们可以使用匿名类简化这一流程。以筛选出绿色苹果为例,改进后的代码如下:

filterApples(apples, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
});

但匿名类还不够好:

  • 笨重:每次实现新的new ApplePredicate()都要写很多模板代码...test...
  • 费解:这里不过多解释

因此我们可以使用Lambda表达式让代码更干净

方案2-2:使用Lambda表达式

使用Lambda表达式让上述代码重写为:

filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()) );

练习

1

将苹果按照重量由大到小排序

//对苹果按重量排序,使用java8 Collections.sort
//使用匿名类
Collections.sort(apples, new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight() - a2.getWeight();
}
});

//使用lambda表达式
Collections.sort(apples, (Apple a1, Apple a2) -> a1.getWeight() - a2.getWeight() );

参考资料

  1. 《Java 8实战》
文章作者: Met Guo
文章链接: https://guoyujian.github.io/2022/05/05/Java8%E6%96%B0%E7%89%B9%E6%80%A71%EF%BC%9A%E8%A1%8C%E4%B8%BA%E5%8F%82%E6%95%B0%E5%8C%96/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Gmet's Blog