通过行为参数化传递代码
应对不断变化的需求
有实体如下:
Apple.java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Apple {
private String name;
private String color;
private int mass;
}
有集合如下:
List<Apple> inventory = Arrays.asList(new Apple("aaa","green",80),
new Apple("bbb","green",155),
new Apple("ccc","red",120));
1. 筛选绿苹果
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if("green".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
2. 把颜色作为参数把颜色作为参数
public static List<Apple> filterRedApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if("red".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
调用方法
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
这样就得到结果了…
改变需求: 要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。
实现代码如下:
//区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克
public static List<Apple> filterApplesByWeight(List<Apple> inventory,int weight) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getMass() > weight){
result.add(apple);
}
}
return result;
}
缺点:复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。
3. 对你能想到的每个属性做筛选
一种把所有属性结合起来的笨拙尝试如下:
public static List<Apple> filterApplesByWeight(List<Apple> inventory,String color,int weight,boolean flag) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(flag && apple.getColor().equals(color) || (!flag && apple.getMass()>weight)){
result.add(apple);
}
}
return result;
}
你可以这么用(但真的很笨拙):
调用如下:
List<Apple> greenApples = filterApplesByWeight(new ArrayList(),"red",0,true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
问题:
/**
* 客户端代码糟糕透顶,
* 1. true 和 false 是什么意思
* 2. 这个解决方案还是不能很好地应对变化的需求
* 3. 你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?
* 4. 要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?
*
* 解决之道:如何利用行为参数化实现这种灵活性
*/
行为参数化
已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让
我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据 Apple 的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean 值。我们把它称为谓词(即一个返回 boolean 值的函数)。让我们定义一个接口来对选择标准建模:
/**
* 定义一个接口,定义选择标准
* 这里定义实现,定义一族算法,把他们封装起来称为策略模式
*
* 算法族就是 ApplePredicate
*
* 不同的策略就是:AppleHeavyWeightPredicate和 AppleGreenColorPredicate
*
*
* 但是,该怎么利用 ApplePredicate 的不同实现呢?你需要 filterApples 方法接受
* ApplePredicate 对象,对 Apple 做条件测试。这就是行为参数化:让方法接受多种行为(或战
* 略)作为参数,并在内部使用,来完成不同的行为。
*
* 要在我们的例子中实现这一点,你要给 filterApples 方法添加一个参数,让它接受
* ApplePredicate 对象。这在软件工程上有很大好处:现在你把 filterApples 方法迭代集合的
* 逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
*/
public interface ApplePredicate {
/**
* 谓词(即一个返回 boolean 值的函数)
* @param apple
* @return
*/
boolean test(Apple apple);
}
现在你就可以用 ApplePredicate 的多个实现代表不同的选择标准了
仅仅选出重的苹果
/**
* 仅仅选出重的苹果
*/
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return apple.getMass() > 150;
}
}
仅仅选出绿苹果
/**
* 仅仅选出绿苹果
*/
public class AppleGreenColorPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
根据抽象条件筛选
这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活了,读起来、用起来也更容易!现在你可以创建不同的 ApplePredicate 对象,并将它们传递给 filterApples方法。filterApples 方法的行为取决于你通过 ApplePredicate 对象传递的代码。换句话说,你把 filterApples 方法的行为参数化了!
public static List<Apple> filterApples(List<Apple> inventory,ApplePredicate predicate) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(predicate.test(apple)){//predicate 封装了测试苹果的条件
result.add(apple);
}
}
return result;
}
筛选出绿色苹果
List<Apple> greenApples = filterApples(new ArrayList(),new AppleGreenColorPredicate());
筛选出重苹果
List<Apple> heavyApples = filterApples(new ArrayList(),new AppleHeavyWeightPredicate());
问题:每多一个刷选,就需要额外创建一个类,实现ApplePredicate,啰嗦。
解决知道:匿名内部类
List<Apple> apples = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
});
缺点:
- 它往往很笨重,因为它占用了很多空间
- 很多程序员觉得它用起来很让人费解
解决知道:使用 Lambda 表达式
List<Apple> apples2 = filterApples(inventory,(Apple apple) -> "green".equals(apple.getColor()));
不得不承认这代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。我们现在已经解决了 啰 嗦的问题。下图对我们到目前为止的工作做了一个小结
进一步加强:
filterApples 方法还只适用于 Apple 。你还可以将 List 类型抽象化,从而超越你眼前要处理的问题,改进如下:
public interface Predicate<T> {
boolean test(T t);
}
刷选方法如下:
public static <T> List<T> filter(List<T> inventory,Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for(T t: inventory){
if(predicate.test(t)){//predicate 封装了测试苹果的条件
result.add(t);
}
}
return result;
}
现在你可以把 filter 方法用在香蕉、桔子、 Integer 或是 String 的列表上了
代码如下:
List<Apple> apples2 = filterApples(inventory,(Apple apple) -> "red".equals(apple.getColor()));
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
List<Integer> evenNumbers = filter(numbers,(Integer i) -> i%2 == 0);
酷不酷?你现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!
真实的例子
- 使用Comparator排序
apples2.sort((Apple o1, Apple o2)-> o1.getColor().compareTo(o2.getColor()));
//可以替换如下
apples2.sort(Comparator.comparing(Apple::getColor));
- 用 Runnable 执行代码块
Thread t = new Thread(() -> System.out.println("Hello world"));