引自:http://www.blogjava.net/flyingis/archive/2005/11/09/18958.html
2.Commons Collections中的算子
算子成为Commons Collections 3.1中的有趣的部分有两个原因:它们没有得到应得的重视并且它们有改变你编程的方式的潜力。算子只是一个奇特的名字,它代表了一个包装了函数的对象—一个“函数对象”。当然,它们不是一回事。如果你曾经使用过C和C++的方法指针,你就会理解算子的威力。
一个算子是一个对象—一个Predicate,一个Closure, 一个Transformer。
Predicates求对象的值并返回一个boolean,Transformer求对象的值并返回新对象,Closure接受对象并执行代码。算子可以被组合成组合算子来模仿循环,逻辑表达式,和控制结构,并且算子也可以被用来过滤和操作集合中的元素。在这么短的篇幅中解释清楚算子是不可能的,所以跳过介绍,我将会通过使用和不使用算子来解决同一问题(解释算子)。在这个例子中,从一个ArrayList中而来的Student对象会被排序到两个List中,如果他们符合某种标准的话。
成绩为A的学生会被加到honorRollStudents(光荣榜)中,得D和F的学生被加到problemStudents (问题学生)list中。学生分开以后,系统将会遍历每个list,给加入到光荣榜中学生一个奖励,并安排与问题学生的家长谈话的时间表。下面的代码不使用算子实现了这个过程:
List allStudents = getAllStudents();
// 创建两个ArrayList来存放荣誉学生和问题学生
List honorRollStudents = new ArrayList();
List problemStudents = new ArrayList();
// 遍历所有学生,将荣誉学生放入一个List,问题学生放入另一个
Iterator allStudentsIter = allStudents.iterator();
while( allStudentsIter.hasNext() ) {
Student s = (Student) allStudentsIter.next();
if( s.getGrade().equals( "A" ) ) {
honorRollStudents.add( s );
} else if( s.getGrade().equals( "B" ) &&
s.getAttendance() == PERFECT) {
honorRollStudents.add( s );
} else if( s.getGrade().equals( "D" ) ||
s.getGrade().equals( "F" ) ) {
problemStudents.add( s );
} else if( s.getStatus() == SUSPENDED ) {
problemStudents.add( s );
}
}
// 对于的有荣誉学生,增加一个奖励并存储到数据库中
Iterator honorRollIter =
honorRollStudents.iterator();
while( honorRollIter.hasNext() ) {
Student s = (Student) honorRollIter.next();
// 给学生记录增加一个奖励
s.addAward( "honor roll", 2005 );
Database.saveStudent( s );
}
// 对所有问题学生,增加一个注释并存储到数据库中
Iterator problemIter = problemStudents.iterator();
while( problemIter.hasNext() ) {
Student s = (Student) problemIter.next();
// 将学生标记为需特殊注意
s.addNote( "talk to student", 2005 );
s.addNote( "meeting with parents", 2005 );
Database.saveStudent( s );
}
上述例子是非常过程化的;要想知道Student对象发生了什么事必须遍历每一行代码。例子的第一部分是基于成绩和考勤对Student对象进行逻辑判断。
第二部分对Student对象进行操作并存储到数据库中。像上述这个有着50行代码程序也是大多程序所开始的—可管理的过程化的复杂性。但是当需求变化时,问题出现了。一旦判断逻辑改变,你就需要在第一部分中增加更多的逻辑表达式。
举例来说,如果一个有着成绩B和良好出勤记录,但有五次以上的留堂记录的学生被判定为问题学生,那么你的逻辑表达式将会如何处理?或者对于第二部分中,只有在上一年度不是问题学生的学生才能进入光荣榜的话,如何处理?当例外和需求开始改变进而影响到过程代码时,可管理的复杂性就会变成不可维护的面条式的代码。
从上面的例子中回来,考虑一下那段代码到底在做什么。它在一个List遍历每一个对象,检查标准,如果适用该标准,对此对象进行某些操作。上述例子可以进行改进的关键一处在于从代码中将标准与动作解藕开来。下面的两处代码引用以一种非常不同的方法解决了上述的问题。首先,荣誉榜和问题学生的标准被两个Predicate对象模型化了,并且加之于荣誉学生和问题学生上的动作也被两个Closure对象模型化了。这四个对象如下定义:
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
// 匿名的Predicate决定一个学生是否加入荣誉榜
Predicate isHonorRoll = new Predicate() {
public boolean evaluate(Object object) {
Student s = (Student) object;
return( ( s.getGrade().equals( "A" ) ) ||
( s.getGrade().equals( "B" ) &&
s.getAttendance() == PERFECT ) );
}
};
//匿名的Predicate决定一个学生是否是问题学生
Predicate isProblem = new Predicate() {
public boolean evaluate(Object object) {
Student s = (Student) object;
return ( ( s.getGrade().equals( "D" ) ||
s.getGrade().equals( "F" ) ) ||
s.getStatus() == SUSPENDED );
}
};
//匿名的Closure将一个学生加入荣誉榜
Closure addToHonorRoll = new Closure() {
public void execute(Object object) {
Student s = (Student) object;
// 对学生增加一个荣誉记录
s.addAward( "honor roll", 2005 );
Database.saveStudent( s );
}
};
// 匿名的Closure将学生标记为需特殊注意
Closure flagForAttention = new Closure() {
public void execute(Object object) {
Student s = (Student) object;
// 标记学生为需特殊注意
s.addNote( "talk to student", 2005 );
s.addNote( "meeting with parents", 2005 );
Database.saveStudent( s );
}
};
这四个匿名的Predicate和Closure是从作为一个整体互相分离的。flagForAttention(标记为注意)并不知道什么是确定一个问题学生的标准 。现在需要的是将正确的Predicate和正确的Closure结合起来的方法,这将在下面的例子中展示:
import org.apache.commons.collections.ClosureUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.functors.NOPClosure;
Map predicateMap = new HashMap();
predicateMap.put( isHonorRoll, addToHonorRoll );
predicateMap.put( isProblem, flagForAttention );
predicateMap.put( null, ClosureUtils.nopClosure() );
Closure processStudents =
ClosureUtils.switchClosure( predicateMap );
CollectionUtils.forAllDo( allStudents, processStudents );
在上面的代码中,predicateMap将Predicate与Closure进行了配对;如果一个学生满足作为键值的Predicate的条件,那么它将把它的值传到作为Map的值的Closure中。通过提供一个NOPClosure值和null键对,我们将把不符合任何Predicate条件的Student对象传给由ClosureUtils调用创建的“不做任何事”或者“无操作”的NOPClosure。
一个SwitchClosure, processStudents,从predicateMap中创建。并且通过使用CollectionUtils.forAllDo()方法,将processStudents Closure应用到allStudents中的每一个Student对象上。这是非常不一样的处理方法;记住,你并没有遍历任何队列。而是通过设置规则和因果关系,以及CollectionUtils和SwitchClosur来完成了这些操作。
当你将使用Predicate的标准与使用Closure的动作将分离开来时,你的代码的过程式处理就少了,而且更容易测试了。isHonorRoll Predicate能够与addToHonorRoll Closure分离开来进行独立的单元测试,它们也可以合起来通过使用Student类的模仿对象进行测试。第二个例子也会演示CollectionUtils.forAllDo(),它将一个Closure应用到了一个Collection的每一个元素中。
你也许注意到了使用算子并没用减少代码行数,实际上,使用算子还增加了代码量。但是,通过算子,你得到了将到了标准与动作的模块性与封装性的好处。如果你的代码题已经接近于几百行,那么请考虑一下更少过程化处理,更多面向对象的解决方案—通过使用算子。
Jakarta Commons Cookbook中的第四章“算子”介绍了Commons Collections中可用的算子,在第五章,“集合”中,向你展示了如何使用算子来操作Java 集合类API。
所有的算子-- Closure, Predicate, 和 Transformer—能够被合并为合并算子来处理任何种类的逻辑问题。switch, while和for结构能够被SwitchClosure, WhileClosure, 和 ForClosure模型化。
复合的逻辑表达式可以被多个Predicate构建,通过使用OrPredicate, AndPredicate, AllPredicate, 和 NonePredicate将它们相互联接。Commons BeanUtils也包含了算子的实现被用来将算子应用到bean的属性中-- BeanPredicate, BeanComparator, 和 BeanPropertyValueChangeClosure。算子是考虑底层的应用架构的不一样的方法,它们可以很好地改造你编码实现的方法。