1. 背景
在软件工程中,一个众所周知的问题是,不管你做什么,客户的需求是肯定会变化的。如何去应对这种不断变化的
需求,尽量降低工作量,行为参数化就是一种可以帮助你处理频繁变更需求的软件开发一种模式。
2. 定义
行为参数化就是一个方法接收多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。它意味着拿出一个代码块,把它准备好却不马上执行它。这个代码块以后可以被你其它程序部分调用,这意味着你可以推迟这代码块的执行。
3. 实战
编写能够应对需求变化的代码并不容易。让我们通过一个简单的例子,我们会逐步改进这个例子,以展示一些让代码更具灵活性的方式方法。就一个学校师生管理程序而言,你必须实现一个学生列表的筛选功能。so easy?
1. 牛刀小试:筛选一年级的学生
第一个解决方案可能如下:
public static List<Student> filterFirstYearStudents(List<Student> peopleList) {
List<Student> results = new ArrayList<>();
for (Student student : peopleList) {
if (student.getGrade() == 1) {
results.add(student);
}
}
return results;
}
student.getGrade() == 1这句代码就是筛选条件,但是现在客户改主意了,还想要筛选二年级的学生。简单解决办法是复制上面部分的代码,将函数名称修改为filterSecondYearStudents,然后修改条件。这种修改方式无法满足客户想要筛选各个年级等需求。
2. 再展身手:把年级作为参数
给方法加一个参数,把年级变为参数,这样就能灵活的适应变化了:
public static List<Student> filterStudentsByGrade(List<Student> students, int grade) {
List<Student> results = new ArrayList<>();
for (Student student : peopleList) {
if (student.getGrade() == grade) {
results.add(student);
}
}
return results;
}
虽然当前代码能灵活应对年级属性作为筛选条件的情况,但是若客户需要对学生的各个属性做筛选呢?
3. 第三次尝试:把各属性作为参数
这里我们假设学生的属性就grade、sex、totaleScore三项,则根据以上经验代码为:
public static List<Student> filterStudents(List<Student> peopleList, int grade, char sex, double totalScore) {
List<Student> results = new ArrayList<>();
for (Student student : peopleList) {
if (grade != 0) {
if (student.getGrade() == grade) {
results.add(student);
}
} else if (sex != 0) {
if (student.getSex() == sex) {
results.add(student);
}
} else if (totalScore != 0.0) {
if (student.getTotalScore() == totalScore) {
results.add(student);
}
}
}
return results;
}
这个解决方案不能再low了,此外,这段代码还是不能很好的应对客户变化的需求。比如,客户想要筛选一年级的女生,或者二年级总分在250及以上的男生,等等这种组合属性的筛选。
4. 第四次尝试:根据抽象条件筛选
通过上述尝试可以看到,我们需要一种比添加很多参数更好的方法来适应灵活多变的需求。一种可能的解决方案是:对我们的筛选条件进行建模,此处考虑的是学生,通过判断其属性(年级、性别、总分…),然后返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。如下是我们定义的选择标准建模:
public interface StudentPredicate {
boolean test(Student student);
}
现在我们可以用StudentPredicate的多个实现来代表不同的选择标准了,比如:
public class StudentFirstGradePredicate implements StudentPredicate {
@Override
public boolean test(Student student) {
return student.getGrade() == 1;
}
}
public class StudentGirlPredicate implements StudentPredicate {
@Override
public boolean test(Student student) {
return student.getSex() == 'W';
}
}
利用StudentPredicate改造之后,filter看起来如下:
public static List<Student> filterStudents(List<Student> peopleList, StudentPredicate predicate) {
List<Student> results = new ArrayList<>();
for (Student student : peopleList) {
if (predicate.test(student)) {
results.add(student);
}
}
return results;
}
至此,我们可以小小的庆祝一下,这段代码比我第一次尝试的灵活多了,读起来、用起来也更容易。我们现在可以创建不同的StudentPredicate实现来满足客户的不同需求了。filterStudents方法的行为取决于我们通过StudentPredicate参数传递的代码。换句话说,我们把filterStudents方法的行为参数化了。
5. 第五次尝试:使用匿名类
当要把新的行为传递给filterStudents时,我们必须声明好几个实现StudentPredicate接口的类,然后实例化这些可能只会使用一次的对象,这真是很啰嗦且浪费时间。有没有什么更好点的方式?Java有一个称为匿名类的机制,他可以同时让你声明和实例化一个接口类。使用匿名类改进的代码如下:
List<Student> firstGradeStudents = filterStudents(students, new StudentPredicate() {
@Override
public boolean test(Student student) {
return student.getGrade() == 1;
}
});
匿名类让我们节省去声明StudentPredicate接口类的时间,但它还是稍显笨重,且用起来也会让人费解,尤其是当方法体中引入了this指针时。我们下一部分将讨论Java8的另一特性:Lambda表达式。