《Java8实战》第2章 通过行为参数化传递代码

一个系统需求,今天需要找全是绿色的苹果,明天说要找大于150G的水果,后天要找体积大于120立方厘米的苹果。如果这样子,你的代码就需要一直改。
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。

2.1 应对不断变化的需求

2.1.1 初试牛刀:筛选绿苹果

使用一个枚举变量 Color 来表示苹果的各种颜色:enum Color { RED, GREEN }
筛选出绿苹果,可能是这样写的
《Java8实战》第2章 通过行为参数化传递代码_第1张图片

2.1.2 再展身手:把颜色作为参数

如果给方法增加一个颜色的参数,那么我们就可以应对变化了吧?

public static List filterApplesByColor(List inventory, Color color) { 
 	List result = new ArrayList<>(); 
   for (Apple apple: inventory) {
     // 颜色为参数
     if ( apple.getColor().equals(color) ) { 
       result.add(apple); 
     } 
   } 
 	return result; 
}

调用
List greenApples = filterApplesByColor(inventory, GREEN); 
List redApples = filterApplesByColor(inventory, RED);

但是如果这个时候,又来一个需求说,需要区别大于150g的水果,有了上次的经验,你肯定会把重量也作为参数

public static List filterApplesByWeight(List inventory,int weight) { 
  List result = new ArrayList<>(); 
  For (Apple apple: inventory){ 
    if ( apple.getWeight() > weight ) { 
    	result.add(apple); 
    } 
  } 
  return result; 
} 

但是这样子也不是长久之计,万一还有其他的属性筛选呢?
这代码也重复得太多了吧?
因为它打破了 DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则。

2.1.3 第三次尝试:对你能想到的每个属性做筛选

一种把所有属性结合起来的笨拙尝试如下所示:

public static List filterApples(List inventory, Color color, int weight, boolean flag) { 
  List result = new ArrayList<>(); 
  for (Apple apple: inventory) { 
    if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ) {
    	result.add(apple); 
    } 
  } 
  return result; 
}

调用
List greenApples = filterApples(inventory, GREEN, 0, true); 
List heavyApples = filterApples(inventory, null, 150, false); 

上面的代码,如果多个属性怎么办,比如现状,产地等。 写这种方法维护成本高,后面来的人都不行维护了,屎一样的代码。

2.2 行为参数化

需要根据 Apple 的某些属性来返回一个 boolean 值。我们把它称为谓词(即一个返回 boolean 值的函数)。
定义一个接口来对选择标准建模:

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

现在你就可以用 ApplePredicate 的多个实现代表不同的选择标准了,

// 仅仅选出重的苹果
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()); 
 } 
} 

《Java8实战》第2章 通过行为参数化传递代码_第2张图片

public static List<Apple> filterApples(List<Apple> inventory,
                                       ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory) {
        if(p.test(apple)){// 谓词 p 封装了测试苹果的条件
            result.add(apple);
        }
    }
    return result;
}

1. 传递代码/行为
这样子,已经比之前的灵活好多了,就算再来一个需求,找出所有重量超过 150 克的红苹果

public class AppleRedAndHeavyPredicate implements ApplePredicate { 
 public boolean test(Apple apple){ 
   return RED.equals(apple.getColor()) && apple.getWeight() > 150; 
 } 
} 
List redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());

那也是多些一个类来实现ApplePredicate就可以了,行为已经参数化了。
《Java8实战》第2章 通过行为参数化传递代码_第3张图片

public static void prettyPrintApple(List inventory, ???){ 
 for(Apple apple: inventory) { 
 String output = ???.???(apple); 
 System.out.println(output); 
 } 
}

答案:首先,你需要一种表示接受 Apple 并返回一个格式 String 值的方法。前面我们在编写 ApplePredicate 接口的时候,写过类似的东西,现在你就可以通过实现 AppleFormatter 方法来表示多种格式行为了:

public interface AppleFormatter{ 
 String accept(Apple a); 
} 
public class AppleFancyFormatter implements AppleFormatter{ 
 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"; 
 } 
}

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

public static void prettyPrintApple(List inventory, 
 AppleFormatter formatter){ 
   for(Apple apple: inventory){ 
     String output = formatter.accept(apple); 
     System.out.println(output); 
   } 
} 

现在你就可以给 prettyPrintApple 方法传递多种行为了。

prettyPrintApple(inventory, new AppleFancyFormatter()); 
prettyPrintApple(inventory, new AppleSimpleFormatter()); 

把行为抽象出来,但是这个过程很啰嗦。,因为你需要声明很多只要实例化一次的类。
但是这样也没什么特别的吧?无非是抽象出来,然后把接口给子类去实现
避免每次都new一次,还可以把先把new出来的对象放在map里面,需要的时候再取

2.3 对付啰唆

之前的做法是定义一个接口类,然后再实现他的接口行为。还是有点麻烦的
Java 有一个机制称为匿名类,它可以让你同时声明和实例化一个类。

2.3.1 匿名类

匿名类和你熟悉的 Java 局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。

2.3.2 第五次尝试:使用匿名类

创建一个用匿名类实现 ApplePredicate 的对象,重写筛选的例子

List redApples = filterApples(inventory, new ApplePredicate() {
 public boolean test(Apple apple){ 
   return RED.equals(apple.getColor()); 
 } 
});

GUI 应用程序中经常使用匿名类来创建事件处理器对象

button.setOnAction(new EventHandler() { 
 public void handle(ActionEvent event) { 
   System.out.println("Whoooo a click!!"); 
 } 
}); 

但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。看起来很不舒服。第二,很多程序员觉得它用起来很让人费解。

2.3.3 第六次尝试:使用 Lambda 表达式

使用lambda重写

	List result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor())); 

这样子看起来就干净很多,并且也不啰嗦
《Java8实战》第2章 通过行为参数化传递代码_第4张图片

2.3.4 第七次尝试:将 List 类型抽象化

在通往抽象的路上,还可以更进一步。目前,filterApples 方法还只适用于 Apple。你还可以将 List 类型抽象化,从而超越你眼前要处理的问题:

public interface Predicate{ 
	boolean test(T t); 
} 
public static  List filter(List list, Predicate p){ 
 List result = new ArrayList<>(); 
 for(T e: list){ 
   if(p.test(e)){ 
  	 result.add(e); 
   } 
 } 
 return result; 
} 

现在你可以把 filter 方法用在香蕉、橘子、Integer 或是 String 的列表上了。这里有一个使用 Lambda 表达式的例子:
List redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
修改成这个样子了,这在java8之前是做不到的,在灵活性和简洁性之间找到了最佳平衡点。

2.4 真实的例子

行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对 Apple的不同谓词)将方法的行为参数化,有点类似于策略模式

2.4.1 用 Comparator 来排序

对集合进行排序是一个常见的编程任务,有时候也是需要通过各种属性来排序。
在 Java 8 中,List 自带了一个 sort 方法(你也可以使用 Collections.sort)。sort 的行为可以用java.util.Comparator 对象来参数化,它的接口如下:

// java.util.Comparator 
public interface Comparator {
	int compare(T o1, T o2); 
}

根据这个接口,我们就可以使用不同的方式进行排序,使用匿名内部类,按照重量进行排序

inventory.sort(new Comparator() { 
  public int compare(Apple a1, Apple a2) { 
    return a1.getWeight().compareTo(a2.getWeight()); 
  } 
}); 

产品改了主意,你可以随时创建一个 Comparator 来满足他的新要求并把它传递给sort 方法。
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

2.4.2 用 Runnable 执行代码块

之前使用能传递给线程结构的只有对象,因此之前典型的使用模式是传递一个带有 run 方法,返回值为 void(即不返回任何对象)的匿名类,非常臃肿。这种匿名类通常会实现一个 Runnable 接口。

接口不会返回结果
// java.lang.Runnable 
public interface Runnable{ 
 void run(); 
}

使用多线程的方式
Thread t = new Thread(new Runnable() { 
 public void run(){ 
   System.out.println("Hello world"); 
 } 
}); 

如果使用lambda的话
Thread t = new Thread(() -> System.out.println("Hello world"));

2.4.3 通过 Callable 返回结果

Java5引用的ExecutorService 接口解耦了任务的提交和执行。与使用线程和 Runnable 的方式比较起来,通过 ExecutorService 你可以把一项任务提交给一个线程池,并且可以使用 Future 获取其执行的结果,这种方式用处非常大。可以看作是升级版的Runnable
使用方式:

// java.util.concurrent.Callable 
public interface Callable { 
 V call(); 
} 
下面这段代码会返回执行任务的线程名:
ExecutorService executorService = Executors.newCachedThreadPool(); 
Future threadName = executorService.submit(new Callable() { 
 @Override 
 public String call() throws Exception { 
   return Thread.currentThread().getName(); 
 } 
}); 

如果使用 Lambda 表达式,上述代码可以更加简化,如下所示:
Future threadName = executorService.submit( () -> Thread.currentThread().getName());

2.4.4 GUI 事件处理

GUI 编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单击或在文本上悬停。在 JavaFX 中,你可以使用EventHandler,把它传给 setOnAction 来表示对事件的响应:

Button button = new Button("Send"); 
button.setOnAction(new EventHandler() { 
 public void handle(ActionEvent event) { 
 label.setText("Sent!!"); 
 } 
});

setOnAction 方法的行为就用 EventHandler 参数化了。用 Lambda 表达式的话,看起来就是这样:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

2.5 小结

  • 行为参数化就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码就是将新行为作为参数传递给方法。但在 Java 8 之前这实现起来很啰唆。为接口声明许多只用一次的实体类而造成的啰唆代码,在 Java 8 之前可以用匿名类来减少。
  • Java API 包含很多可以用不同行为进行参数化的方法,包括排序、线程和 GUI 处理。

你可能感兴趣的:(java8实战,软件工程,java,开发语言)