程序員修煉之道:正交軟件架構方法

程序員修煉之道:正交軟件架構方法
責任編輯:李倩作者:ITPUB論壇 2009-02-20
【內容導航】
第1頁:什麼是正交性
第2頁:你可以將若干技術用於維持正交性
文本Tag: 系統架構 軟件架構
【IT168 技術文章】
如果你想要製作易於設計、構建、測試及擴展的系統,正交性是一個十分關鍵的概念,但是,正交性的概念很少被直接講授,而常常是你學習的各種其他方法和技術的隱含特性。這是一個錯誤。一旦你學會了直接應用正交性原則,你將發現,你製作的系統的質量立刻就得到了提高。

什麼是正交性


  “正交性”是從幾何學中借來的術語。如果兩條直線相交成直角,它們就是正交的,比如圖中的座標軸。用向量術語說,這兩條直線互不依賴。沿着某一條直線移動,你投影到另一條直線上的位置不變。
  在計算技術中,該術語用於表示某種不相依賴性或是解耦性。如果兩個或更多事物中的一個發生變化,不會影響其他事物,這些事物就是正交的。在設計良好的系統中,數據庫代碼與用戶界面是正交的:你可以改動界面,而不影響數據庫;更換數據庫,而不用改動界面。
  在我們考察正交系統的好處之前,讓我們先看一看非正交系統。
非正交系統
  你正乘坐直升機遊覽科羅拉多大峽谷,駕駛員——他顯然犯了一個錯誤,在吃魚,他的午餐——突然呻吟起來,暈了過去。幸運的是,他把你留在了離地面100英尺的地方。你推斷,升降杆控制總升力,所以輕輕將其壓低可以讓直升機平緩降向地面。然而,當你這樣做時,卻發現生活並非那麼簡單。直升機的鼻子向下,開始向左盤旋下降。突然間你發現,你駕駛的這個系統,所有的控制輸入都有次級效應。壓低左手的操作杆,你需要補償性地向後移動右手柄,並踩右踏板。但這些改變中的每一項都會再次影響所有其他的控制。突然間,你在用一個讓人難以置信的複雜系統玩雜耍,其中每一項改變都會影響所有其他的輸入。你的工作負擔異常巨大:你的手腳在不停地移動,試圖平衡所有交互影響的力量。
  直升機的各個控制器斷然不是正交的。
正交的好處
  如直升機的例子所闡明的,非正交系統的改變與控制更復雜是其固有的性質。當任何系統的各組件互相高度依賴時,就不再有局部修正(local fix)這樣的事情。
提示13

Eliminate Effects Between Unrelated Things
消除無關事物之間的影響

  我們想要設計自足(self-contained)的組件:獨立,具有單一、良好定義的目的(Yourdon和Constantine稱之爲內聚(cohesion)[YC86])。如果組件是相互隔離的,你就知道你能夠改變其中之一,而不用擔心其餘組件。只要你不改變組件的外部接口,你就可以放心:你不會造成波及整個系統的問題。
  如果你編寫正交的系統,你得到兩個主要好處:提高生產率與降低風險。
提高生產率
l 改動得以局部化,所以開發時間和測試時間得以降低。與編寫單個的大塊代碼相比,編寫多個相對較小的、自足的組件更爲容易。你可以設計、編寫簡單的組件,對其進行單元測試,然後把它們忘掉——當你增加新代碼時,無須不斷改動已有的代碼。
l 正交的途徑還能夠促進複用。如果組件具有明確而具體的、良好定義的責任,就可以用其最初的實現者未曾想象過的方式,把它們與新組件組合在一起。
l 如果你對正交的組件進行組合,生產率會有相當微妙的提高。假定某個組件做M件事情,而另一個組件做N件事情。如果它們是正交的,而你把它們組合在一起,結果就能做M x N件事情。但是,如果這兩個組件是非正交的,它們就會重疊,結果能做的事情就更少。通過組合正交的組件,你的每一份努力都能得到更多的功能。
降低風險
  正交的途徑能降低任何開發中固有的風險。
l 有問題的代碼區域被隔離開來。如果某個模塊有毛病,它不大可能把病症擴散到系統的其餘部分。要把它切掉,換成健康的新模塊也更容易。
l 所得系統更健壯。對特定區域做出小的改動與修正,你所導致的任何問題都將侷限在該區域中。
l 正交系統很可能能得到更好的測試,因爲設計測試、並針對其組件運行測試更容易。
l 你不會與特定的供應商、產品、或是平臺緊綁在一起,因爲與這些第三方組件的接口將被隔離在全部開發的較小部分中。
讓我們看一看在工作中應用正交原則的幾種方式。
項目團隊
  你是否注意到,有些項目團隊很有效率,每個人都知道要做什麼,並全力做出貢獻,而另一些團隊的成員卻老是在爭吵,而且好像無法避免互相妨礙?
  這常常是一個正交性問題。如果團隊的組織有許多重疊,各個成員就會對責任感到困惑。每一次改動都需要整個團隊開一次會,因爲他們中的任何一個人都可能受到影響。
  怎樣把團隊劃分爲責任得到了良好定義的小組,並使重疊降至最低呢?沒有簡單的答案。這部分地取決於項目本身,以及你對可能變動的區域的分析。這還取決於你可以得到的人員。我們的偏好是從使基礎設施與應用分離開始。每個主要的基礎設施組件(數據庫、通信接口、中間件層,等等)有自己的子團隊。如果應用功能的劃分顯而易見,那就照此劃分。然後我們考察我們現有的(或計劃有的)人員,並對分組進行相應的調整。
  你可以對項目團隊的正交性進行非正式的衡量。只要看一看,在討論每個所需改動時需要涉及多少人。人數越多,團隊的正交性就越差。顯然,正交的團隊效率也更高(儘管如此,我們也鼓勵子團隊不斷地相互交流)。
設計
  大多數開發者都熟知需要設計正交的系統,儘管他們可能會使用像模塊化、基於組件、或是分層這樣的術語描述該過程。系統應該由一組相互協作的模塊組成,每個模塊都實現不依賴於其他模塊的功能。有時,這些組件被組織爲多個層次,每層提供一級抽象。這種分層的途徑是設計正交系統的強大方式。因爲每層都只使用在其下面的層次提供的抽象,在改動底層實現、而又不影響其他代碼方面,你擁有極大的靈活性。分層也降低了模塊間依賴關係失控的風險。你將常常看到像下一頁的圖2.1這樣的圖表示的層次關係。
  對於正交設計,有一種簡單的測試方法。一旦設計好組件,問問你自己:如果我顯著地改變某個特定功能背後的需求,有多少模塊會受影響?在正交系統中,答案應

圖2.1 典型的層次圖
該是“一個”。移動GUI面板上的按鈕,不應該要求改動數據庫schema。增加語境敏感的幫助,也不應該改動記賬子系統。
  讓我們考慮一個用於監視和控制供暖設備的複雜系統。原來的需求要求提供圖形用戶界面,但後來需求被改爲要增加語音應答系統,用按鍵電話控制設備。在正交地設計的系統中,你只需要改變那些與用戶界面有關聯的模塊,讓它們對此加以處理:控制設備的底層邏輯保持不變。事實上,如果你仔細設計你的系統結構,你應該能夠用同一個底層代碼庫支持這兩種界面。157頁的“它只是視圖”將討論怎樣使用模型-視圖-控制器(MVC)範型編寫解耦的代碼,該範型在這裏的情況下也能很好地工作。
  還要問問你自己,你的設計在多大程度上解除了與現實世界中的的變化的耦合?你在把電話號碼當作顧客標識符嗎?如果電話公司重新分配了區號,會怎麼樣?不要依賴你無法控制的事物屬性。
工具箱與庫
  在你引入第三方工具箱和庫時,要注意保持系統的正交性。要明智地選擇技術。
  我們曾經參加過一個項目,在其中需要一段Java代碼,既運行在本地的服務器機器上,又運行在遠地的客戶機器上。要把類按這樣的方式分佈,可以選用RMI或CORBA。如果用RMI實現類的遠地訪問,對類中的遠地方法的每一次調用都可能會拋出異常;這意味着,一個幼稚的實現可能會要求我們,無論何時使用遠地類,都要對異常進行處理。在這裏,使用RMI顯然不是正交的:調用遠地類的代碼應該不用知道這些類的位置。另一種方法——使用CORBA——就沒有施加這樣的限制:我們可以編寫不知道我們類的位置的代碼。
  在引入某個工具箱時(甚或是來自你們團隊其他成員的庫),問問你自己,它是否會迫使你對代碼進行不必要的改動。如果對象持久模型(object persistence scheme)是透明的,那麼它就是正交的。如果它要求你以一種特殊的方式創建或訪問對象,那麼它就不是正交的。讓這樣的細節與代碼隔離具有額外的好處:它使得你在以後更容易更換供應商。
  Enterprise Java Beans(EJB)系統是正交性的一個有趣例子。在大多數面向事務的系統中,應用代碼必須描述每個事務的開始與結束。在EJB中,該信息是作爲元數據,在任何代碼之外,以聲明的方式表示的。同一應用代碼不用修改,就可以運行在不同的EJB事務環境中。這很可能是將來許多環境的模型。
  正交性的另一個有趣的變體是面向方面編程(Aspect-Oriented Programming,AOP),這是Xerox Parc的一個研究項目([KLM+97]與[URL 49])。AOP讓你在一個地方表達本來會分散在源碼各處的某種行爲。例如,日誌消息通常是在源碼各處、通過顯式地調用某個日誌函數生成的。通過AOP,你把日誌功能正交地實現到要進行日誌記錄的代碼中。使用AOP的Java版本,你可以通過編寫aspect、在進入類Fred的任何方法時寫日誌消息:
aspect Trace {
advise * Fred.*(..) {
static before {
Log.write("-> Entering " + thisJoinPoint.methodName);
}
}
}
  如果你把這個方面編織(weave)進你的代碼,就會生成追蹤消息。否則,你就不會看到任何消息。不管怎樣,你原來的源碼都沒有變化。


編碼
  每次你編寫代碼,都有降低應用正交性的風險。除非你不僅時刻監視你正在做的事情,也時刻監視應用的更大語境,否則,你就有可能無意中重複其他模塊的功能,或是兩次表示已有的知識。
  你可以將若干技術用於維持正交性:
l 讓你的代碼保持解耦。編寫“羞怯”的代碼——也就是不會沒有必要地向其他模塊暴露任何事情、也不依賴其他模塊的實現的模塊。試一試我們將在183頁的“解耦與得墨忒耳法則”中討論的得墨忒耳法則(Law of Demeter)[LH89]。如果你需要改變對象的狀態,讓這個對象替你去做。這樣,你的代碼就會保持與其他代碼的實現的隔離,並增加你保持正交的機會。
l 避免使用全局數據。每當你的代碼引用全局數據時,它都把自己與共享該數據的其他組件綁在了一起。即使你只想對全局數據進行讀取,也可能會帶來麻煩(例如,如果你突然需要把代碼改爲多線程的)。一般而言,如果你把所需的任何語境(context)顯式地傳入模塊,你的代碼就會更易於理解和維護。在面向對象應用中,語境常常作爲參數傳給對象的構造器。換句話說,你可以創建含有語境的結構,並傳遞指向這些結構的引用。
  《設計模式》[GHJV95]一書中的Singleton(單體)模式是確保特定類的對象只有一個實例的一種途徑。許多人把這些singleton對象用作某種全局變量(特別是在除此而外不支持全局概念的語言中,比如Java)。使用singleton要小心——它們可能造成不必要的關聯。
l 避免編寫相似的函數。你常常會遇到看起來全都很像的一組函數——它們也許在開始和結束處共享公共的代碼,中間的算法卻各有不同。重複的代碼是結構問題的一種症狀。要了解更好的實現,參見《設計模式》一書中的Strategy(策略)模式。
  養成不斷地批判對待自己的代碼的習慣。尋找任何重新進行組織、以改善其結構和正交性的機會。這個過程叫做重構(refactoring),它非常重要,所以我們專門寫了一節加以討論(見“重構”,184頁)
測試
  正交地設計和實現的系統也更易於測試,因爲系統的各組件間的交互是形式化的和有限的,更多的系統測試可以在單個的模塊級進行。這是好消息,因爲與集成測試(integration testing)相比,模塊級(或單元)測試要更容易規定和進行得多。事實上,我們建議讓每個模塊都擁有自己的、內建在代碼中的單元測試,並讓這些測試作爲常規構建過程的一部分自動運行(參見“易於測試的代碼”,189頁)。
  構建單元測試本身是對正交性的一項有趣測試。要構建和鏈接某個單元測試,都需要什麼?只是爲了編譯或鏈接某個測試,你是否就必須把系統其餘的很大一部分拽進來?如果是這樣,你已經發現了一個沒有很好地解除與系統其餘部分耦合的模塊。
  修正bug也是評估整個系統的正交性的好時候。當你遇到問題時,評估修正的局部化程度。
你是否只改動了一個模塊,或者改動分散在整個系統的各個地方?當你做出改動時,它修正了所有問題,還是又神祕地出現了其他問題?這是開始運用自動化的好機會。如果你使用了源碼控制系統(在閱讀了86頁的“源碼控制”之後,你會使用的),當你在測試之後、把代碼籤回(check the code back)時,標記所做的bug修正。隨後你可以運行月報,分析每個bug修正所影響的源文件數目的變化趨勢。
文檔
  也許會讓人驚訝,正交性也適用於文檔。其座標軸是內容和表現形式。對於真正正交的文檔,你應該能顯著地改變外觀,而不用改變內容。現代的字處理器提供了樣式表和宏,能夠對你有幫助(參見“全都是寫”,248頁)。
認同正交性
  正交性與27頁介紹的DRY原則緊密相關。運用DRY原則,你是在尋求使系統中的重複降至最小;運用正交性原則,你可降低系統的各組件間的相互依賴。這樣說也許有點笨拙,但如果你緊密結合DRY原則、運用正交性原則,你將會發現你開發的系統會變得更爲靈活、更易於理解、並且更易於調試、測試和維護。
  如果你參加了一個項目,大家都在不顧一切地做出改動,而每一處改動似乎都會造成別的東西出錯,回想一下直升機的噩夢。項目很可能沒有進行正交的設計和編碼。是重構的時候了。
  另外,如果你是直升機駕駛員,不要吃魚……
工具箱與庫
  在你引入第三方工具箱和庫時,要注意保持系統的正交性。要明智地選擇技術。
  我們曾經參加過一個項目,在其中需要一段Java代碼,既運行在本地的服務器機器上,又運行在遠地的客戶機器上。要把類按這樣的方式分佈,可以選用RMI或CORBA。如果用RMI實現類的遠地訪問,對類中的遠地方法的每一次調用都可能會拋出異常;這意味着,一個幼稚的實現可能會要求我們,無論何時使用遠地類,都要對異常進行處理。在這裏,使用RMI顯然不是正交的:調用遠地類的代碼應該不用知道這些類的位置。另一種方法——使用CORBA——就沒有施加這樣的限制:我們可以編寫不知道我們類的位置的代碼。
  在引入某個工具箱時(甚或是來自你們團隊其他成員的庫),問問你自己,它是否會迫使你對代碼進行不必要的改動。如果對象持久模型(object persistence scheme)是透明的,那麼它就是正交的。如果它要求你以一種特殊的方式創建或訪問對象,那麼它就不是正交的。讓這樣的細節與代碼隔離具有額外的好處:它使得你在以後更容易更換供應商。
  Enterprise Java Beans(EJB)系統是正交性的一個有趣例子。在大多數面向事務的系統中,應用代碼必須描述每個事務的開始與結束。在EJB中,該信息是作爲元數據,在任何代碼之外,以聲明的方式表示的。同一應用代碼不用修改,就可以運行在不同的EJB事務環境中。這很可能是將來許多環境的模型。
  正交性的另一個有趣的變體是面向方面編程(Aspect-Oriented Programming,AOP),這是Xerox Parc的一個研究項目([KLM+97]與[URL 49])。AOP讓你在一個地方表達本來會分散在源碼各處的某種行爲。例如,日誌消息通常是在源碼各處、通過顯式地調用某個日誌函數生成的。通過AOP,你把日誌功能正交地實現到要進行日誌記錄的代碼中。使用AOP的Java版本,你可以通過編寫aspect、在進入類Fred的任何方法時寫日誌消息:
aspect Trace {
advise * Fred.*(..) {
static before {
Log.write("-> Entering " + thisJoinPoint.methodName);
}
}
}

  如果你把這個方面編織(weave)進你的代碼,就會生成追蹤消息。否則,你就不會看到任何消息。不管怎樣,你原來的源碼都沒有變化。

編碼
  每次你編寫代碼,都有降低應用正交性的風險。除非你不僅時刻監視你正在做的事情,也時刻監視應用的更大語境,否則,你就有可能無意中重複其他模塊的功能,或是兩次表示已有的知識。
  你可以將若干技術用於維持正交性:

l 讓你的代碼保持解耦。編寫“羞怯”的代碼——也就是不會沒有必要地向其他模塊暴露任何事情、也不依賴其他模塊的實現的模塊。試一試我們將在183頁的“解耦與得墨忒耳法則”中討論的得墨忒耳法則(Law of Demeter)[LH89]。如果你需要改變對象的狀態,讓這個對象替你去做。這樣,你的代碼就會保持與其他代碼的實現的隔離,並增加你保持正交的機會。
l 避免使用全局數據。每當你的代碼引用全局數據時,它都把自己與共享該數據的其他組件綁在了一起。即使你只想對全局數據進行讀取,也可能會帶來麻煩(例如,如果你突然需要把代碼改爲多線程的)。一般而言,如果你把所需的任何語境(context)顯式地傳入模塊,你的代碼就會更易於理解和維護。在面向對象應用中,語境常常作爲參數傳給對象的構造器。換句話說,你可以創建含有語境的結構,並傳遞指向這些結構的引用。
  《設計模式》[GHJV95]一書中的Singleton(單體)模式是確保特定類的對象只有一個實例的一種途徑。許多人把這些singleton對象用作某種全局變量(特別是在除此而外不支持全局概念的語言中,比如Java)。使用singleton要小心——它們可能造成不必要的關聯。
l 避免編寫相似的函數。你常常會遇到看起來全都很像的一組函數——它們也許在開始和結束處共享公共的代碼,中間的算法卻各有不同。重複的代碼是結構問題的一種症狀。要了解更好的實現,參見《設計模式》一書中的Strategy(策略)模式。
  養成不斷地批判對待自己的代碼的習慣。尋找任何重新進行組織、以改善其結構和正交性的機會。這個過程叫做重構(refactoring),它非常重要,所以我們專門寫了一節加以討論(見“重構”,184頁)

測試
  正交地設計和實現的系統也更易於測試,因爲系統的各組件間的交互是形式化的和有限的,更多的系統測試可以在單個的模塊級進行。這是好消息,因爲與集成測試(integration testing)相比,模塊級(或單元)測試要更容易規定和進行得多。事實上,我們建議讓每個模塊都擁有自己的、內建在代碼中的單元測試,並讓這些測試作爲常規構建過程的一部分自動運行(參見“易於測試的代碼”,189頁)。
  構建單元測試本身是對正交性的一項有趣測試。要構建和鏈接某個單元測試,都需要什麼?只是爲了編譯或鏈接某個測試,你是否就必須把系統其餘的很大一部分拽進來?如果是這樣,你已經發現了一個沒有很好地解除與系統其餘部分耦合的模塊。
  修正bug也是評估整個系統的正交性的好時候。當你遇到問題時,評估修正的局部化程度。
你是否只改動了一個模塊,或者改動分散在整個系統的各個地方?當你做出改動時,它修正了所有問題,還是又神祕地出現了其他問題?這是開始運用自動化的好機會。如果你使用了源碼控制系統(在閱讀了86頁的“源碼控制”之後,你會使用的),當你在測試之後、把代碼籤回(check the code back)時,標記所做的bug修正。隨後你可以運行月報,分析每個bug修正所影響的源文件數目的變化趨勢。
文檔
  也許會讓人驚訝,正交性也適用於文檔。其座標軸是內容和表現形式。對於真正正交的文檔,你應該能顯著地改變外觀,而不用改變內容。現代的字處理器提供了樣式表和宏,能夠對你有幫助(參見“全都是寫”,248頁)。
認同正交性
  正交性與27頁介紹的DRY原則緊密相關。運用DRY原則,你是在尋求使系統中的重複降至最小;運用正交性原則,你可降低系統的各組件間的相互依賴。這樣說也許有點笨拙,但如果你緊密結合DRY原則、運用正交性原則,你將會發現你開發的系統會變得更爲靈活、更易於理解、並且更易於調試、測試和維護。
  如果你參加了一個項目,大家都在不顧一切地做出改動,而每一處改動似乎都會造成別的東西出錯,回想一下直升機的噩夢。項目很可能沒有進行正交的設計和編碼。是重構的時候了。
  另外,如果你是直升機駕駛員,不要吃魚……
相關內容:
l 重複的危害,26頁
l 源碼控制,86頁
l 按合約設計,109頁
l 解耦與得墨忒耳法則,138頁
l 元程序設計,144頁
l 它只是視圖,157頁
l 重構,184頁
l 易於測試的代碼,189頁
l 邪惡的嚮導,198頁
l 注重實效的團隊,224頁
l 全都是寫,248頁
挑戰
l 考慮常在Windows系統上見到的面向GUI的大型工具和在shell提示下使用的短小、但卻可以組合的命令行實用工具。哪一種更爲正交,爲什麼?如果正好按其設計用途加以應用,哪一種更易於使用?哪一種更易於與其他工具組合、以滿足新的要求?
l C++支持多重繼承,而Java允許類實現多重接口。使用這些設施對正交性有何影響?使用多重繼承與使用多重接口的影響是否有不同?使用委託(delegation)與使用繼承之間是否有不同?
練習
1. 你在編寫一個叫做Split的類,其用途是把輸入行拆分爲字段。下面的兩個Java類的型構(signature)中,哪一個是更爲正交的設計?  (解答在279頁)

class Split1 {
public Split1(InputStreamReader rdr) { ...
public void readNextLine() throws IOException { ...
public int numFields() { ...
public String getField(int fieldNo) { ...
}
class Split2 {
public Split2(String line) { ...
public int numFields() { ...
public String getField(int fieldNo) { ...
}
2. 非模態對話框或模態對話框,哪一個能帶來更爲正交的設計? (解答在279頁)
3. 過程語言與對象技術的情況又如何?哪一種能產生更爲正交的系統? (解答在280頁)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章