設計模式(design pattern)為我們提供了一條途徑,使我們能夠進行簡潔明了地溝通,以得到期望的軟件經驗;反模式與此類似,只不過它是用於溝通那些不合乎需求的經驗的--下面是一些可以幫助你起步的通常遇到的反模式。 反模式是一種典型的、糟糕的設計;換句話說,它是設計模式的對立面--設計模式提出的是良好的設計。在某種意義上講,反模式表現的是糟糕的解決方案,它讓相關人員更容易理解根本的問題和問題之間的因果關系。盡管了解設計模式很重要,但是我相信理解反模式也是一樣重要的--我們應該理解反模式。
我們來證明一下自己的觀點。軟件世界是圍繞應用程序的維護來"運行"的。當然,每個軟件產品的生命周期都是從構造的時候開始的,但是在開始大量生產以後,就需要維護了。根據開發小組的技巧,產品可能擁有"良好的"或"糟糕的"設計,其中的"良好"或"糟糕"是對應於一定的環境中,因為一種良好的設計如果應用在錯誤的環境中也可能成為一種反模式。例如,在單應用程序(single-application)服務器環境中使用Singleton是恰當的,但是在群集應用程序(clustered-application)服務器環境中,如果它沒有被正確地處理好,就會產生很多問題。與正面的設計模式形成對比的是,反模式引出的是負面的解決方案或以前的方法(去年的解決方案在今年就可能是反模式了),這可能是由於小組成員缺乏足夠的信息,或者在實現設計或解決問題方面作出了糟糕的判斷。
在產品開始生產並進入維護模式之後,真正的問題才開始顯現。一個從未參與產品開發的從事維護工作的開發者可能給項目引入"糟糕的設計"元素。但是如果這種糟糕的實踐被編寫為反模式"法典",就可以預先提醒開發者,避免最普通的缺陷,就像設計模式"法典"為開發者提供了識別出普通的有用的技術途徑一樣。根據這種邏輯,反模式是一種值得我們記載的普遍存在的糟糕的設計。
當我們使用J2EE等技術的時候,這種方法的優勢尤其明顯。J2EE的初始設計哲學強調簡單性,但是它的復雜程度已經變得難以置信了。在這種復雜的環境中,模式和反模式同時為軟件經理、架構師、設計師和開發者提供了通用的"詞典"。
無論在構造模式還是在維護模式中,為了獲得成功,我們理解反模式都是必要的。在反模式被記錄下來之後,開發者一般可以認識到這些負面的模式,以根除糟糕的設計,改善軟件。
本文從軟件架構和開發的角度來談論反模式。接著它提出了在J2EE應用程序的大多數通用層次(用戶界面、永續性、EJB等)中普遍存在的反模式。它的全部目標是為這些反模式提供背景知識,並為避免這些問題提供建議。
表1列舉了本文中討論的三種普遍的設計、開發和架構的反模式。
表1:普遍的反模式
領域
普通的反模式
設計
編寫具體的類而不是接口在代碼中耦合了邏輯(例如日志記錄、安全性和緩沖)
開發
Golden Hammer(金錘)Input Kludge(輸入雜亂)
架構
Reinvent the wheel(重新發明輪子)Vendor lock-in(廠商的鎖定)
普遍的反模式 上表列舉的反模式跨越了廣闊的開發者領域。
編寫具體的類而不是接口
這是一條重要的設計原則,但是卻經常被破壞--編寫接口而不是具體的類可以提供數不清的優點!你不會被"捆綁"在使用某種特定的實現上,同時可以在運行時改變行為。"接口"這個術語意味著要麼是一個Java接口,要麼是一個抽象類。只要你應用多態性(polymorphism),應用程序的行為就不會被"鎖定"在特定的代碼中。請注意,當你知道其行為不會改變的時候,這條規則就不適用了。
編寫一個實現的例子如下所示:
Dog animal = new Dog();
animal.bark();
作為對比,編寫一個接口的例子是:
Animal animal = getAnimal(Dog.class);
animal.makeNoise();
上面的兩個例子有兩個不同點。第二個版本使用制造廠(factory)方法來動態地獲取Dog類的實例。同時,第二個版本泛化(generalize)了bark()方法,它是makeNoise()方法的Dog具體實現,而任何動物都可以實現這個方法。下面的代碼顯示了AnimalFactory類的getAnimal()方法和Dog類,它舉例說明了一個特定的Animal實現。
getAnimal(Class c)
{
if (c == Dog.class)
return new Dog();
// 此處測試其它類型
}
class Dog
{
makeNoise()
{
bark();
}
}
過多的耦合
我們在編寫代碼的時候,需要緊記一條基本的軟件觀念--一般情況下,耦合越少越好。例如,你編寫一段代碼來執行某項事務,那麼它就應該只執行該項事務,這樣代碼才整潔、容易閱讀、易於維護。但是有些東西是不可避免的,例如日志記錄、安全性和緩沖等方面可能需要耦合。幸運的是,有些辦法可以避免這種情況的發生。其中的一種技術--面向方面的編程(AOP)通過在編譯時(compile time)給應用程序注入方面(ASPect)的行為,為達到這個目的提供了一條靈巧的途徑。
開發:金錘反模式
過多地或者強制性地使用某種技術或模式可能會導致金錘反模式。精通特定技術或軟件的團體或個人傾向於在特性相似的其它項目中也使用類似的技術--即使其它的技術更適合那種情況。他們把不熟悉當作是冒險。此外,計劃和評估熟悉的技術也更加簡單。通過書本、培訓和用戶組(例如Java用戶組)的形式來擴充開發者的知識對避免這種反模式非常有益。
輸入信息雜亂
軟件錯誤地處理簡單的用戶輸入信息就會形成輸入信息雜亂。例如,Web站點讓用戶輸入ID和密碼來登錄,那麼它接受的輸入內容就應該僅僅是可用作ID和密碼的字符。如果該站點的邏輯拒絕了無效的輸入,那麼它在這個方面就是安全的,但是如果它沒有拒絕這些無效的輸入,就可能出現不可預料的結果。輸入信息雜亂很容易被最終用戶發現,但是在開發者進行單元測試的時候很難發現。
你可以使用怪用測試(monkey test)來檢測輸入信息雜亂的問題。雖然它超出了本文討論的范圍,但是它的確在沒有任何"典型用戶"偏好的情況下,實現了隨機的自動化測試。
架構反模式
這些反模式廣泛地出現在應用程序架構中。
重新發明輪子
這個術語不需要任何解釋。在開發軟件的時候,一般有兩種選擇--在已有的技術上建立或者從草稿開始。盡管在不同的情況下,兩者都可能適用,但是在重新發明輪子(為了滿足需求,重新開發已經存在的功能或函數)之前分析已有的技術仍然是有用處的。這可以節約時間、金錢,並提升開發者已有的知識水平。
廠商的鎖定
當軟件部分或整個依賴於特定廠商的時候會發生這種情況。J2EE優勢之一就是其輕便性,但是它仍然賦予了廠商提供豐富的擁有版權(所有權)特性的機會。的確,在開發過程中,這些特性可能是有幫助的,但是它們有時也有負面作用。其中出現的一個問題是控制權的丟失。你可能非常熟悉和喜歡六個月之前的某種特性。另一個問題是,當廠商作出改變(修改)的時候,可能會打亂你的軟件開發、降低協同工作的能力、強迫你持續升級等等。
解決這個問題的一種方法是在版權元素的上面提供一個隔離層。例如,Commons Logging就可以插入任何日志記錄框架組件(例如Log4J或Java Logging)之中。
J2EE特定的反模式 表2列舉了普通應用程序層的幾個反模式。
表2:普通的J2EE反模式。這個表列舉了J2EE應用程序的不同層次中出現的反模式。
層
反模式
持久層
撈網
窒息
JSP
對話數據太多嵌入的導航信息
Servlet
每個Servlet中都有公用函數訪問優良紋理的EJB接口
EJB
大的事務在JMS中過多的查詢
Web服務
假設SOA = Web服務
J2EE
硬編碼的JNDI查找沒有有效利用EJB容器
下面解釋表2中列舉的反模式。
持久層反模式
它出現在那些需要從數據庫檢索數據的應用程序所引起的一些有趣的挑戰中。
撈網(Dredge)
例如,我們來考慮一下淺度和深度的數據查詢。開發者一般喜歡在一個操作中執行昂貴的(深度)查找,載入的信息量超過了需要的信息,而不喜歡通過分部的(淺度)查找來檢索必要的數據以供顯示和生成報表。這種策略可以在某些情形中使用,但是如果我們沒有仔細地計劃,它很容易引起性能低下、占用大量內存的問題。
窒息(Stifle)
J2EE應用程序包含了幾個網絡資源之間的交互操作--數據庫就是其中一種。當應用程序與數據庫交互操作的時候,我們就應該調整它,只讓它消耗最合適宜的網絡帶寬,否則就可能導致伸縮性的問題。如果沒有優化數據庫通訊的網絡管道,軟件應用可能遇到瓶頸,甚至於會阻塞網絡本身。
JSP和Servlet反模式 JSP和Servlet開發的過程中也有一些反模式。
對話(Session)數據過量
對於開發者來說,把JSP對話作為通用的數據空間是很有誘惑力的。對話為我們存儲那些傳輸中的數據提供了一種簡單的機制。但是網站訪問量和相應的對話數據的增長可能引起崩潰。同時,粗心地使用對話可能導致鍵沖突,引發應用程序錯誤。更糟糕的情況是,可能出現跨越多個對話的共享數據錯誤(本來不應該共享的),可能導致敏感的用戶信息洩漏。
即使沒有這些技術問題,讓對話不斷"膨脹"也不是好的想法。對話數據應該限制在一定的范圍中,只用於存儲那些跨越多個用戶界面的工作流所必要的數據,當那些信息不再需要的時候,就應該被清除掉。
嵌入的導航信息
當開發者硬編碼(hardcode)或者向其它的JSP嵌入鏈接的時候可能會發生這種情況。如果頁面的名稱改變了,就必須搜索和改變其它頁面中的所有相關的鏈接。我們必須在開始的時候就慎重地考慮導航方案,才能避免在後面的維護階段出現"夢魇"。
每個Servlet中都有公用函數(功能)
在應用程序的多個servlet中經常出現硬編碼函數(功能)會使應用程序難於維護。作為代替,開發者應該刪除servlet中重復的代碼。請使用一個框架組件(例如Struts)提供簡單的、在XML文件中指定工作流程的途徑(我們可以更改這個文件而不需要改變servlet代碼)。還有其它一些工具,例如前台管理員(Front Controller)和過濾器也可以在一定的程度上移除硬編碼。
訪問優良紋理的(Fine-grained)EJB接口
在通過網絡進行遠程調用的時候,數據的列集(marshaling)和散列(unmarshalling)操作可能引發應用程序中主要的時間延遲。多重遠程調用也可能引發延遲。根據應用程序的不同需求,其解決方案可能包括重新設計EJB,但是如果這不是問題的起因,servlet可能無法通過優良紋理的API來訪問EJB實體。如果該EJB同時暴露了優良紋理的和粗糙紋理的API,那麼servlet就會用單個的粗糙紋理API調用來代替優良紋理方法的多個調用。
EJB反模式
這些反模式涉及一些企業級的方面,例如事務和消息隊列。
大型事務
那些包含了調用多個資源的復雜處理的事務可能會把其它一些等待相同資源的線程鎖定一段時間。這對性能可能產生重要的影響。一般情況下,把這些事務分解成較小的片斷(依賴於應用程序的需求)、或者使用替代的方法(例如用存儲過程來替代長的數據庫處理)是一種謹慎的做法。
JMS中隊列過載(Overloading)
JMS同時提供了隊列和主題目的地。隊列可以作為不同類型的消息(例如二進制的地圖消息或基於文本的消息)的"家"。但是如果你利用這個特性,在同一個隊列中發送不同類型的消息,那麼區分消息就變成了使用者的責任了。更好的解決方案是分開發送。你可以編程或者使用不同的目的地來實現這樣的方案(這依賴於不同的應用程序需求)。
Web服務反模式 隨著Web服務的增長,某些特定的反模式將變得很突出。
認為SOA=Web服務
面向架構的服務(SOA)這個術語經常與Web服務混淆了,但是事實上SOA的概念比Web服務的概念出現得早。SOA是為了實現組件之間的松散耦合。它提供一個軟件服務,供另一個軟件服務使用。但是後來,SOA與Web服務的含義變得相同了。我們要記得把SOA作為其它服務技術的基礎是完全可行的。
其它的反模式 還有其它一些反模式不好分類,但是仍然值得我們注意。
硬編碼的JNDI查找
Java命名和目錄接口為我們提供了一條查詢對象信息(例如EJB接口的位置)的便捷途徑。但是JNDI查找操作一般是很昂貴的,特別是在重復執行的情況下。但是如果你緩沖了其結果,就可以獲取顯著的性能改善。其中一種實現方案就是使用單態(singleton)類。
但是網絡不是靜態的。隨著網絡的改變,查詢也可能改變。這意味著對於網絡的每種變化,程序員必須重新訪問原應用程序代碼,做出必要的修改,並重新編譯/部署代碼。其替代辦法是,通過XML配置文件來提供可配置的查找,這樣就可以使開發者在每次遇到網絡發生改變的時候,不必重新編譯代碼。
沒有充分利用EJB容器的特性
在我們開放企業級組件的時候,可以從兩種辦法中選擇--使用容器提供的服務或者編寫自己的服務。盡管EJB遇到了大量的替代產品(例如Spring),但是它仍然被廣泛地被用於與這些框架組件協同工作。在我們使用EJB的時候,你應該試圖利用容器的服務,例如群集、負載均衡、安全性、事務管理、容錯和數據存儲。如果你沒有充分地利用容器的豐富特性,最終可能導致"重新發明輪子"(這是本文前面提到的另一種反模式)。
我相信你已經認識到反模式與設計模式的重要性相當。即使你還沒有明白本文描述的某些反模式的名稱,你也應該能夠記住它們的特性和可能引起的問題。對這些反模式進行分類和命名所帶來的好處與設計模式的分類和命名是一樣的;這樣做可以為軟件經理、架構師、設計者和程序員提供一個通用的"詞典",幫助他們認識未來的錯誤和維護麻煩可能的根源。