如何使用行为参数化传递代码

在软件开发过程中,有一个比较头疼的问题,就是用户的需求会经常改变,这也使得我们总是要不断的去修改代码,从而浪费了不少时间和精力。如何应对不断变化的需求?如何降低软件开发的工作量?java8提出了一种新的软件开发模式:行为参数化;这能有效的帮助你处理频繁变更的需求。

那么,如何编写能够应对变化的需求的代码呢?下面我们通过不断改进一个示例代码来理解行为参数化传递的思想。

有这样一个问题:农场主要在农场库存中选出绿色的苹果。

对于这个问题,我们很容易的可以得到以下的解决方案:

1
2
3
4
5
6
7
8
9
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;
}

这样我们很容易的就将绿色的苹果筛选出来了。可是这时候农场主改变主意了,他想要筛选出红色的苹果,这时候怎么办呢? 很简单,复制上面的代码,将filterGreenApples改为filterRedApples;然后改变if的条件来匹配红色的苹果。但是,如果多变的农场主想要筛选多种颜色那又改怎么办呢?显然,这样的方式就很有局限性。

既然颜色总是变化,那么我们何不把颜色作为参数提取出来呢?这样就可以灵活的适应农场主对颜色的变化了:

1
2
3
4
5
6
7
8
9
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;
}

这样只需要调用这个方法,传递想要筛选的颜色就可以满足农场主的要求了:

1
2
3
List<Apple> grennApples = filterApplesByColor( inventory, "green");
List<Apple> redApples = filterApplesByColor( inventory, "red");
...

开心吧? 别高兴太早,事情没有这么简单;这位善变的农场主有要搞事情了,他来找你说:“要是能够区分苹果的轻重就好了,重的苹果一般要大于150g。”。

这怎么能难道一个帅气的软件工程师呢,你立马更改了函数的参数,将String color改为了int weight,同时更改了if的条件;

1
2
3
4
5
6
7
8
9
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(Don’t Repeat Yourself, 不要重复自己)的软件工程原则。

这时,你可能要这样想了,那我可不可以将每个属性都作为参数传递呢?就像下面这样:

1
2
3
4
5
6
7
8
9
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;
}

根据需求,可以这么用:

1
2
3
List<Apple> greenApples = filterApples( inventory, "green", 0, true );
List<Apple> heavyApples = filterApples( inventory, "", 150, false );
...

这种解决方式真的很糟糕,同样不能很好的应对变化的需求。试想一下,如果农场主需要根据不同属性做筛选,如:大小、形状、产地等怎么办呢?又比如要求你组合的筛选那你又怎么办呢?

很容易就发现,通过属性参数化传递并不能很好的解决农场主不断变化的需求。

那么我们就需要寻找一种新的解决方案来应对;来看一看更高层次的抽象,苹果是需要根据某些属性来进行筛选的,能不能对这种选择标准进行建模呢?通过定义一个接口来对选择标准进行建模,然后用该接口的多个实现来代表不同的选择标准。

1
2
3
public interface ApplePredicate {
boolean test ( Apple apple );
}

如果需要选出重的苹果:

1
2
3
4
5
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test ( Apple apple ) {
return apple.getWeight() > 150;
}
}

如果需要选择绿色的苹果:

1
2
3
4
5
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test ( Apple apple ) {
return "green".equals(apple.getColor());
}
}

以上这些标准就可以看作filter方法的不同行为,如何利用ApplePredicate的不同实现呢?这就需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是:

行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。

利用ApplePredicate改过之后,filter方法看起来是这样的:

1
2
3
4
5
6
7
8
9
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;
}

写到这里,可以看到此时的代码相比之前的代码要灵活很多了,至少可以应对任何涉及苹果属性的需求变更了。filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,就是把filterApples方法的行为参数化了。

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

如下图所示:

上述方法虽好,但是也有不足的地方,就是当要把新的行为传递给filterApples方法的时候,不得不声明好几个实现ApplePredicate接口的类,然后实例化好多个只会提到一次的ApplePredicate对象。下面,我将上面的改进过程最终的完成代码展现出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface ApplePredicate {
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() );
}
}

public class FilteringApples {
public static void main( String... args ){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155,"green"),
new Apple(120,"red"));
List<Apple> heavyApples = filterApples( inventory, new AppleHeavyWeightPredicate() );
List<Apple> greenApples = filterApples( inventory, new AppleGreenColorPredicate() );
}

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

费劲巴拉的写了这么多代码,这怎么复合我的懒人性格呢,这是想到了使用java提供的匿名类,可以同时声明和实例化一个类,然后我将上面的代码改进如下样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FilteringApples {
public static void main( String... args ){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155,"green"),
new Apple(120,"red"));
List<Apple> heavyApples = filterApples( inventory, new ApplePredicate() {
public boolean test ( Apple apple ) {
return apple.getWeight() > 150;
}
});
List<Apple> greenApples = filterApples( inventory, new ApplePredicate() {
public boolean test ( Apple apple ) {
return "green".equals( apple.getColor() );
}
});
}

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

这样看起来虽然代码少了一点,但是匿名类有两点不好的地方:

  1. 很笨重,它需要占用很多空间;
  2. 很多程序员很觉得用它很费解。

考虑到这两个原因,采用匿名类的方式仍然还有改进的空间。好在java8中引入了Lambda表达式,给我们这种极简追求者带来了福音,下面我用Lambda表达式改进上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FilteringApples {
public static void main( String... args ){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155,"green"),
new Apple(120,"red"));
List<Apple> heavyApples = filterApples( inventory, ( Apple apple ) -> apple.getWeight() > 150 );
List<Apple> greenApples = filterApples( inventory, ( Apple apple ) -> "green".equals( apple.getColor() );
}
});
}

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

此时此刻,可以发现代码已经看上去很简洁干净了;但目前的filterApples方法只适用于苹果,接下来可以更进一步的抽象它,我们将List的类型抽象化:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

完美!我们在灵活性和间接性之间找到了最佳平衡点。

参考资料

Java8实战

-------------本文结束感谢您的阅读-------------

本文标题:如何使用行为参数化传递代码

文章作者:Mr.wj

发布时间:2020年02月11日 - 13:49

最后更新:2020年02月11日 - 13:51

原始链接:https://www.wjqixige.cn/2020/02/11/如何使用行为参数化传递代码/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Mr.wj wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!