2.2 行为参数化

你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模

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

现在你就可以用ApplePredicate的多个实现代表不同的选择标准了,比如(如图2-1所示):

  1. public class AppleHeavyWeightPredicate implements ApplePredicate{ ←---- 仅仅选出重的苹果
  2. public boolean test(Apple apple){
  3. return apple.getWeight() > 150;
  4. }
  5. }
  6. public class AppleGreenColorPredicate implements ApplePredicate{ ←---- 仅仅选出绿苹果
  7. public boolean test(Apple apple){
  8. return GREEN.equals(apple.getColor());
  9. }
  10. }

2.2 行为参数化 - 图1

图 2-1 选择苹果的不同策略

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicateAppleGreenColorPredicate

但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(策略)作为参数,并在内部使用,来完成不同的行为。

要在我们的例子中实现这一点,你要给filterApples方法添加一个参数,让它接受ApplePredicate对象。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。

第四次尝试:根据抽象条件筛选

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

  1. public static List<Apple> filterApples(List<Apple> inventory,
  2. ApplePredicate p) {
  3. List<Apple> result = new ArrayList<>();
  4. for(Apple apple: inventory) {
  5. if(p.test(apple)){ ←---- 谓词p 封装了测试苹果的条件
  6. result.add(apple);
  7. }
  8. }
  9. return result;
  10. }
  • 传递代码/行为

这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:

  1. public class AppleRedAndHeavyPredicate implements ApplePredicate {
  2. public boolean test(Apple apple){
  3. return RED.equals(apple.getColor())
  4. && apple.getWeight() > 150;
  5. }
  6. }
  7. List<Apple> redAndHeavyApples =
  8. filterApples(inventory, new AppleRedAndHeavyPredicate());

你已经做成了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法的行为参数化了!

请注意,在上一个例子中,唯一重要的代码是test方法的实现,如图2-2所示,正是它定义了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。你将在2.3节(第3章中有更详细的内容)中看到,通过使用Lambda,可以直接把表达式RED.equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无须定义多个ApplePredicate类,从而去掉不必要的代码。

2.2 行为参数化 - 图2

图 2-2 参数化filterApples的行为并传递不同的筛选策略

  • 多种行为,一个参数

正如先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如图2-3所示。这就是行为参数化是一个有用的概念的原因。你应该把它放进你的工具箱里,用来编写灵活的API。

2.2 行为参数化 - 图3

图 2-3 参数化filterApples的行为并传递不同的筛选策略

为了保证你对行为参数化运用自如,看看测验2.1吧!

测验2.1:编写灵活的prettyPrintApple方法

编写一个prettyPrintApple方法,它接受一个AppleList,并可以对它参数化,以多种方式根据苹果生成一个String输出(有点儿像多个可定制的toString方法)。例如,你可以告诉prettyPrintApple方法,只打印每个苹果的重量。此外,你可以让prettyPrintApple方法分别打印每个苹果,然后说明它是重的还是轻的。解决方案和前面讨论的筛选的例子类似。为了帮你上手,我们提供了prettyPrintApple方法的一个粗略的框架:

  1. public static void prettyPrintApple(List<Apple> inventory, ???){
  2. for(Apple apple: inventory) {
  3. String output = ???.???(apple);
  4. System.out.println(output);
  5. }
  6. }

答案:首先,你需要一种表示接受Apple并返回一个格式String值的方法。前面我们在编写ApplePredicate接口的时候,写过类似的东西:

  1. public interface AppleFormatter{
  2. String accept(Apple a);
  3. }

现在你就可以通过实现AppleFormatter方法来表示多种格式行为了:

  1. public class AppleFancyFormatter implements AppleFormatter{
  2. public String accept(Apple apple){
  3. String characteristic = apple.getWeight() > 150 ? "heavy" :
  4. "light";
  5. return "A " + characteristic +
  6. " " + apple.getColor() +" apple";
  7. }
  8. }
  9. public class AppleSimpleFormatter implements AppleFormatter{
  10. public String accept(Apple apple){
  11. return "An apple of " + apple.getWeight() + "g";
  12. }
  13. }

最后,你需要告诉prettyPrintApple方法接受AppleFormatter对象,并在内部使用它们。你可以给prettyPrintApple加上一个参数:

  1. public static void prettyPrintApple(List<Apple> inventory,
  2. AppleFormatter formatter){
  3. for(Apple apple: inventory){
  4. String output = formatter.accept(apple);
  5. System.out.println(output);
  6. }
  7. }

搞定啦!现在你就可以给prettyPrintApple方法传递多种行为了。为此,你首先要实例化AppleFormatter的实现,然后把它们作为参数传给prettyPrintApple

  1. prettyPrintApple(inventory, new AppleFancyFormatter());

这将产生一个类似于下面的输出:

  1. A light green apple
  2. A heavy red apple

或者试试这个:

  1. prettyPrintApple(inventory, new AppleSimpleFormatter());

这将产生一个类似于下面的输出:

  1. An apple of 80g
  2. An apple of 155g

你已经看到,可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰唆,因为你需要声明很多只要实例化一次的类。来看看可以怎样改进。