我们在开发的时候,需求永远都是不断变化的。比如客户有查询用户库的功能,第一期客户要求通过姓名进行筛选,第二期客户要求还需要通过年龄进行筛选。我们如何面对客户不断变化的需求?在理想的状态下,我们需要把工作量降低,同时还需要实现新功能要简单,易于维护。
行为参数化就是一种可以帮助我们处理这样需求不断变更的软件开发模式。简单地说,他就是拿出一个代码块,把他提前准备好却不执行。这个代码块可以在程序的其他地方被调用。比如我们可以将这个代码块作为参数传递给别的方法,稍后再去执行他。这样的话,这个方法的行为就被参数化了。例如,我们处理一个集合,可能会写一个方法:
可以对集合中的每个元素做“某件事”可以在集合处理完后做“另一件事”遇到错误是可以做“别的一件事”
行为参数化就是说的这个。在举个例子,妈妈让我们下班的时候去超市买东西,买完回家。就会告诉我们买蔬菜、水果等等。这就相当于调用一个goAndBuy方法,把要买什么东西作为参数。然而,有一个妈妈需要咱们去取一个快递。这个时候就不是goAndBuy方法了,就是一个新的方法了。需要把取快递作为一个参数传递给我们,让我们去执行。
本文我会先举一个例子,不断改进代码从而更灵活的适应不断变化的需求。在此基础上,展示行为参数化的实际例子。例如,使用行为参数化对List进行排序、筛选。或者告诉Thread去执行代码块。很快就会发现Java使用这种模式代码会很啰嗦,Java8中的Lambda就是解决这种代码啰嗦的。在下一篇会展示如何构建Lambda表达式、其使用场合,以及如何利用它让代码更简洁。
二、应对不断变化的需求 直接写出能应对不断变化需求的代码并不容易,让我们逐步改进代码。就实现一个用户库筛选男性的用户吧。是不是感觉很简单。
准备代码:
//用户类class User { private String sex; private String name; private Integer age; public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public User() { } public User(String sex, String name, Integer age) { this.sex = sex; this.name = name; this.age = age; } @Override public String toString() { return "User{" + "sex='" + sex + ''' + ", name='" + name + ''' + ", age=" + age + '}'; }}//初始数据public static void main(String[] args) { List
第一段的代码如下:
public static List
现在要是客户改主意了,还需要筛选女性用户,那我们怎么办呢?简单的方法就是复制这个方法,把名字改成filterWomanUser,然后更改if条件来匹配女性用户。然而,要是客户又想筛选未知性别的就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。
再显身手:把性别作为参数 这一种做法是给方法加一个参数,把性别变成参数,这个就能灵活适应变化了:
public static List
现在,只要先下面只要调用方法就可以完成需求了:
List
很简单吧,让我们把需求弄复杂一点。客户要能区分成年的和未成年的用户,成年的年纪大于等于18。
我们早就想到了,用另一个参数来应对不同的年龄:
public static List
解决方案不错,但是咱们复制了大部分代码来实现遍历用户库,并对每个用户应用筛选条件。如果我们要是需要改变筛选遍历方法去提升性能,那么就需要修改全部的方法,而不是只修改一个。这样工作量将会很大。
当然我们也可以将年龄和性别结合成一个方法,称之为filter。不过就先这样的话,还需要一个方法来区分想要筛选的是哪个属性。我们可以加上一个标志来区别对年龄查询还是对性别查询。但是最好不要这样做,很快我就会解释到。
再接再厉:对你能想到的每个属性做筛选 把两种属性结合起来:
public static List
我们就这个这样使用,不过真的很笨拙:
List
这种解决方式真的很差。首先在调用的时候true和false是什么意思?另外,这个方案还不能应对变化的需求。如果客户又需要筛选年龄,性别,姓名就无法应对了。而且如果客户还需要组合查询,比如要女性成年的用户,就更不好应对了。但现在这种情况下,我们需要一种更好的方法,来吧筛选标准告诉我们的filterUser方法。下面我们来介绍如何使用行为参数化实现这种灵活性。
三、行为参数化 之前我们已经看到了,我们需要一种比添加很多参数更好的方法来应对变化的需求。让我们退一步来看看更高一层的抽象。一种可能的解决方案是对筛选标准建模:我们考虑的是用户,根据User的某些属性(比如年龄,性别)来返回一个boolean值。我们把它称为一个谓词(一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:
public interface UserPredicate { boolean test(User user);}
就可以使用UserPredicate的多个实现代表不同的筛选标准了:
public class UserSexPredicate implements UserPredicate { @Override public boolean test(User user) { return "man".equals(user.getSex()); }}public class UserAgePredicate implements UserPredicate { @Override public boolean test(User user) { return user.getAge() >= 18; }}
我们可以把这些标准看做filter方法的不同行为。我们刚做的这些和策略设计模式相关,定义一类算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是UserPredicate,不同的策略就是UserSexPredicate和UserAgePredicate。
但是该怎么利用UserPredicate的不同实现呢?我们需要接受UserPredicate对象,对User做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。要在我们的例子中实现这一点,你要添加一个参数,让它接受UserPredicate对象。
public static List
现在看这段代码就比开始的时候灵活很多,读起来用起来更加容易。现在我们可以创建不同的UserPredicate对象,并将他们传递给filterUser方法。filterUser方法的行为取决于你通过UserPredicate对象传递的代码。换句话说,你把filterUser方法的行为参数化了!
但是,在UserSexPredicate和UserAgePredicate中真正重要的是test方法的实现,正是它定义了filterApples方法的新行为。但令人遗憾的是,由于该filterUser方法只能接受对象,所以你必须把代码包裹在UserPredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。
Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。但这也不完全令人满意。
通过创建一个用匿名内部类实现UserPredicate的对象,重写筛选的例子:
List
但匿名类还是不够好。它往往很笨重,因为它占用了很多空间。
第六次尝试:使用Lambda表达式 上面的代码可以使用Java8中的Lambda重写如下:
List
是不是比之前的代码简洁很多。
第七次尝试:将List类型抽象化 在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题:
public interface Predicate
现在我们可以把filter方法用在Integer、String等等其他对象的列表上了。如下:
List
在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!
四、真实案例 行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为将方法的行为参数化。这种做法类似于策略设计模式。可能已经在实践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿名类一起使用。我们会展示两个例子:用一个Comparator排序,用Runnable执行一个代码块。
Comparator排序 对集合进行排序是一个常见的编程任务。比如,客户想要用户库根据用户年龄进行排序,或者他可能改了主意,希望你根据姓名进行排序。听起来有点儿耳熟?是的,你需要一种方法来表示和使用不同的排序行为,来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:
public interface Comparator
因此,你可以随时创建Comparator的实现,用sort方法表现出不同的行为。用Lambda表达式的话,看起来就是这样:
//根据用户年龄排序users.sort((User user1, User user2) -> user1.getAge().compareTo(user2.getAge()));//根据用户姓名排序users.sort((User user1, User user2) -> user1.getName().compareTo(user2.getName()));
用Runnable执行代码块 线程就像是轻量级的进程:它们自己执行一个代码块。但是,怎么才能告诉线程要执行哪块代码呢?多个线程可能会运行不同的代码。我们需要一种方式来代表稍候执行的一段代码。在Java里,你可以使用Runnable接口表示一个要执行的代码块。请注意,代码不会返回任何结果(即void):
public interface Runnable { public abstract void run();}
我们可以像下面这样,使用这个接口创建执行不同行为的线程:
new Thread(() -> System.out.println("hello world!"));
五、小结行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程。