天天看点

从强耦合到行为化参数最终到 Lambda 表达式从强耦合到行为化参数最终到 Lambda 表达式

从强耦合到行为化参数最终到 Lambda 表达式

Java 语法层面上的这种变化应当站在历史的角度上来考虑,站在后 Java8 时代,一切并非是理所当然。

1. 强耦合实现

 首先给出本次讨论的背景。农民希望你能够进行苹果库存的筛选,不过他作为甲方,需求经常会改变。

筛选一个库房的所有苹果实际上可以分为两个要素:

  • 筛选苹果的条件;
  • 迭代集合逻辑;

 在强耦合中,两个要素是捆绑在一起的,比如你要实现筛选颜色为绿色的苹果,代码就会如下编写:

public static List<Apple> filterGreenApples(List<Apple> inventory) {
  List<Apple> result = new ArrayList<Apple>();
  for (Apple apple : inventory) {//迭代集合逻辑
    if ("green".equals(apple.getColor()) {//筛选苹果的条件
      result.add(apple);
    }
        } return result;
  }           

复制

 为什么筛选条件也可以被视为强耦合?

  • 首先,依赖倒置、控制反转的概念出现于 Lambda 表达式之前是可以从时间线上证明的。Spring MVC 2002 年就出现了,而 Java8 则是在 2014 年才推出。而 Spring 的一大特色就是依赖倒置、控制反转,这说明依赖倒置可以不由 Lambda 表达式实现,后者是前者的一种优雅实现。
  • 其次,如果把条件本身看做是对象(虽然筛选条件写在 if 语句中,但是完全可以将其抽象为对象),将条件内嵌于迭代逻辑中,就是一种耦合性很强的依赖关系。而将其独立出来,作为方法的参数,实际上就是实现了去耦合,实现了依赖注入。

强耦合的缺陷:

 一旦需求改变,比如农民要求:将筛选条件修改为重量大于 150 g,那么在强耦合的代码逻辑下,你不得不重写一个类似的方法:

filterApplesByWeight(List<Apple> inventory,int weight)

,你会发现你仅仅是为了修改一个的筛选条件就要重写一个方法。一旦有新需求,你就要写新方法。所以需要依赖导倒置、控制反转来帮助我们实现去耦合。

2. 去耦合的行为化参数

 依赖倒置在 Java 中最广泛的实现方式便是控制反转。控制反转的一大特色就是如果 A 在运行时依赖于 B,那么在设计时 B 依赖于 A。控制反转常见的实现方式便是将 B 以方法参数的形式传入 A。

 这里 A 是迭代集合逻辑,B 是筛选条件。

 下面假设你现在处于 Java8 语法出现以前面临这个去耦合的实现,你该怎么做呢?Java 是一切都是对象(除了基本数据类型),那么筛选条件也必然只能利用对象来实现,下面则是具体的做法:

  • 将筛选条件抽象为 Predicate(谓词,谓词就是返回一个 Boolean 的条件判决抽象表示)接口;
  • 通过将方法参数设计为谓词类型的 filter 方法,filter 利用 Predicate 实现迭代集合逻辑;

这样一来,按条件对苹果库存进行筛选就变成了如下的解耦方式:

//首先是两个筛选条件类,分别是对谓词接口的实现
//1. 谓词类1:筛选重量大于 150g 的苹果
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
//2. 谓词类2:筛选颜色为绿色的苹果
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

//其次是依赖导致的 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;

    }           

复制

 此时 Predicate 接口的实例的就是一种行为化参数,行为化参数的定义是:就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。由于 Java 中一切皆对象,行为也自然需要一个实例来封装。

 但是,此处利用行为参数 Predicate 实现的筛选苹果库存仅仅实现了去耦合的目的。但是代码数量并没有变少。详细点说:

  • 去耦合之前,为了一个新的需求,你需要写一个 “筛选苹果的条件 + 迭代集合逻辑” 对;
  • 去耦合后,为了一个新的需求,你虽然不用重写迭代结合逻辑,但是为了封装“筛条件的方法”却需要声明一个新的 Predicate 实现类;

为了避免声明很多只要实例化一次的类,Java 在 JDK8 之前提供了匿名内部类。

 Java 匿名类机制,它可以让你同时声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。在 JDK8 之前,匿名内部类是向类传递方法的最主要现手段。提一句 Android,因为其没有支持 JDK8 所以 Android 中匿名内部类是其最简洁实现行为化参数的方式。

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

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

复制

 匿名内部类提供了一种同时声明类和实例化类的功能,这是一种进步,至少从代码上看,没有纷繁复杂的类声明了,但是其还不够简洁。。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。在只需要传递一段简单的代码时(例如表示选择标准的 boolean 表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如 Predicate 中的 test 方法或是 EventHandler 中的 handler 方法)。

 从接口类声明下的依赖倒置去耦合到匿名内部类,无论哪种方式你都只能将方法封装在一个实例中传递,而 Lambda 表达式的出现就是为了去掉这层冗余的封装,无封装地将方法本身作为行为参数传递。

3. Lambda 表达式

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

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

复制

 Lambda 表达式的语法在这里我并不想多提,但是显而易见的是我们得到了一种耦合度最低,代码整洁度最高的行为参数传递实现方式。这样看来,Java 的确一直在吸收其他语言的优点,因为其不断进步,才有现在 Java 在工业界的繁荣。

从语言设计层面上看上述历史演变:

从强耦合到行为化参数最终到 Lambda 表达式从强耦合到行为化参数最终到 Lambda 表达式

 下图摘自 《Java8 实战》,体现了 Java 在行为参数实现上历史发展以及性能对比:

从强耦合到行为化参数最终到 Lambda 表达式从强耦合到行为化参数最终到 Lambda 表达式