概要
值对象是一些单一的参数,用来联合一系列的对象——在大多数的情况下,在一个方法调用里有各种各样的参数。这些参数描述了一个大数量级的属性,通常,这些属性需要单独检测,而且大多数情况下是检测其是否为null。通常,这些检测带出来了大量的代码行。这篇文章描述了如何实现基于著名的Visitor模式和反射的值对象。
在业务过程中,你通常有一些属性不能为空,而另外一些则没有这样的要求。在那些必须有实例的属性的案例中,你不得不实现如下所示的检测:
if( attribute1 == null )
{
throw new Attribute1IsNullException()
}
如果值对象有N个属性,你将会得到如下所示的代码:
if( attribute1 == null )
{
throw new Attribute1IsNullException()
}
if( attribute2 == null )
{
throw new Attribute2IsNullException()
}
...
if( attribute N == null )
{
throw new AttributeNIsNullException()
}
结果:一大堆的IF语句,但是你不得不把它们全部打出来。
现在假设校验的数量从10增加到25,因为有15个新增的用例必须在一个循环中实现。你是不是失去了勇气?用来减少这些检测的一个有效的方法是将他们从值对象类移到值对象的校验类。
从这个观点看来,你可能承认你永远执行相同的检测。唯一的不同是属性的名称和类型。在大多数情况下,类型不让人感兴趣,因为由编译器检测它。还有重要的一点需要确认:接收这些属性的值的方法都由同一个名称开始,在我们的案例中,是get。
通过反射调用这些值对象的getters方法非常简单。如果你使用Eclipse,例如,你可以自动为所有的属性产品setters和getters方法。对于我们的attribute1,getter方法是getAttribute1(),setter方法为setAttribute1(Integer attributeValue)。如果attribute1是Integer类型的属性。如果这些前提给定了的话,你就能考虑一个一般的解决方案。这篇文章解释了如何使用Visitor模式和反射来实现这个一般的解决方案。
框架类和接口
下面的类图显示了建立我们一般的校验框架需要用到的类和接口之间的关系:
注意:你可以从Resources上下载这些类和接口
Validateable接口
Validateable接口有着和Visitable接口相同的功能。那个定义的方法validateWith()是一个和Visitor模式里的Visitable接口的accept()方法相似的方法。通过validateWith()方法,你就能够校验有着不同validators的值对象,因为这个方法以IClassAttributeValidator接口的实现作为参数。
IClassAttributeValidator接口
IClassAttributeValidator接口和Visitor模式的Visitor接口相对应。其中的validate(AbstractParameter param)方法和Visitor接口的visit(object SomeObject)方法相似。validate()方法的AbstractParameter参数类型允许我们通过任何AbstractParameter类的子类类型的参数访问validate。另外,在validateWith()方法里使用这个接口作为参数允许我们在将来改变使用的validator,用这些改变的validator作为来满足不同的validation的需求——例如,除了null检测以外,在一个定义的值范围测试参数属性。
AbstractParameter
AbstractParameter类实现了Validateable接口的validateWith()方法。就像你将要看到的下面的代码片断一样,这个实现非常简单。这个方法仅仅是调用给定的validator的validate()方法,并且传递参数对象到validator:
public void validateWith( IClassAttributeValidator validator ) throws Exception
{
validator.validate( this );
}
而且,AbstractParameter也实现一些常用的其他方法。受保护的方法:addOptionalMethod()使得所有的子类型增加一些可选择的方法到optionalGetAttributeMethods HashMap()。继承自AbstractParameter使得你能够取得哪些可能传递null的getters方法。就像你能想象到的一样,你能够增加可选择的方法,例如,到继承自AbstractParameter的值对象的构造器里。
isOptionalMethod()方法用于检测属性是否已经被校验过了。
toString()方法实现了一些便利,因为值对象可能是由很多属性组成。使得在AbstractParameter的子类里,不需要写很多的System.out.printlns实现。这个方法也是使用反射达到目的的。
GenericClassAttributeValidator
GenericClassAttributeValidator类实现了IClassAttributeValidator接口的validate()方法。这个类同时也实现了单态模式。validate()的实现看起来象下面这样:
public synchronized void validate( AbstractParameter param ) throws AttributeValidatorException
{
Class clazz = param.getClass();
Method[] methods = clazz.getMethods();
//Cycle over all methods and call the getters!
//Check if the getter result is null.
//If result is null throw AttributeValidatorException.
Iterator methodIter = Arrays.asList( methods ).iterator();
Method method = null;
String methodName = null;
while ( methodIter.hasNext() )
{
method = (Method) methodIter.next();
methodName = method.getName();
if ( methodName.startsWith( "get" ) &&
clazz.equals( method.getDeclaringClass() ) &&
!param.isOptionalMethod( methodName ) )
{
Object methodResult = null;
try
{
methodResult = method.invoke( param, null );
}
catch ( IllegalArgumentException e )
{
throw new AttributeValidatorException( e.getMessage() );
}
catch ( IllegalAccessException e )
{
throw new AttributeValidatorException( e.getMessage() );
}
catch ( InvocationTargetException e )
{
throw new AttributeValidatorException( e.getMessage() );
}
if ( methodResult == null )
{
String attributeName = methodName.substring( 3, 4 ).toLowerCase() +
methodName.substring( 4, methodName.length() );
String className = clazz.getName();
className = className.substring( className.lastIndexOf( '.' ) + 1 );
Integer errorNumber = new Integer( 1000 );
throw new AttributeValidatorException( "Error: " + errorNumber + " "
+ attributeName + " in " + className +" is null!!!");
}
}
}
}
首先,就像你在代码里看到的那样,我们从值对象里取得所有的方法。然后,我们遍历所有方法的集合。如果方法以get开头,便是AbstractParameter的子类型,而不是可选择的方法。我们通过反射调用getter方法,并且检测它的结果。如果结果是null,那么这就是一个错误;如果不是,便是正常情况。那些可选择的方法和继承自父类的方法不会被执行。
测试我们的类
现在,我们实现了我们所需要的所有的类和接口。我们必须做一些测试来检验我们的类是否能够正常工作。为了做到这一点,我们写了一点小的测试类和一个main方法来运行测试。
TestParameter
TestParameter类继承自AbstractParameter,并且包括了一些需要校验的私有属性:很简单的4个Integer属性。
Optional attributes
为了识别可选的属性没有被检测,我们定义了为属性:testParam3可选的getter方法。为了这个目的,我们通过TestParameter的构造器里的addOptionalMethod(methodName)方法将这个getter方法输入到父类AbstractParameter的可选方法map里。
校验框架是如何工作的
为了测试,我们在TestParameter里使用如下方式输入:
TestParameter param = new TestParameter( );
param.setTestParam1( new Integer( 1 ) );
param.setTestParam2( new Integer( 2 ) );
param.setTestParam3( new Integer( 3 ) );
param.setTestParam4( new Integer( 4 ) );
就像你所看到的那样,4个Integer属性记作Integer1,2,3和4。为了校验,我们仅仅调用:param.validateWith( GenericClassAttributeValidator.getInstance( ) );
这个校验的结果是:
testParam1: 1
testParam2: 2
testParam4: 4
testParam3属性没有被校验,因为我们记录了它的getter方法为可选的。其他所有的方法得到了校验,并且结果是正常的。现在,我们希望看到其中的一个属性值为空,这样我们就能检测是否validator能够检测到这个错误。我们注释掉下面的行:
param.setTestParam2( new Integer( 2 ) );
我们重新开始测试以后,得到如下的结果:
testParam1: 1
Error: testParam2 in TestParameter is null!!!
testParam4: 4
现在我们看到了validator已经检测到了这个没有赋值的属性。
如果属性类型为集合类型,将会怎么样呢?
如果属性类型为集合,它仍然会检测这个集合是否为空。但是,可能检测集合是否为null并不是你想要的。在大多数情况下,你希望检测集合里的对象是否为null。如果集合的实现不允许null对象,你不需要关心这些在GenericClassAttributeValidator里的null对象继承自AbstractParameter。一些为集合保持继承自AbstractParameter的对象的辅助代码看起来如下所示:
if ( methodResult instanceof Collection )
{
Collection col = (Collection) methodResult;
Iterator iter = col.iterator();
Object subParam = null;
while ( iter.hasNext() )
{
subParam = iter.next();
if ( subParam instanceof AbstractParameter )
{
AbstractParameter abstractParam = ( AbstractParameter ) subParam;
abstractParam.validateWith( this );
}
}
}
集合里的所有没有继承自AbstractParameter类的对象没有被检测,因为我们将使用一个不允许null对象的集合实现。所以集合实现为我们完成了检测。如果你决定使用一个允许null对象的实现,那么为while循环的所有其他的对象的一个额外的null检测就是必须的了:
else if( subParam == null )
{
System.out.println( "Error: SubParameter not set in Collection!" );
}
值之间的依赖
在一些情况下,只有当值对象的其他属性被分配了值,一个属性才有可能是可选的。属性的“可选性” sometimesOptional依赖于actionType属性的值。可能action属性持有的值代表了actions:例如addSomething = 1, updateSomething = 2, 和 deleteSomthing = 3。如果action的值是1或者3,sometimesOptional属性不是可选的;如果action的值是2,则是可选的。当我们为actionType赋值的时候,我们必须设置sometimesOptional的可选性:
public void setActionType(int actionType)
{
this.actionType = actionType;
super.clearOptionalMethods( );
switch( this.actionType )
{
case ActionParameter.ACTION_ADD :
super.addOptionalMethod( "getSometimesOptional4" );
super.addOptionalMethod( "getSometimesOptional5" );
break;
case ActionParameter.ACTION_UPDATE :
super.addOptionalMethod( "getSometimesOptional1" );
break;
case ActionParameter.ACTION_REMOVE :
super.addOptionalMethod( "getSometimesOptional1" );
super.addOptionalMethod( "getSometimesOptional2" );
super.addOptionalMethod( "getSometimesOptional3" );
break;
default :
break;
}
}
你会看到清除可选方法列表是必需的,因为如果你给actionType赋值超过一次的话,越来越多的方法将作为可选的方法添加进来。另外的一个解决方法包括实现一个AddActionParameter,一个UpdateActionParameter和一个RemoveActionParameter,它们都是从AbstractParameter类继承得来。那么你可能不需要actionType属性。但是拥有actionType属性的类存在并且经常被使用,对该类使用反射非常容易,你必须使用Switch语句。
展望
现在,我们可以考虑继承AbstractParameter的更多的功能——例如,范围校验。AbstractParamter需要一个数据结构来存储范围值。HashMap能够做到,它以方法名作为key存储范围对象。或者你可以检测是否一个String类型的值包含一些定义的字。等等。你也可以考虑Perl 5的正则表达式。所有的这些检测都可以在Validator类里实现,它实现了IClassAttributeValidator接口。如果你想使用属性的null检测和附加值检测,那么你可以写一个子类来继承GenericClassAttributeValidator。
在J2EE应用里,值对象经常被用来在客户端和服务器之间传递业务过程的数据。但是,如果你仅仅在服务器端校验这些值对象的属性,你常常不得不因为一个错误的非可选属性为null而取消业务过程。你必须中断业务过程,而向客户端发送一个错误页面。这是一个好的实践:在服务端应用这些validators的同时,你也可以以委派的形式在客户端应用它们。在客户端检测值对象可以避免不必要的对服务器的请求和降低网络堵塞。
写一次,使用多次
如果你使用我描述的方法来检测你的值对象的属性,你可以永远只增加值对象的属性——不用改变validator,它们能够被自动检测是否为null。你也可以不用改变validator而改变一个属性的条件。而且,当然,一个已有的validator也能够校验一个未来的值对象,如果这个值对象继承AbstractParameter的话。还有,你也可以不用改变值对象而写一个额外的validators,因为validators实现的是Visitor模式。这就是所谓的写一次,使用多次。