行为参数化就是可以帮助你处理频繁变更需求的一种软件开发模式。它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如:你可以将代码块作为参数传递给另一个方法,稍后再去执行它。
应对不断变化的需求
1.第一次尝试:实现一个功能,从一个列表中筛选出绿色苹果的功能。
首先准备Apple实体类
public class Apple { private Integer Id; private String Color; private Double Weight; //getter..setter..toString.. }
编写过滤出绿色苹果的功能
public static List<Apple> filter(List<Apple> apples){ List<Apple> result = new ArrayList<>(); for(Apple apple : apples){ //筛选出绿色的苹果 if("green".equals(apple.getColor())){ result.add(apple); } } return result; }
测试数据
public static void main(String[] args) { List<Apple> apples = Arrays.asList( new Apple(1,"green",18.0), new Apple(2,"yellow",36d), new Apple(3,"red",42d), new Apple(4,"green",15d), new Apple(5,"red",16d) ); List<Apple> greenApple = filter(apples); System.out.println(greenApple); }
输出结果:
[Apple{Id=1, Color='green', Weight='18.0'}, Apple{Id=4, Color='green', Weight='15.0'}]
实现功能了,现在产品说我又想要筛选红色的苹果了,最简单的办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果,然而产品想要筛选更多颜色的苹果,黄色、橘黄色、大酱色等,这种方法就不行了,将颜色作为参数
2.第二次尝试:把颜色作为参数
//把颜色作为参数 public static List<Apple> filterApplesByColor(List<Apple> apples,String color){ List<Apple> result = new ArrayList<>(); for(Apple apple : apples){ if(color.equals(apple.getColor())){ result.add(apple); } } return result; }
现在只需要这样调用,产品就会满意了。
List<Apple> yellowApple = filterApplesByColor(apples,"yellow");
List<Apple> redApple = filterApplesByColor(apples,"red");
然后产品又跑过来说,要是能区分苹果大小就太好了,把大于32的分为大苹果,小于32的分为小苹果。
于是你下了下面的方法,又增加了重量的参数
//根据重量来筛选苹果 public static List<Apple> filterApplesByWeight(List<Apple> apples,Double weight){ List<Apple> result = new ArrayList<>(); for(Apple apple : apples){ if(apple.getWeight() > weight){ result.add(apple); } } return result; }
你终于实现了产品的需求,但是请注意,你复制了大部分的代码来实现遍历苹果列表,并对每个苹果筛选条件。这有点令人失望,因为它打破了Don't Repeat Yourself(不要重复你自己)的软件工程原则。
3.第三次尝试:把你能想到的每个属性做筛选
你可以将颜色和重量结合为一个方法,称为filterColorOrWeight,然后增加一个参数来区分需要筛选哪个属性。
//按颜色筛选或按重量筛选 public static List<Apple> filterColorOrWeight(List<Apple> apples, String color, Double weight, boolean flag) { List<Apple> result = new ArrayList<>(); for (Apple apple : apples) { if ((flag && color.equals(apple.getColor())) || (!flag && apple.getWeight() > weight)) { result.add(apple); } } return result; }
然后你可以这样使用:
List<Apple> yellowApples = filterColorOrWeight(apples, "yellow",0d,true); List<Apple> bigApples = filterColorOrWeight(apples, "",32d,false);
这个解决方案再差不过了,首先客户端代码看上去烂透了,true和false是什么意思?此外,这个解决方案还是不能很好的区应对变化的需求,如果产品又让你对苹果的其他不同的属性做筛选,比如大小、形状、产地等,又该怎么办?或者要求你组合属性,作更复杂的查询,比如绿色的大苹果,又该怎么办?
行为参数化
你在上一节中看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。 一种解决方案是对你选择的标准建模:比如根据Apple的某些属性(是否是绿色,是否大于32)来返回一个boolean值,我们把它称之为谓词。首先我们来定义一个借口来对选择标准建模。
public interface ApplePredicate { boolean test(Apple apple); }
现在就可以使用ApplePredicate的多个实现来代表不同的选择标准了,比如:
public class AppleGreenColorPredicate implements ApplePredicate { //绿苹果谓词 public boolean test(Apple apple) { return "green".equals(apple.getColor()); } }
public class AppleHeavyWeightPredicate implements ApplePredicate { //大苹果谓词 public boolean test(Apple apple) { return apple.getWeight() > 32; } }
现在,你可以把这些标准看做filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一组算法,把他们封装起来(成为“策略”),然后在运行时选择一个算法。在这里,ApplePredicate就是算法组,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
4.第四次尝试:根据抽象条件筛选
//根据抽象条件筛选 public static List<Apple> filterApples(List<Apple> apples,ApplePredicate p){ List<Apple> result = new ArrayList<>(); for(Apple apple : apples){ if(p.test(apple)){ result.add(apple); } } return result; }
使用的时候这样:
List<Apple> greenApple = filterApples(apples,new AppleGreenColorPredicate()); List<Apple> bigApple = filterApples(apples,new AppleHeavyWeightPredicate());
1.传递代码/行为
到这里可以小小的庆祝一下了,这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将他们传递给filterApples方法。比如现在产品让你找出所有重量超过80克的红苹果,你只需创建一个类来实现ApplePredicate就行了,你的代码现在足够灵活,可以应对任何设计苹果属性的需求变更了:
public class AppleRedAndBigPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return "red".equals(apple.getColor()) && apple.getWeight() > 80; } }
传递给filterApples方法,无需修改filterApples方法的内部实现:
List<Apple> redBigApple = filterApples(apples,new AppleRedAndBigPredicate());
现在你做了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法行为参数化了!
在上一个例子中,唯一重要的代码是test方法的实现,正式它定义了filterApples方法的新行为。由于filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。这种做法类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。
2.多种行为,一个参数
行为参数化的好处在于你可以把遍历集合的逻辑和 对集合中每个元素的判断逻辑 区分开来。这样就可以重复使用同一个方法,给它不同的行为来达到不同的目的。
行为参数化练习
编写printApple方法,实现一个功能,以多种方式根据苹果生成一个String输出(例如,可以让printApple方法只打印每个苹果的颜色,或者让它打印什么颜色的大(小)苹果)
创建AppleFormater接口
public interface AppleFormater { String accept(Apple a); }
创建AppleWeightFormater、AppleColorFormater 来实现接口
public class AppleWeightFormater implements AppleFormater { @Override public String accept(Apple a) { return "这是一个" + a.getColor() + "的" + (a.getWeight() > 32 ? "大" : "小") + "苹果"; } }
public class AppleColorFormater implements AppleFormater { @Override public String accept(Apple a) { return "一个" + a.getColor() + "的苹果"; } }
然后创建printApple方法,给它传递不同的formater对象
public static void printApple(List<Apple> apples,AppleFormater af) { for (Apple apple : apples) { String output = af.accept(apple); System.out.println(output); } }
测试:
printApple(apples, new AppleWeightFormater()); printApple(apples, new AppleColorFormater());
现在,你可以把行为抽象出来了,让你的代码更加适应需求的变化,但是这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。
匿名类
使用匿名类来对付这些只需要使用一次的类。
比如说使用匿名类来新增加一个打印样式为:哇塞,这是一个大苹果啊?
printApple(apples, new AppleFormater() { public String accept(Apple a) { return "哇塞,这是一个" + (a.getWeight() > 32 ? "大" : "小") + "苹果啊?"; } });
使用匿名类虽然解决了为一个接口声明好几个实体类的啰嗦问题,但它仍然不能令人满意。
使用Lambda表达式
上面的代码可以用Lambda表达式重写为下面的样子:
printApple(apples, (Apple a) -> "哇塞,这是一个" + (a.getWeight() > 32 ? "大" : "小") + "苹果啊?");
这样看起来是不是更清爽多了?更像是在描述问题本身!关于lambda将会在下节介绍。
将List类型抽象化
目前filterApples方法还只适用于Apple,可以将List类型抽象化,让它支持香蕉、橘子、Integer或是String的列表上!
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; }
测试:
List<Apple> greenApple = filter(apples,(Apple apple) -> "green".equals(apple.getColor())); System.out.println(greenApple); List<Integer> evenNumbers = filter(Arrays.asList(1,2,3,4,5,6,10),(Integer i) -> i%2 ==0); System.out.println(evenNumbers);
帅不帅?你现在在灵活性和间接性之间找到了最佳的平衡点,这在java 8之前是不可能做到的!
真实的例子
你现在已经看到,行为参数化是一个很有用的模式,它能够轻松的适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用穿件的行为(例如对Apple的不同谓词)将方法的行为参数化。
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方法来排序,使用匿名类按照重量升序排序:
apples.sort(new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } });
使用lambda如下:
apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
2.用Runnable执行代码块
线程就像是轻量级的进程:它们自己执行一个代码块。在Java离可以使用Runnable接口表示一个要执行的代码块。
package java.lang; @FunctionalInterface public interface Runnable { public abstract void run(); }
使用匿名类创建执行不同行为的线程:
Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("t111"); } });
使用Lambda:
Thread t2 = new Thread(() -> System.out.println("t2222"));
小结:
1.行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
2.行为参数化可让代码更好地适应不断变化的需求,减轻未来的工作量。
3.传递代码,就是将新行为作为参数传递给方法,在java 8 之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在java 8 之前可以使用匿名类来减少。
4.java API 包含很多可以用不同行为进行参数化的方法、包括排序、线程等。