【Java 8】行为参数化

本文参考书籍《Java 8实战》,陆明刚、劳佳  译,如有侵权,请联系删除!


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

 应对不断变化的需求

假设有一批苹果,现在需要筛选出其中的绿苹果,代码如下:

    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;
    }

如果现在又需要选出其中的红苹果该怎么办呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改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;
    }

上面的代码是基于颜色进行筛选的,那如果需要按照重量区分轻的苹果和重的苹果(重的苹果一般大于150克)呢?于是又写了下面的代码:

    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,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式来提升性能呢?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。

可以将颜色和重量结合为一个方法,称为filterApples。但是,我们需要一种方式来区分想要筛选哪个属性。可以加上一个标志来区分对颜色和重量的查询:

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

这个解决方案它仍然不能很好地应对变化的需求。如果要对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?

行为参数化

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

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

现在你就可以用ApplePredicate的多个实现代表不同的选择标准了。比如,仅仅选出重的苹果:

    static class AppleWeightPredicate implements ApplePredicate {
        public boolean test(Apple apple) {
            return apple.getWeight() > 150;
        }
    }

仅仅选出绿苹果:

    static class AppleColorPredicate implements ApplePredicate {
        public boolean test(Apple apple) {
            return "green".equals(apple.getColor());
        }
    }

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

但是,该怎么利用ApplePredicate的不同实现呢?需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或策略)作为参数,并在内部使用,来完成不同的行为。利用ApplePredicate改过之后, filterApples方法看起来是这样的:

    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;
    }

这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在我们可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法,代码灵活性大大提高!比如,如果要找出所有重量超过150克的红苹果,我们只需要创建一个类来实现ApplePredicate就行了:

    static class AppleRedAndHeavyPredicate implements ApplePredicate {
        public boolean test(Apple apple) {
            return "red".equals(apple.getColor())
                    && apple.getWeight() > 150;
        }
    }

到这里,我们已经完成了一件很酷的事情:filterApples方法的行为取决于通过ApplePredicate对象传递的代码。换句话说,filterApples方法的行为被参数化了!唯一重要的代码是test方法的实现,正是它定义了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象,所以必须把代码包裹在ApplePredicate对象里。这种做法就类似于在内联“传递代码”,因为我们是通过一个实现了test方法的对象来传递布尔表达式的。后面我们会看到,通过使用Lambda,可以直接把表达式"red".equals(apple.getColor())&&apple.getWeight() > 150传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。

行为参数化的好处在于可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样就可以重复使用同一个方法,给它不同的行为来达到不同的目的。

使用匿名类

目前,当要把新的行为传递给filterApples方法的时候,我们不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会使用一次的ApplePredicate对象。能不能做得更好呢?Java有一个机制称为匿名类,可以同时声明和实例化一个类,帮助我们进一步改善代码,让它变得更简洁。下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

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

即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),我们还是要创建一个对象,明确地实现一个方法来定义一个新的行为。在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化。下一章我们会看到Java 8的语言设计者通过引入Lambda表达式——一种更简洁的传递代码的方式——解决了这个问题。下面简单介绍一下Lambda表达式是怎么让代码更干净的。

使用Lambda

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

        List<Apple> redApples = filterApples(inventory, (Apple a) -> {
                    return a.getColor().equals("red");
                }
        );

不得不承认这代码看上去比先前干净很多。

将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;
    }

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

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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值