最早的設計質量的標誌之一就是耦合。它在最早的結構化設計中和內聚一起出現,並且從未消失過。我在考慮軟件設計時仍然總是想到它。有幾種方法描述耦合,不過它可以縮減成這樣:如果在一個程序中的一個模塊的變化需要另一個模塊的變化,那麼耦合存在了。這可能是兩個模塊在一點做相似的事情,因此在一個模塊中的代碼是另一個模塊中的代碼
的有影響的重複。這是一個代碼重複的最主要和明顯的罪惡的例子。重複總是意味着耦合,因爲一段重複代碼的改變意味着另一段代碼的改變。而且也很難找出重複代碼,因爲可能在兩段代碼間沒有顯而易見的關係
耦合也出現在當在一個模塊中的代碼使用了另外模塊的代碼,可能通過調用一個函數或存取一些數據。在這一點上,變得很清楚,不像重複代碼,你不能總是避免耦合。你能將一個程序分成許多模塊,而這些模塊需要用某種方式通信—否則,你只不過是有許多程序。耦合是需要的,因爲如果你禁止模塊之間的耦合,你只有將所有的東西放一個大模塊中。那麼,
將有大量的耦合—只藏在地毯下。
因此耦合是我們需要控制的東東,不過怎樣控制呢?我們需要任何地方都擔心有耦合嗎,或是不是在一些地方有比另外一些地方重要呢?哪個因素使耦合不好,哪些是可以允許的呢?
圖1:
看待依賴
我自己最關心最高層模塊的耦合。如果我們將一個系統分成一打
(或更少)大型模塊,這些模塊怎麼耦合呢?我關注粗粒度的模
塊,因爲擔心任何地方的耦合會令人困惑而不知所措。最大的問
題來自上層的未控制的耦合。我不擔心耦合在一起的模塊的數目,
但是我關注模塊之間依賴關係的樣式。我也發現圖(diagram)對
我們很有幫助。
當我使用依賴這個術語,我把它作爲uml中定義的那樣來使用。
因此如果UI(user interface)模塊中的任何代碼通過調用一個函數
使用一些數據,或使用了領域模塊中定義的類型的方式引用了領域
模型中的任何代碼,那麼UI模塊依賴於領域模塊。如果任何人改變
了領域模型,UI模型將會有需要改變的可能性。依賴是單向的:UI
模塊通常依賴於領域模塊,而不是其他情況。我們將會有第二個
依賴如果領域模塊也依賴於UI模塊。
UML依賴也可以是不傳遞的。如果UI模塊依賴於
領域模塊,並且領域模塊依賴於數據模塊,我們不能假設UI模塊
依賴於數據庫模塊。如果確實是依賴的,我們必須用一個額外的在
UI和數據庫模塊的直接的依賴。這個非傳遞性很重要因爲它讓我們
圖1a顯示了我怎麼用UML符號畫這種情況。UML是爲OO系統設計的,
不過基本模塊的符號和依賴適用於大多數軟件的風格。這種高層
模塊的UML名字叫包(package),因此我從現在起將用這個術語(因此
UML警察不會逮捕我!:P)因爲這些是包,我叫這種圖包圖(然而在UML
中嚴格的叫類圖)。
我這裏描述的是分層結構,對任何從事信息系統的人來說都是很熟悉的。
一個信息系統中的層爲我們描述在考慮依賴時必須的事情提供了很好的
素材。對於依賴結構的最通常的建議是避免循環。循環是帶來問題的,
因爲它們指出你會得到一個每個變化導致其他變化,而這些變化又回到了
初始的包中。這樣的系統更難理解因爲你只有重複循環很多次。我不
把避免包之間的循環作爲一個嚴格的定律。如果它們是局部化的,我將
會容忍它們。在一個應用程序的同一層的兩個包之間的循環的問題更小。
圖2:
一個映射器(mapper)包
在圖1a中,所有的依賴都是一個方向上的。這是一個控制得很好的依賴的集合的標誌,但不是需求。圖1b表現了另一個信息系統的通常的特徵,當一個映射器包把領域模型從數據庫分開。(一個映射器是提供雙向絕緣的包。)映射器包提供雙向絕緣,讓領域模型和數據庫分別獨立的變化。作爲結果,你能常常在更復雜的OO模型中發現這種樣式。
當然,如果你想想當你存取數據時發生了什麼,你會發現這張圖片不是很正確。如果領域模型中的一個模塊需要從數據庫得到一些數據,它怎麼得到呢?它不能請求映射器,因爲如果它可以,將導致從領域模型到映射器的一個依賴,從而導致循環。爲了解決這個問題,我需要不同種類的依賴。
到現在爲止,我已經討論了一段代碼使用其它代碼的的依賴。不過有另一種依賴----接口和它的實現的關係。實現依賴於接口,反之不成立。特別是任何一個接口的調用者只依賴於接口,即使是一個分離的模塊實現了它。
圖2描述了這個想法。領域模型依賴於接口,而不是實現。領域模型離開了
一些映射器實現不能工作,不過只有接口裏面的變化會導致領域模型的變化。
在這種情況下,有一些分離的包,不過不是必需的。圖3表現了領域模型中
的一個存儲包,由映射器中的一個存儲實現來實現。在這種情況下,領域模型爲映射器定義接口。簡而言之就是領域包可以和任何被選擇去實現存儲接口的映射器工作。
定義一個分離的用於實現的一個模塊的另一個模塊中的接口是一個基本的
打破依賴和減小耦合的方法。這種實踐有很多形式,最基本的是回調(call
back)在這種形勢下,一個調用者被請求用一個特定的記號爲一個函數提供
一個引用,這個記號過一會會被調用。在java世界中的一個通常的例子是
listener。因爲listener是類,它們更淺白易懂,使情況更清楚明瞭。
另一個例子是一個模塊定義了它們傳出、別人可以反應的事件。你可以考
慮把事件作爲監聽模塊通常遵守的接口。回調函數的調用者,監聽模塊的
定義者,和事件的製造者不知道哪個模塊實際上被調用,因此這裏沒有
依賴。
圖3:
我覺得缺少一個結尾,因爲我所說的包含如“控制的好的依賴”這樣
的晦澀的詞。我很難提供試圖去定義一個控制得好的依賴的集合的
好的指南。當然,是關於減少耦合的量,不過這不是全部的事情。
依賴的方向和他們指向的方式,如避免循環,也很重要。並且,我對
待所有的依賴都一樣,不考慮接口的寬度。有依賴比擔心你依賴於什
麼更重要。
我所遵守的基本定律是發現我的高層依賴並弄清楚它們,分離接口與
實現來打破我不希望的依賴。像很多設計的經驗的研究一樣,這看上
去很不完善。不過我已經覺得很有幫助了----最後,我要說的結束了。