程式者的胡言亂語

pageicon 星期二 六月 24, 2008

漫談程式碼的相依性 - 1

在前文中我們時常會提到程式碼間的相依性。在程式設計上,我們通常都會盡量降低程式碼之間的相依性。程式碼間的高相依性會帶來許多負面的效應,其中最令程式設計者頭痛的,莫過於程式碼變動所造成的衝擊。當兩個程式碼元素之間存在相依性時,當被依賴的那一個元素有了變化時,就有機會造成另一個元素也受到波及,因而必須跟著一同變化。所以你可以想像,當你修改某個被許多其他的程式碼元素高度依賴的程式碼元素時,可能會影響到的層面究竟會有多廣泛。這是我們之所以要控制程式碼相依性的主要原因之一。

所謂的程式碼元素之間的相依性,其實還是可以在不同的粒度(granularity)上去檢視。例如,程式庫和程式庫之間的相依性、packagepackage之間的相依性、類別和類別之間的相依性、等等。但無論放在那一種粒度的層級上看,高相依性都不會是一件好事。例如大名鼎鼎的Jakarta Apache Project,其中的許多程式庫都和同一專案下的許多程式庫存在著相依性,這意謂著採用其中一者,也得同時引入其他,這種高度耦合的情況,讓使用這些專案的開發者,被迫面對相當複雜的組態管理問題。

對一般程式設計者來說,多半比較關心package間的相依性以及類別之間的相依性,尤其是後者,更是許多設計技巧與方法關注的重心。而本文,也將以類別之間的相依性為主。

談了這麼久的相依性,究竟什麼樣的情況,我們會認為兩個類別之間存在「相依性」呢?當類別A相依於類別B時,意指當我們改變B的介面時,我們可能得修改A。也就是說,所謂的相依性,其實是對另一類別介面的依賴。從這個定義來判斷,下述的情況都是A相依於B的例子,像是(1A存取B的值(2A呼叫B的函式(3A外貌式(signature)中的回傳型別或引數列表中含有B

如果你曾使用UML來從設計的建模工作,那麼你對其中的相依性關係(dependency relationship)肯定不會陌生。不過,就「相依性」本身來看,我們在評估兩個類別之間是否存在相依性時,其中的「相依性」其涵蓋範圍就不僅僅侷限於UML中所提及相依性關係,事實上它包含了更多的關係。

舉例來說,若類別A繼承自類別B,那麼當父類別B的介面有所變更時,子類別A難道沒有可能需要修改嗎?當然需要 依據本文對相依性的定義,類別A與類別B之間存在相依性,而這相依性則是基於繼承關係而來的。

在明白我們想要探討的相依性後,讀者不妨暗自想想,自己在進行設計的時候,是否曾經將設計的相依性高低與否納入考量呢?是否會在設計時,試著降低類別與類別之間的相依性呢?

有許多工具可以協助你分析你的系統中各類別之間的相依性。例如,我所使用的一個工具能產生以下的相依圖(dependency graph):


所謂的相依圖,即將你系統中存在相依性的類別之間以帶有箭號的線條相連接。相依性是單向關係,也就是說類別A相依於類別B(此時箭號指向B),不意謂著類別B也同樣相依著類別A。以上圖為例,DBFacade依賴SQLManagerSQLExecutorQueryResult

從相依圖中可以觀察出現有程式碼在設計上的一些問題。例如,當某個類別被許許多多的箭頭所指向時,意謂著它是被高度依賴的類別。也就是說,當這個類別有所變動時,可能會影響到的類別數量就會很多。換一個角度來看,有著一個高度依賴其他類別的類別,同樣也不是一件好事。這意謂著這個類別有可能會被頻繁的更動,因為只要任一個它所依賴的類別有了變化,它都有機會被波及,因而需要修改。此外,像循環相依(circular dependency)也是另一個常見的設計問題。

使用相依圖,對於分析既有系統的某些設計問題,能夠發揮一些作用。不過,儘管如此,此類的工具多半屬於靜態分析的形式,也就是說,不論是在原始碼層級或位元碼層級上分析,這樣的工具都只能分析靜態的程式碼,而無法捕捉到系統於執行期的行為。以像Java這種支援reflection(映射)的程式語言,當某個類別運用reflection來和其他的類別發生關係時,此種工具便無法分析出它們之間存在的相依性。大量利用reflection的系統仍然是少數,而且懂得運用reflection機制的程式員,也多半會留意相依性的議題,所以並不構成此類工具的重大缺陷。

當你在設計類別以及類別所具備的介面時,必須時時刻刻把類別間的相依性放在心裡。當你安置類別與類別間的關係時,你可能會想到:「唔,這麼做的話,豈不是讓那些類別都和這個類別有了相依性了嗎?」因而促使你思考如何降低自己設計中類別間的相依性,進而持續的改善自己的設計。許多導因自相依性的設計問題,多半都是由於設計者不在意、或是不知道應該要留意設計中類別的相依性而引起的。

在有名的GoFDesign Patterns一書中提到設計時的一個重要原則:「針對介面來撰寫程式,而不要針對實作(Program to interface, not an implementation)」。這中間的原因,如果從相依性的角度來理解,就十分的清楚了。倘若你的客戶端程式相依五個實作的類別,那麼它在相依圖上,就有著五條對外的連結。如果這五個實作的類別有著共通的介面,那麼你就有機會讓你的客戶端程式僅相依於該介面,因此大大的降低了相依性的程度。即便後端的實作類別有所變動,共通的介面也能扮演著緩衝的角色,有機會吸收掉實作類別造成的衝擊,也就不致於影響到客戶端程式。

從這邊你可以觀察到另一個特性 在相依圖上兩類別間若存在一條路徑,那麼當它們之間的距離愈遠,對其中一個類別所做的修改,會波及到另一個類別的機會就會愈低。兩類別之間間接的層級愈高,愈能抵抗改變的影響。不過另一方面,程式中若處處充滿著間接的設計,也會衍生出效率及可讀性的問題。類別間訊息傳遞路徑太長,自然沒有效率。而太高的間接層級也有可能造成閱讀的理解障礙。

有許多設計模式,都是把重點放在降低類別之間的相依性。例如有名的Façade模式,便是為了消除客戶端類別與許多位於同一子系統中類別的相依性而設計的。


圖引自Design Patterns, by Erich Gamma, Richard Helm, Ralph Johnson, John Vissides Addison-Wesley (October 1994)

Façade設計模式希望所有的客戶端程式碼對某個子系統的存取,一律從單一窗口進入,而不要直接存取其背後所有相關的實作。當客戶端程式碼只直接相依於Façade類別時,Façade類別就做為一道防火牆。當子系統實作或內部設計改變時,Façade便有機會吸收這些改變。倘若沒有Façade的存在,而放任客戶端程式碼直接存取構成子系統中的類別時,那麼子系統的變動,可能會同時造成多個子系統中的類別需要改變,同時因為客戶端程式碼建立了許多條連至各個類別上的連結,所以各個類別的改變,都有可能造成客戶端程式碼得同時做出許多的因應與調整。因此,運用Façade模式,可以有效的降低相依性,也降低客戶端程式碼在子系統有所變化時受到影響的機會。

Blogged with Flock

迴響:

發表迴響:
  • HTML 語法: 關閉
把對母乳媽媽的感謝與支持傳出去

« 九月 2010
星期日星期一星期二星期三星期四星期五星期六
   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  
       
今日

Search this blog

Links

Weblog menu

Today's referrers

Feeds