概要
值對象是一些單一的參數,用來聯合一系列的對象——在大多數的情況下,在一個方法調用里有各種各樣的參數。這些參數描述了一個大數量級的屬性,通常,這些屬性需要單獨檢測,而且大多數情況下是檢測其是否為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模式。這就是所謂的寫一次,使用多次。