Java函数式编程(二):通过行为参数化传递代码

3,942 阅读6分钟

不断变化的需求

  • 在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变

  • 比如之前的苹果的例子

    • 找绿色苹果
    • 找红色苹果
    • 找大于150G的苹果
    • 找大于150G的绿苹果
    • 找大于150G的红苹果
    • 找大于150G且小于400G的红苹果
  • 按照上述经常变动,且都需要的,那要写多少方法?

  • 我们将上述的“需求”看做是一种行为。那在进行处理的时候,只需要传递这种“行为”,那么所有的行为方法都不需要写了,简直一劳永逸。

  • 我们称之为行为,而传递的行为叫做行为化参数。行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。

  • 一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。

  • 假如你要处理一个集合,会写这样的一个方法

    • 可以对列表中的每个元素做“某件事”
    • 可以在列表处理完后做“另一件事”
    • 遇到错误时可以做“另外一件事”
  • 这就是行为化,如果还不理解,继续往下看。

  • 打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go,它可以接受不同的新行为作为参数,然后去执行。

  • 如果对上一篇有过阅读、或者学习过用过Stream对数据的操作,那是能够体会到一定的编程的爽快感。

应对不断变化的需求

  • 这里会使用一个案例,并且逐步改善
  • 筛选绿苹果
public static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> arrayList = new ArrayList<>();
    for (Apple apple : apples) {
        // 筛选
        if ("green".equals(apple.getColor())) {
            arrayList.add(apple);
        }
    }
    return arrayList;
}
  • 现在做的就是筛选绿苹果的,现在需求变更,需要筛选红苹果,那还要把这个方法拷贝一下,把Green换成Red。很明显,违反了DRY(Do not Repeat Youself)
  • 怎么做呢?把颜色作为参数
  • 因为只是改变颜色,所以把颜色传递进去,可以省去一个方法
public static List<Apple> filterApplesByColor(List<Apple> apples, String color) {
    List<Apple> arrayList = new ArrayList<>();
    for (Apple apple : apples) {
        if (color != null && color.equals(apple.getColor())) {
            arrayList.add(apple);
        }
    }
    return arrayList;
}
  • 上述已经完成了操作,此时又有新的需求:要区分重的苹果和轻的苹果,同理
public static List<Apple> filterApplesByWeight(List<Apple> apples, int Weight) {
    List<Apple> arrayList = new ArrayList<>();
    for (Apple apple : apples) {
        if (apple.getWeight() > Weight) {
            arrayList.add(apple);
        }
    }
    return arrayList;
}
  • 但是还是复制了代码,对整个输出行为有影响的只有那一行判断条件
  • 再次尝试:对你能所想到的每个属性进行筛选
public static List<Apple> filterApples(List<Apple> apples, int weight, String color, boolean flag) {
    List<Apple> arrayList = new ArrayList<>();
    for (Apple apple : apples) {
        if ((flag && apple.getWeight() > weight) || (!flag && color != null && color.equals(apple.getColor()))) {
            arrayList.add(apple);
        }
    }
    return arrayList;
}
  • 这种代码已经脏的不行了,传递weight、color、flag,用flag判断到底哪一个属性生效,那么可以遇见的是,如果Apple增加了其他的字段:甜度、水分、生产地......
  • 那么这个方法已经无法维护了,如果需要更加复杂的查询,也无法编写。而且,向一个方法传递一个boolean是非常危险的事情,有boolean就意味着有分支。

行为参数化

  • 经过上面的案例,我们需要一种更好的方式来应对变化的需求。

  • 现在对上述的需求进行建模之前,先看一下可能的需求案例

    • 找绿色苹果
    • 找红色苹果
    • 找大于150G的苹果
    • 找大于150G的绿苹果
    • 找大于150G的红苹果
    • 找大于150G且小于400G的红苹果
  • 这是上文提到的,我们将所有的过滤条件去掉,那么这些需求案例,就是 找满足xxx属性的苹果

  • 那么建模如下:你考虑的是苹果,需要根据Apple的某些属性,返回一个满足条件的boolean值。

  • 我们称之为谓词,定义接口如下

public interface ApplePredicate {
    boolean test(Apple apple);
}
  • 然后可以使用ApplePredicate的多个实现来执行不同的行为了
class AppleHeavyWeightPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

class AppleRedPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
}
  • 这些实现,就是不同的筛选条件(filter),也相当于不同的策略,算法族就是 ApplePredicate
  • 但是怎么利用这种不同的实现呢?需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。
  • 这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
  • 那么现在就来修改之前的代码
public class Test5 {
    public static void main(String[] args) {
        System.out.println(filterApples(AppleClient.getApples(), new AppleHeavyWeightPredicate()));
        System.out.println(filterApples(AppleClient.getApples(), new AppleRedPredicate()));
    }

    public static List<Apple> filterApples(List<Apple> apples, ApplePredicate applePredicate) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if (applePredicate.test(apple)) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
  • 现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
  • 这样一来,任何需求的变更,只需要增加相应的实现类即可。filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。但是会有一个问题:类膨胀
  • 代码传递/行为
  • 在上述的实现中,我们发现,对于整个测试,唯一重要的就是test方法的实现,这个方法决定了需要怎么样进行过滤。
  • 所以在传递行为的时候,我们可以直接使用匿名内部类来传递,如下
public class Test5 {
    public static void main(String[] args) {
        System.out.println(filterApples(AppleClient.getApples(), new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return apple.getWeight() > 150;
            }
        }));
        System.out.println(filterApples(AppleClient.getApples(), new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        }));
    }

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

  • 但是多个很多无用的代码,此时再将匿名内部类更改为Lambda表达式即可
public class Test5 {
    public static void main(String[] args) {
        System.out.println(filterApples(AppleClient.getApples(), apple -> apple.getWeight() > 150));
        System.out.println(filterApples(AppleClient.getApples(), apple -> "red".equals(apple.getColor())));
    }

    public static List<Apple> filterApples(List<Apple> apples, ApplePredicate applePredicate) {
        List<Apple> arrayList = new ArrayList<>();
        for (Apple apple : apples) {
            if (applePredicate.test(apple)) {
                arrayList.add(apple);
            }
        }
        return arrayList;
    }
}
  • 如上,你只需要关注行为,也就是你需要test的实现即可。
  • 多种行为/一个参数
  • 正如我们先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的
  • 新需求,对苹果进行遍历,然后对其进行格式化输出
  • 这个需求需要定义一个新的方法,prettyPrintApple,参照上面的筛选苹果的案例,整体的执行框架如下
public static void prettyPrintApple(List<Apple> apples,???){
    for (Apple apple : apples) {
        String output = ???.???(apple);
        System.out.println(output);
    }
}
  • 其中???的部分就是我们要填充的行为,其比较简单:输入一个Apple,然后输出一个String,那么就来定义这样的一个接口 FormatApple
public interface AppleFormat {
    String accept(Apple apple);
}
  • 那么 prettyPrintApple 就可以实现了,如下
public static void prettyPrintApple(List<Apple> apples,AppleFormat appleFormat){
    for (Apple apple : apples) {
        String output = appleFormat.accept(apple);
        System.out.println(output);
    }
}
  • 这样就可以表示多种行为了
public class Test6 {
    public static void main(String[] args) {
        prettyPrintApple(AppleClient.getApples(), apple -> {
            return "颜色:" + apple.getColor() + ",重量:" + apple.getWeight();
        });
        prettyPrintApple(AppleClient.getApples(), apple -> {
            return "颜色:" + apple.getColor();
        });
    }

    public static void prettyPrintApple(List<Apple> apples, AppleFormat appleFormat) {
        for (Apple apple : apples) {
            String output = appleFormat.accept(apple);
            System.out.println(output);
        }
    }
}
  • 到此为止,我们可以将类、匿名类、Lambda进行行为参数化,替代了之前的案例中的值参数化。
  • 现在我们发现上述的ApplePredicate只能处理Apple,且filterApples也只能处理Apple,所以我们将这部分使用泛型进行抽象化,如下
public interface Predicate<T> {
    boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (predicate.test(e)){
            result.add(e);
        }
    }
    return result;
}
  • 通过这样的处理,所有类型的数据,都可以这样进行筛选出结果。
  • 其他的行为参数化案例
  • 对集合进行排序,根据苹果颜色排序或者根据大小排序
  • 在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)来进行排序
  • sort的行为可以用java.util.Comparator对象来参数化,它的接口如下
public interface Comparator<T>{
    int compare(T o1, T o2);
}
  • 因此,你可以随时创建Comparator的实现,用sort方法表现出不同的行为。比如,你可以使用匿名类,按照重量升序对库存排序
public class Test8 {
    public static void main(String[] args) {
        AppleClient.getApples().sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight() - o2.getWeight();
            }
        });
    }
}
  • 或者是Lambda表达式
public class Test8 {
    public static void main(String[] args) {
        AppleClient.getApples().sort((o1, o2) -> o1.getWeight() - o2.getWeight());
    }
}
  • 如果对匿名类转Lambda表达式不熟悉,我们会在下面进行讲解。

小结

  • 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
  • Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。