项目源码:https://github.com/java8/
1 应对不断变化的需求
在我们进行开发中,经常需要面临需求的不断变更,我们可以将行为参数化以适应不断变更的需求。
行为参数化就是可以帮助我们处理频繁变更的需求的一种软件开发模式
我们可以将代码块作为参数传递给方法。
例如,现有一个仓库,我们想定义从仓库中查询绿苹果的功能。后来我们又想查询重苹果(>150g)…
面对这种不断变更的需求,我们就可以使用行为参数化来维护我们的代码。
1.1 初试牛刀:筛选出绿苹果
Apple类:
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Apple {
private int weight = 0;
private String color = "";
}
// 初试牛刀:筛选绿苹果
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;
}
如果说我们现在改变主意,想要查询红色的苹果,最简单的办法就是copy上面这个方法,然后将方法改名为 fliterRedApples,改变if 判断条件。谈若我们需要查询各种演示的苹果:浅绿色、暗红色、黄色等,那么再按照前面的这种方法来做代码将变得非常的冗余。
一个好的原则是尝试将我们上的的这个方法进行抽象化,以适应不同颜色的苹果的查询。
下面我们进行尝试:
1.2 再展身手:把颜色作为参数
我们立马能想到的方法是将上面的 filterGreenApples 加上一个颜色参数,就可以了:
// 再展身手:把颜色作为参数
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
这样我们就可以查询各种各样的苹果了,如下:
List<Apple> greenApple = filterApplesByColor(inventory,"green");
List<Apple> redApple = filterApplesByColor(inventory,"red");
...
假设我们现在又要查询重苹果(>150g)或轻苹果,那么我们只需要根据
filterApplesByColor 进行稍稍修改即可,如下:
// 再展身手:根据苹果的重量进行查询
public static List<Apple> filterApplesWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
现在我们完成了根据颜色查询苹果、根据重量查询苹果的功能,但是我们这两个方法极为相似,复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件稍稍做了修改。
如果后续我们想改变查询的遍历方式来提升性能,那么需要修改所有的方法,这样显然是不合适的。
我们可以考虑将颜色和重量结合为一个方法,称为filterApples。不过这样需要加上一个标记来区分是对什么属性(颜色或重量)的查询(但是我们不推荐这种方式)
1.3 第三次尝试:对你能想到的每个属性做筛选
下面是一个比较笨拙的方法:
// 生产环境别这么用
public static List<Apple> filterApples(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.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
我们可以这样调用这个方法,以使用不同属性的查询:
// 根据颜色查询:查询绿苹果
List<Apple> greenApples = filterApples(inventory, "green",0,true);
// 根据重量查询:查询>150g的重苹果
List<Apple> heavyApples = filterApples(inventory,"",150,false);
这个方法能解决根据不同属性查询苹果的功能,但是这样写代码很烂,并且我们也不知道 boolean flag 参数传入 true、flase是什么意思,可读性很差。
并且这样也不能很好的使用根据不同属性进行查询,如果我们需要根据多个属性进行查询(比如:查询绿色的重苹果),那更是天方夜谭了。
下面我们来解决这个问题:
2 行为参数化
我们需要我们适应各种各样的属性来查询。
我们可以考虑根据Apple的属性来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。下面我们定义一个接口来实现:
// 判断型接口
@FunctionalInterface
public interface ApplePredicate {
boolean test(Apple apple);
}
现在我们就可以用 ApplePredicate 的多个实现代表不同的查询标准了,例如:
// 查询出重苹果
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
// 查询出绿苹果
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
我们可以把这些标准看作filter方法的不同行为。刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
但是,该怎么利用ApplePredicate的不同实现呢?
我们需要 filterApples方法接受 ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
个人对于行为参数化的理解:
所谓行为参数化,就是将行为(一般封装成方法的形式)以参数的形式传递到其他方法中执行。
如何实现行为参数化:
我们要给filterApples方法添加一个参数,让它接受 ApplePredicate对象。
行为参数化的好处:
我们把filterApples方法迭代集合的逻辑与我们应用到集合中每个元素的行为(这里是一个谓词,即我们根据Apple的属性查询的行为)区分开了。
2.1 第四次尝试:根据抽象条件筛选
利用 ApplePredicate 修改后的 filter方法如下:
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (predicate.test(apple)){ // 谓词对象封装了测试苹果的条件
result.add(apple);
}
}
return result;
}
2.2 传递代码/行为
现在,这段代码以及比我们前面写的方法灵活多了,代码的可读性也高了。比如,我们要查询红的重苹果,只需要创建一个类实现现ApplePredicate接口即可(甚至可以使用Lambda表达式):
public class AppleRedAndHeavyPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
// 调用
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
现在,我们的filterApples方法完成了行为参数化。
但是我们现在发现这样还是很麻烦,我们要完成一个查询功能,为此我们定义了一个类,里面有一个方法,完成对查询结果的判断,这样有很多的无用代码,代码可读性还是不高。
我们可以使用lmabda表达式简化不必要的代码,直接把表达式"red".equals(apple.getColor())
&&apple.getWeight() > 150传递给filterApples方法,无需定义一个类
// 使用Lambda表达式实现传递代码
List<Apple> redAndHeavyApples = filterApples(inventory, (apple) ->
"red".equals(apple.getColor()) && apple.getWeight() > 150);
2.3 多种行为,一个参数
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。
演示:编写灵活的prettyPrintApple方法
编写一个prettyPrintApple方法,它接受一个Apple的List,并可以对它参数化,以多种方式根据苹果生成一个String输出(有点儿像多个可定制的toString方法)。
例如,你可以告诉 prettyPrintApple 方法,只打印每个苹果的重量。此外,你可以让 prettyPrintApple方法分别打印每个苹果,然后说明它是重的还是轻的。
@FunctionalInterface
public interface AppleFormatter{
String accept(Apple a);
}
public class AppleFancyFormatter implements AppleFormatter { // 后续我们可以通过Lambda简化,以省略这样的类
public String accept(Apple apple) {
String characteristic = apple.getWeight() > 150 ? "heavy" :
"light";
return "A " + characteristic +
" " + apple.getColor() + " apple";
}
}
public class AppleSimpleFormatter implements AppleFormatter {
public String accept(Apple apple) {
return "An apple of " + apple.getWeight() + "g";
}
}
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter) {
for (Apple apple : inventory) {
String output = formatter.accept(apple);
System.out.println(output);
}
}
使用
// 你首先要实例化AppleFormatter的实现,然后把它们作为参数传给prettyPrintApple方法
prettyPrintApple(inventory, new AppleFancyFormatter());
现在,我们已经将行为抽象出来了,这样使我们的代码适应不同的需求,但是这样很繁琐
因为我们需要声明很多个只使用一次的类,下面通过匿名内部类、Lambda表达式等方式进行简化
3 简化代码
在前面,当要把新的行为传递给 filterApples方法的时候,我们不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。这真是很啰嗦,很费时间!
3.1 匿名类
匿名类和我们熟悉的 Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许我们同时声明并实例化一个类。换句话说,它允许你随用随建
3.2 第五次尝试:使用匿名类
下面我们将通过创建匿名类的方式实现ApplePredicate的对象,重写筛选的例子(以查询绿色苹果为例):
List<Apple> greenApples = filterApples(inventory, new ApplePredicate() { // 直接内联参数化filterapples方法的行为
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
但是匿名类的方式还是不够好:
- 它往往很笨重,因为它占用了很多空间
- 代码阅读性较差
总的来说,使用匿名类的方式,不仅代码编写、维护比较费时间,可读性也不太好。
接下来,我们使用Java 8中引人的Lambda表达式——一种更简洁的传递代码的方式。
3.3 第六次尝试:使用 Lambda 表达式
使用Lambda表达式重写上面的代码:
List<Apple> greenApples = filterApples(inventory, (apple) ->
"red".equals(apple.getColor()));
到目前为止,区别于以往的值参数传递,我们已经实现了将类、匿名类、Lambda表达式等行为参数化传递到了方法中
3.4 第七次尝试:将 List 类型抽象化
在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。
我们还可以将List类型抽象化,从而超越你眼前要处理的问题:
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){ // 引入类型参数T,即泛型
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
现在,我们的 filter 方法能更好的适应不同的查询了,可以用在香蕉、桔子、Integer或是String的列表等等上了。
例如:
List<Apple> redApples =
filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
// 从numbers中筛选出偶数
List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
4 真实的例子
到现在,我们已经清除的知道了行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿名类一起使用。
我们会展示两个例子,这应该能帮助你巩固传递代码的思想了:用一个Comparator排序,用Runnable执行一个代码块
4.1 用 Comparator 来排序
例如,根据苹果的重量对库存进行排序,或者希望你根据颜色对苹果进行排序。听起来有点儿耳熟?是的,
你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。sort的行为可以用java.util.Comparator
对象来参数化,如下:
package java.util;
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
因此,我们可以随时创建Comparator的实现,用sort方法表现出不同的行为。
比如,你可以 使用匿名类,按照苹果的重量升序对库存排序:
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
});
后续,我们可以随时创建一个Comparator来满足新要求,并把它传递给 sort方法。而如何进行排序这一内部细节都被抽象掉了。用Lambda表达式的话,看起来就是这样:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight() - a2.getWeight());
4.2 用 Runnable 执行代码块
在 Java里,你可以使用Runnable接口表示一个要执行的代码块
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
我们可以像下面这样,使用这个接口创建执行不同行为的线程:
Thread t = new Thread(new Runnable() {
@Override
public void run(){
System.out.println("Hello world");
}
});
使用Lambda表达式简化:
Thread t = new Thread(() -> System.out.println("Hello world"));
5 小结
-
行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
-
行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
-
传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
-
Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。