嘗試使用 Java 的 reflection 重構指派資料欄位值的程式碼
如果你熟悉動態語言,你大概會嘗試使用 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 的反射來做一些很自然的事,但是我們只會感到窒息。當我艱苦地達成目標後,我只想對那段程式碼吐口水,存檔之後就再也不想回頭多看幾眼了。
樂多舊回應