行为参数化是指一个方法的功能,部分或全部由传递给这个方法的某个或多个参数决定,但这些参数不是一般意义上的值(一个字符串或数字),它代表了一个具体的行为,其本质是代码传递,表现可能有多种:对象、匿名类、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程序员都应该学会使用它。
一言以蔽之,行为参数化使我们的代码能够更好地适应不断变化的要求,很大程度上减轻了我们程序员未来的工作量