通过行为参数化传递代码

初试牛刀:筛选绿苹果

第一个解决方案可能是下面这样的:

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事件处理。

猜你喜欢

转载自juejin.im/post/5b88e2c6e51d45389620959d