《Java 8 in Action》【02】----通过行为参数化传递代码

1.前言

  本章会先对行为参数化做一简单介绍,而后以一个苹果库存的案例来说明,如何对代码加以改进,以应对不断变化的需求。在此基础上列举了几个行为参数化应用的真实案例,如用Java API中现有的类和接口对 List 进行排序、用Thread去执行代码块或者处理GUI事件。使用匿名类的方式代码十分繁琐,而Java8中的Lambda很好解决了这个问题。

2.应对不断变化的需求

  在软件工程中,众所周知的是客户需求是不断变化的,行为参数化可以帮助我们处理频繁变更需求的一种软件开发模式。可以理解为,程序中有一先不执行的代码块,这块代码块将会被其他方法调用。这表示可以推迟这段代码的执行。将代码块作为参数传递给另外一个方法。稍后去执行它,这个方法的行为就是基于那块代码被参数化了。例如,处理集合时,可能会写一个方法:

  • 对列表中每个元素做"某件事",
  • 在列表处理完后做"另一件事",
  • 遇到错误时可以做"另外一件事"。

行为参数化说的就是这个,相当于方法可以接受不同的新行为作为参数,然后去执行。实际上面说到的编写能够应对不断变化的需求的代码并非易事,以苹果库存程序为例,现在用户要从一个列表中筛选出绿苹果。

1.【第一次尝试】:传统编写方案,用if判断筛选结果。

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

 现在需求变了,用户需要筛选出红苹果,最简单的解决方案是复制上面代码,然后将方法名改成filterRedApples,更改if条件匹配红苹果。但用户如果需要筛选其他颜色时,是否继续复制代码,修改判断条件呢?这种方式其实不太合适,因为存在大量重复代码。一个良好的原则是编写类似的代码之后,尝试将其抽象化

2.【第二次尝试】:将颜色作为方法参数。

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> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");

现在用户需求又发生了变化,需要根据苹果的重量来筛选,于是尝试给方法传入另外一个参数weight来筛选。

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

 观察上面根据颜色和重量筛选苹果的方法,大部分代码代码是用来实现遍历库存的,并根据不同的if判断条件进行了筛选。根据软件工程原则中代码不重复原则,需要对现在的方法进行重构,我们尝试将颜色和重量结合为一个名称为filter的方法,不过需要一种方式来区分要筛选哪个属性,如用flag标志来区分对颜色和重量的查询。

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);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);

 回顾这段代码,它调用方法的方式其实很糟糕,参数truefalse是什么?而且这个方案并不能很好应对变化需求,因为用户可能想到其他属性对苹果筛选,如大小,形状,产地等。筛选组合属性更复杂,如筛选绿色的重苹果。对于这些不确定问题,需要有一个更好的方式来解决,即行为参数化。

3.行为参数化

 为了更好应对用户需求,需要更高层次的抽象,一种可能的解决方案对选择标准进行建模:基于Apple某些属性(如颜色是绿色的?重量大于150g?)来返回一个boolean值,这种行为称为谓词Predicate,即一个返回boolean类型的函数)。基于此,定义一个接口ApplePredicate来对选择标准建模,用ApplePredicate的多个实现代表不同的选择标准。

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

 可以将这些标准看做是filter方法不同行为,这种方式与策略设计模式很相似,即定义一族算法,封装每个算法(称为策略)和运行时选择一种算法执行。本例中算法族就是ApplePredicate,而AppleHeavyWeightPredicateAppleGreenColorPredicate表示不同的策略。现在需要方法filterApples方法接收ApplePredicate对象,并对Apple做条件测试,这就是行为参数化:方法参数可以接收多种行为(或策略),并在内部使用,来完成不同的行为。本例需要为filterApples方法添加一个参数ApplePredicate,这在软件工程中有很大好处:将filterApples方法迭代集合的逻辑与应用到集合中每个元素的行为(这里是一个predicate)区分开了。

4.【第四次尝试】:根据抽象条件筛选。

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方法行为参数化了。本例中唯一重要的是代码是test方法实现,正是它定义了filterApples方法的新行为,由于filterApples方法只能接收对象,所以必须将代码包裹在ApplePredicate对象里。由于通过一个实现了test方法对象来实传递布尔表达式,实际上这可以使用Lambda表达式,直接将 "red".equals(apple.getColor())&&apple.getWeight() > 150 传递给 filterApples 方法,而无需定义多个ApplePredicate类,此处不细说。行为参数化好处在于将迭代集合的逻辑与对集合中每个元素应用的行为区分开,这样就可以重复使用同一个方法,给它不同行为达到不同的目的
前面代码将行为抽象出来以应对需求的变化,但这个过程比较繁琐,因为这种方式需要声明很多只要实例化一次的类。

5.【第五次尝试】:使用匿名类
 匿名类没有名称,它允许你同时声明并实例化一个类。但匿名类不够友好,它占用了很多空间,因为有很多模板代码,如下。

//匿名类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
   public boolean test(Apple a){
        return "red".equals(a.getColor());
    }
});
List<Apple> heavyApples = filterApples(inventory, new ApplePredicate() {
   public boolean test(Apple a){
        return a.getWeight()>150;
    }
});

 不难看出,两段代码只有test方法实现不同,其他部分是重复的,而且代码不易读。使用匿名类处理在某种程度上改善了为一个接口声明好几个实体类的问题。但匿名类还是需要创建一个类,明确地实现一个方法来定义一个新的行为。

6.【第六次尝试】:使用Lambda表达式
 使用Lambda表达式可以将筛选红苹果的代码重写如下,这段代码简洁,而且看起来更像是问题陈述本身。

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

7.【第七次尝试】:将List类型抽象化
 由于之前方法filterApple仅适用于Apple,因此可以将List的类型进行抽象化。

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

现在可以将filter方法可以应用在香蕉、桔子、Integer或者String的列表上了,如:

List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

 行为参数化是一个很有用的模式,能够轻松应对不断变化的需求。这种模式可以将一个行为封装起来,并通过传递和使用创建的行为(如对Apple的不同谓词)将方法的行为参数化,这种做法类似于策略设计模式。Java API中的很多方法都可以用不同的行为来参数化,这些方法往往与匿名类一起使用。

4.实际案例
4.1 用Comparator来排序

 对集合进行排序是一个常见的编程任务,如对苹果根据颜色来排序,这时需要一种方法来表示和使用不同的排序行为。在Java8中,List自带了一个sort方法(或者使用Collections.sort)。sort的行为可以用java.utils.Comparator对象来参数化,它的接口如下:

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

可以创建一个Comparator实现,并将它传递给sort,而如何排序这一内部细节都被抽象调了。比如可以使用匿名类,按照重量升序对库存排序:

// 参数化实践:使用匿名类进行升序
inventory.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return Integer.compare(o1.getWeight(), o2.getWeight());
    }
});

如果使用Lambda表达式的话,代码会更加简洁。

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()))
4.2 用Runnable执行代码块

 线程就像是轻量级的进程,它执行了一个代码块,Java中使用Runnable接口表示一个要执行的是代码块,需要注意的是方法不会返回任何结果(即void)

// java.lang.Runnable
public interface Runnable{
	public 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"));
4.3 GUI时间处理

 UI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单击或在文字上悬停。例如用户在单击"发送"按钮时,可能要显示一个弹出式窗口,或把行为记录在一个文件中。需要一种方法来应对变化并且应该能够做出任意形式的响应。在JavaFX中,可以使用EventHandler,把它传给setOnAction来表示对事件的响应。

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

这里, setOnAction 方法的行为就用 EventHandler 参数化了。用Lambda表达式的话,代码如下:

button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
5.总结

1.行为参数化,就是一个方法接收多个不同行为作为参数,并在内部使用它们,完成不同行为的能力。
2.行为参数化可让代码更好应对不断变化的需求,Java8之前实现方式很繁琐,需要为接口提供只使用一次表现不同行为的实现类,虽然使用匿名类可以减少代码,但依旧存在问题。使用Java8中的Lambda表示方式更加简洁。
3.Java API包含了很多可以用不同行为进行参数化方法,包括排序,线程和GUI处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值