行为参数化是指一个方法的功能,部分或全部由传递给这个方法的某个或多个参数决定,但这些参数不是一般意义上的值(一个字符串或数字),它代表了一个具体的行为,其本质是代码传递,表现可能有多种:对象、匿名类、java8里的Lambda表达式(或方法引用)等。本篇文章将以一个例子,为了满足不断复杂化的需求,层层递进,逐步演示从值传递到java8行为参数化这个简单到高级的过程,以展示行为参数化的必要与优势。
以前做外包项目的时候,似乎永远不知道客户会在什么时候提出新需求或需求变更,如果没有行为参数化或类似行为参数化的东西,客户的需求小有改动可能会带来代码上较大的变动,或为了一个小的新需求复制粘贴好几个类或方法,但行为参数化对于某些需求的变化或新增可以做到以不变应万变。
有个农民客户要求筛选出绿色的苹果:
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;}
这样的代码没有任何问题,“很好地”实现了农民的需求,但是突然农民说我需要筛选出红色的苹果,怎么办?复制粘贴虽然简单,但农民可能还会提出要筛选深红色的苹果。于是为了防止农民再提出筛选其它颜色的苹果,有了下面的代码:
public static List<Apple> filterApplesByColor(List<Apple> inventory,String color) {List<Apple> result = new ArrayList<Apple>();for (Apple apple: inventory){if ( apple.getColor().equals(color) ) {result.add(apple);}}return result;}
农民很高兴:“无论我想要什么颜色的苹果,都能给我选出来了”。
但没过几天,农民又发愁了,没有选出轻或重的苹果这个功能,于是有了下面的过滤方法:
public static List<Apple> filterApplesByWeight(List<Apple> inventory,int weight) {List<Apple> result = new ArrayList<Apple>();for (Apple apple: inventory){if ( apple.getWeight() > weight ){result.add(apple);}}return result;}
这里农民是高兴了,因为它的需求实现了,但对于我们程序员来说有了太多的重复代码,违反了DRY软件工程原则。
于是,想办法去除重复的代码,将颜色与重量过滤集中在一个方法中:
public static List<Apple> filterApples(List<Apple> inventory, String color,int weight, boolean flag) {List<Apple> result = new ArrayList<Apple>();for (Apple apple: inventory){//flag标志用于区分是颜色筛选还是重量筛选if ( (flag && apple.getColor().equals(color)) ||(!flag && apple.getWeight() > weight) ){result.add(apple);}}return result;}
这次尝试虽然没有重复的代码了,但方法看起来很糟糕,不易理解,而且无法满足农民可能提出的更多过滤需要,比如大小、形状、产地,更不用说可能出现的组合筛选需求了。因此需要作出改变,既不想每一个过滤需求都写一个对应的过滤方法,又不想写一个巨大而糟糕的方法来实现多个筛选需求,怎么办?
前面几次尝试,在过滤时传递的是具体的值(值传递),如string类型的颜色、int类型的重量、boolean,代表的只是苹果的一个属性或状态,更糟糕的它们可能会有无数个组合,每个组合都对应了一个新需求,我们是无法单纯地靠值传递来设计出优雅的过滤方法的!
由于每一次筛选都是一个具体的行为,行为决定了过滤的结果,那直接将行为传递至过滤方法呢?
以下代码是筛选行为的封装:
/***对筛选标准建立模型*/public interface ApplePredicate{//test方法决定了apple是否满足我们的筛选条件boolean test (Apple apple);}/***代表了选出较重苹果的这一行为*/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:
public static List<Apple> filterApples(List<Apple> inventory,ApplePredicate p){List<Apple> result = new ArrayList<>();for(Apple apple: inventory){//ApplePredicate对象封装了测试苹果的条件//满足条件即是我们需要选出的结果if(p.test(apple)){result.add(apple);}}return result;}
这样针对不同的过滤需求(行为),我们只需要定义不同的ApplePredicate的实现类,即可使用同一个过滤方法筛选出我们想要的苹果,比如筛选出红色且较重的苹果:
public class AppleRedAndHeavyPredicate implements ApplePredicate{public boolean test(Apple apple){return "red".equals(apple.getColor())&& apple.getWeight() > 150;}}List<Apple> redAndHeavyApples =filterApples(inventory, new AppleRedAndHeavyPredicate());
无论筛选需求的组合多么复杂,都只有一个行为参数,至此,filterApples方法已经能够应对不断变化的筛选需求了。但每一个筛选行为都需要定义一个类,是不是太啰嗦了?
使用匿名类省去这些类的声明,会不会简单一点:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {public boolean test(Apple apple){return "red".equals(apple.getColor());}});
匿名类虽然省去了大量行为类的声明,但是依然笨重(模板化的代码占了4行,实际的筛选代码却只有一行)且匿名类的使用可能会让人费解。
下面的代码执行时会有什么样的输出呢, 4、 5、 6还是42?
public class MeaningOfThis{public final int value = 4;public void doIt(){int value = 6;Runnable r = new Runnable(){public final int value = 5;public void run(){int value = 10;System.out.println(this.value);}};r.run();}public static void main(String...args){MeaningOfThis m = new MeaningOfThis();m.doIt();}}
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
是不是干净了很多?是不是一眼就能看出想要筛选的是什么?
注:Lanbda表达式会在下篇中详细介绍,这篇的主题是行为参数化
以上各种方式实现筛选需求的优与劣总结如下:
第七次尝试:将 List 类型抽象化,行为参数化趋于完美
前方的过滤方法只能过滤Apple,我们可以使用泛型进一步抽象化,使其可以过滤Orange、Banana等任何实体:
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;}
使用:
//筛选出红苹果List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));//筛选出偶数List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
这样即灵活又简洁的代码,在java8之前想都没想过!由此可见,java8魅力无限,每个java程序员都应该学会使用它。
一言以蔽之,行为参数化使我们的代码能够更好地适应不断变化的要求,很大程度上减轻了我们程序员未来的工作量