初试牛刀: 筛选绿苹果
第一个解决方案可能是下面这样的:
- 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;
- }
复制代码
但是现在还想要筛选红苹果, 你该怎么做呢? 简单的解决方法就是复制这个方法, 把名字改为 filterRedApple, 然后更改 if 条件来匹配红苹果. 然后, 要是还想筛选多种颜色: 浅绿色, 暗红色, 黄色等, 这种方法就应付不了了. 一个良好的原则是在编写类似的代码之后, 尝试将其抽象化.
再展身手: 把颜色作为参数
一种做法是给方法加一个参数, 把颜色变成参数, 这样就能灵活地适应变化了:
- public static List<Apple> filterApplesByColor(List<Apple> inventory,
- String color) {
- List<Apple> result = new ArrayList<>();
- for(Apple apple : inventory) {
- if(apple.getColor().equals(color)) {// 筛选苹果的条件
- result.add(apple);
- }
- }
- return result;
- }
复制代码
但是现在还想筛选重的苹果(质量大于 150g 的苹果), 于是你写了下面的方法, 用另一个参数来应对不同的重量:
- public static List<Apple> filterApplesByWeight(List<Apple> inventory,
- int weight) {
- List<Apple> result = new ArrayList<>();
- for(Apple apple : inventory) {
- if(apple.getWeight()> weight) {// 筛选苹果的条件
- result.add(apple);
- }
- }
- return result;
- }
复制代码
解决方案不错, 但是请注意, 你复制了大部分的代码来实现便利库存, 并对每个苹果应用筛选条件. 这有点儿令人失望, 因为它打破了 DRY(Don't Repeat Yourself, 不要重复自己)的软件工程原则. 如果你想要改变筛选遍历方式来提升性能呢? 那就得修改所有方法得实现, 而不是只改一个. 从工程工作量的角度来看, 这代价太大了. 你可以将颜色和重量结合为一个方法, 成为 filter. 不过就算这样, 你还需要一种方式来区分想要筛选哪个属性.
第三次尝试: 对你能想到的每个属性做筛选
一种把所有属性结合起来的笨拙尝试如下所示:
- public static List<Apple> filterApples(List<Apple> inventory,
- String color, int weight, boolean flag) {
- List<Apple> result = new ArrayList<>();
- 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);
复制代码
这个解决方案在差不过了. 首先, 客户端代码看上去糟透了. true 和 false 是什么意思? 此外, 这个解决方案不能很好地应对变化的需求. 如果要求你对苹果的不同属性做筛选, 比如大小, 形状, 产地等, 又怎么办? 而且, 如果要求组合属性筛选, 做更复杂的查询, 比如绿色的重苹果, 又该怎么办? 但如今这种情况下, 你需要一种更好的方式, 来把苹果的选择标准告诉你得 filterApples 方法. 让我们后退一步来看看更高层次的抽象. 一种可能的解决方案是对你的选择标准建模: 你考虑的是苹果, 需要根据 Apple 的某些属性来返回一个 Boolean 值. 我们把它称为谓词(即一个返回 Boolean 值的函数). 让我们定义一个接口来对选择标准建模:
- public interface ApplePredicate {
- boolean test(Apple apple);
- }
复制代码
现在你就可以用 ApplePredicate 的多个实现代表不同的选择标准了, 比如:
- // 筛选重量大于 150 的苹果的谓词
- 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());
- }
- }
复制代码
但是, 该怎么利用 ApplePredicate 呢? 你需要 filterApples 方法接受 ApplePredicate 对象, 对 Apple 做条件测试. 这就是行为参数化: 让方法接受多种行为 (或策略) 作为参数, 并在内部使用, 来完成不同的行为.
第四次尝试: 根据抽象条件筛选
利用 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;
- }
复制代码
你已经做成了一件很酷的事: filterApples 方法取决于你通过 ApplePredicate 对象传递的代码. 换句话说, 你把 filterApples 方法的行为参数化了! 请注意, 在上一个例子中, 唯一重要的代码事 test 方法的实现, 正是它定义了 filterApples 方法的新行为. 但令人遗憾的是, 由于该 filterApples 方法只能接受对象, 所以你必须把代码包裹在 ApplePredicate 对象里. 你的做法就类似于在内联 "传递代码", 因为你是通过一个实现了 test 方法的对象来传递布尔表达式的.
- public class AppleHeavyWeightPredicate implements ApplePredicate {
- public boolean test(Apple apple) {
- return apple.getWeight()> 150;
- }
- }
复制代码
把策略传递给策略方法: 通过不二表达式筛选封装在 ApplePredicate 对象内的苹果. 为了封装这段代码, 用了很多模板代码来包裹它(粗体显示)
我们都知道, 人们都不愿意用那些很麻烦的功能或概念. 目前, 当要把新的行为传递给 filterApples 方法的时候, 你不得不声明好几个实现 ApplePredicate 接口的类, 然后实例化好几个只会提到一次的 ApplePredicate 对象. 下面的程序总结了你目前看到的一切. 这真是很啰嗦, 很费时间. 费那么大劲儿真没必要, 能不能做的更好呢? Java 有一个机制称为匿名类, 它可以让你同时声明和实例化一个类. 它可以帮助你进一步改善代码, 让他变得更简洁. 但这也不完全令人满意. 匿名类: 匿名类和你熟悉的 Java 局部类 (块中定义的类) 差不多, 但匿名类没有名字. 它允许你同时声明并实例化一个类. 换句话说, 它允许你随用随建.
第五次尝试: 使用匿名类
下面的代码展示了如何通过创建一个匿名类实现 ApplePredicate 的对象, 重写筛选的例子:
- List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
- public boolean test(Apple apple) {
- return "red".equals(apple.getColor());
- }
- })
复制代码
GUI 应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是 Java FX API, 一种现代的 Java UI 平台)
- buuton.setOnAction(new EventHandler<ActionEvent>() {
- public void handler(ActionEvent event) {
- System.out.println("Woooo a click!");
- }
- });
复制代码
但匿名类还是不够好. 第一, 它往往很笨重, 因为它占用了很多空间. 还拿前面的例子来看, 如下面加粗代码所示:
- List<Apple> redApples = filterApples(inventory, new ApplePredicate(){
- public boolean test(Apple a){
- return "red".equals(a.getColor());
- }
- });
- buuton.setOnAction(new EventHandler<ActionEvent>() {
- public void handler(ActionEvent event) {
- System.out.println("Woooo a click!");
- }
- });
复制代码
第二, 很多程序员觉得它用起来很让人费解
第六次尝试: 使用 Lambda 表达式
上面的代码在 Java8 里可以用 Lambda 表达式重写为下面的样子:
- List<Apple> result =
- filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
复制代码
不得不承认这代码看上去比先前干净很多. 这很好, 因为它看起来更像问题陈述本身了. 我们已经解决了啰嗦的问题.
第七次尝试: 将 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;
- }
复制代码
你现在已经看到, 行为参数化是一个很有用的模式, 它能够轻松地适应不断变化的需求. 这种模式可以把一个行为 (一段代码) 封装起来, 并通过传递和使用创建的行为 (例如对 Apple 的不同谓词) 将方法的行为参数化. 前面提到过, 这种做法类似于策略设计模式. 你可能已经在实践中用过这个模式了. Java API 中的很多方法都可以用不同的行为来参数化. 这种方法往往与匿名类一起使用. 我们会展示三个例子, 这应该能帮助你巩固传递代码的思想: 用一个 Comparator 排序, 用 Runnable 执行一个代码块, 以及 GUI 事件处理.
来源: https://juejin.im/post/5b88e2c6e51d45389620959d