如果你熟悉動態語言,你大概會嘗試使用 Java 的反射(reflection)來重構程式碼。我個人提供一個重構經驗,告訴你使用 Java 的反射時,你可能會感到失望。
這是一段透過 Hibernate 進行的資料更新動作。我從使用者端取得要更新的資料項,接著先向 Hibernate 查詢要更新的資料項目是否存在,存在的話再把新的資料內容更新進去。
我要求「查詢是否存在」與「更新資料」這兩個動作要在同一筆交易中進行,看起來應該不會有什麼麻煩。不幸的是, Hibernate 本於 ORM 觀念,將資料項繫結到了查詢動作所取得的 oldItem 上。因此當我要求 Hibernate 使用另一個 item 內的資料更新時, Hibernate 拒絕了我的要求。儘管 id 欄位值一樣,但它不承認 item 是指定資料項的新內容,它認為我應該要用 oldItem 更新。
對此,我一開始用了一個直接的解法。程式碼於下。
public ReturnCode update ( Product item ) {
ReturnCode rc = ReturnCode . Success ;
session = sessionFactory . openSession ();
Product oldItem = null ;
try {
tx = session . beginTransaction ();
oldItem = session_findById ( session , item . getId ());
if ( oldItem != null ) {
oldItem . setContent ( item . getContent ());
oldItem . setTitle ( item . getTitle ());
oldItem . setPrice ( item . getPrice ());
//我知道這樣做非常愚蠢。但是ORM只認同這種做法。
//幸好這個資料結構只有兩三個欄位,而不是十幾個欄位。
session . update ( oldItem );
}
else {
rc = ReturnCode . NotExists ;
}
tx . commit ();
}
catch ( HibernateException e ) {
System . out . println ( e . getMessage ());
rc = ReturnCode . Failed ;
}
session . close ();
return rc ;
}
請看上列程式碼中間那一連三行的 oldItem.setXxx( item.getXxx() )
。
我知道這樣做非常愚蠢,因為它不斷的重複相同動作,犯了 DRY (Don't Repeat Yourself) 的錯誤。
幸好這個資料結構只有兩三個欄位,而不是十幾個欄位。
但如果我日後改變設計,增改欄位,那麼我勢必要回頭進來這兒增刪程式碼。
Ok, 為了避免修改時的邊際效應,我決定用 Java 笨重的反射功能重構上述的程式碼。重構結果於下。
public ReturnCode update ( Product item ) {
ReturnCode rc = ReturnCode . Success ;
session = sessionFactory . openSession ();
Product oldItem = null ;
try {
tx = session . beginTransaction ();
oldItem = session_findById ( session , item . getId ());
if ( oldItem != null ) {
//oldItem.setContent(item.getContent());
//... !!!DRY!!!!
try {
Class < Product > c = ( Class < Product >) item . getClass ();
String setMethodName = null , getMethodName = null ;
Method methods [] = c . getMethods ();
for ( Method method : methods ) {
setMethodName = method . getName ();
//找出 setXxx()
if ( setMethodName . matches ( "^set[A-Z].*" ) ) {
//得出新方法名稱 getXxx
getMethodName = setMethodName . replaceFirst ( "^set" , "get" );
Method getMethod = c . getMethod ( getMethodName );
// object = item.getXxx()
Object object = getMethod . invoke ( item , ( Object []) null );
// oldItem.setXxx(object)
method . invoke ( oldItem , object );
}
}
session . update ( oldItem );
}
catch ( Exception e ) {
// relfection exception...
System . out . println ( e . getMessage ());
rc = ReturnCode . Failed ;
}
}
else {
rc = ReturnCode . NotExists ;
}
tx . commit ();
}
catch ( HibernateException e ) {
System . out . println ( e . getMessage ());
rc = ReturnCode . Failed ;
}
session . close ();
return rc ;
}
重構的結果看起來挺複雜的,但其實它只是要做動態語言兩行可以表達的事。
PHP 表達的方式是:
foreach ( $oldItem as $key => $value )
$oldItem -> $key = $item -> $key ;
JavaScript 表達的方式是:
for ( var key in oldItem )
oldItem [ key ] = item [ key ]
《超越Java (Beyond Java)》的作者 Tate 在書中說: 如果你花時間在 Smalltalk ,你大概會更常使用 Java 的反射 。我覺得這一點還要接上後半句: 好吧,我可能對你的要求太嚴格了 。
我一直認為 Reflection 其實是 Java/C# 等缺乏個體自識能力之語言才有的詞彙 (見什麼是Reflection? ),在 從中介編程與反射能力來談 Java 語言 系列文章中也說過: 在強型態的動態語言中,一個個體認識自己(自識)是再自然不過的事,所謂反射就像呼吸一樣自然,讓人感覺不到它的存在 。正因為在動態語言中,自識(反射)處理起來非常地自然,所以我們才會頻繁地使用的,甚至沒有意識到這個動作有何特殊。
然而,當在我 Java 中嘗試使用反射能力處理本文所提的這項應該很簡單的 重構動作時,我卻有強烈地窒息感。在動態語言中,這只不過是一件到公園去呼吸新鮮空氣般輕鬆的事。 Java 卻強迫我們載上氧氣瓶與面罩去爬山。
那段重構後的程式碼,迂迴曲折,無法讓人一眼看出它的意圖,醜得連它媽媽都不認得(呃... 好像是我寫的)。熟悉動態語言編程的程序員,確實會想要用 Java 的反射來做一些很自然的事,但是我們只會感到窒息。當我艱苦地達成目標後,我只想對那段程式碼吐口水,存檔之後就再也不想回頭多看幾眼了。
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/10867569.html
樂多舊回應