隨著應用程式的規模和複雜度的增加,需要在更高的層次對它們進行組織。類對於小型應用程式來說事非常方便的組織單元,但是對於大型應用程式來 說,如果僅僅使用類作為唯一的組織單元,就會顯得粒度過細。因此,就需要比類“大”的“東西”來輔助大型應用程式的組織。這個“東西”就是包 (package)。
本節描述了6個原則。前3個原則關注包的內聚性,這些原則能夠指導我們如何把類劃分到包中。後3個原則關注包的耦合性,這些原則協助我們確定包之間的相互關係。
在UML的概念中,包可以用作包容一組類的容器。通過把類組織成包,我們可以在更高層次的抽象上來理解設計。我們也可以通過包來管理軟體的開發和發布。目的就是根據一些原則對應用程式中的類進行劃分,然後把這些劃分後的類分配到包中。
但是類經常會和其他類之間存在著依賴關係,這些依賴關係還經常會跨越包的邊界。因此,包之間也會產生依賴關係。包之間的依賴關係展現了應用程式的高層組織圖。我們應該對這些關係進行管理。
1 粒度:包的內聚性原則
這裡要講述的3個關於包的內聚性原則,可以協助開發人員決定如何把類劃分到包中。這些原則依賴與這樣的事實:至少已經存在一些類,並且它們之間的相互關係也已經確定。因此,這些原則是根據“自底向上”的觀點對類進行劃分的。
1.1 重用發布等價原則(REP)
重用的粒度就是發布的粒度:一個包中的軟體要麼都是可重用的,要麼都是不可重用的。
當你重用一個類庫時,對這個類庫的作者有什麼期望呢?你當然想得到好的文檔,可以工作的代碼,規格清晰的介面等。但是,你還會有其他的期望。
首先,你希望代碼的作者能保證為你維護這些代碼,只有這樣才值得你在重用這些代碼上花費時間。畢竟,如果需要你親自去維護這些代碼,那將會花費你大量的時間,這些時間也許可以自己用來設計一個小些但是好些的包。
其次,你希望代碼的作者在計劃對代碼的介面和功能進行任何改變時,提前通知你一下。但是僅僅通知一下是不夠的。代碼的作者必須尊重你拒絕使用任何新版本 的權力。否則,當你處在開發進度中的一個關鍵時刻時,他可能發布了一個新的版本。或者他對代碼進行了改變,之後就乾脆再也無法與你的系統相容了。
無論在哪種情況下,如果你決定不接納新版本,作者必須保證對於你所使用的舊版本繼續提供一段時間的支援。這段時間也許只有3個月,或者長達1年,你們兩 個人之間必須就這些事情進行磋商。但是,他不能和你斷絕關係並且拒絕對你提供支援。如果他不同意對你使用的稍舊一點的版本提供支援,那麼你就應該認真的考 慮一下是否情願忍受對方反覆無常的變化,而繼續使用他的代碼。
這個問題主要是行政問題。如果有其他的人將要重用代碼,就必須要進行行政和支援方面的工作。但是這些行政上的問題對於軟體包的結構具有深刻的影響。為了給重用者提供所需的保證,代碼的作者必須把它們的軟體組織到一個可重用的包中,並且通過版本號碼對這些包進行跟蹤。
REP指出,一個包的重用粒度(granule of reuse)可以和發布粒度(granule of release)一樣大。我們說重用的任何東西都必須同時被發布和跟蹤。簡單的編寫一個類,然後聲稱它是可重用的做法是不現實的。只有在建立一個跟蹤系 統,為潛在的使用者提供所需的變更通知、安全性以及支援後,重用才有可能。
REP帶給了我們關於如何把設計劃分到包中的第一個提示。由於重用性必須是基於包的,所以可重用的包必須包含可重用的類。因此,至少,某些包應該有一組可重用的類組成。
行政上的約束力將會影響到對軟體的劃分,這看上去會令人不安,但是軟體不是一個可以依據純數學規則群組織起來的純數學實體。軟體是一個人的智力活動的產品。軟體由人建立並被人使用,並且如果我們將要對軟體進行重用,那麼它肯定以一種人認為方便重用的方式進行劃分。
那麼,關於包的內部結構方面,我們學到了什麼呢?我們必須從潛在的重用者的角度去考慮包的內容。如果一個包中的軟體是用來重用的,那麼它就不能再包含不是為了重用的目的而設計的代碼。一個包中的代碼要麼都是可重用的,要麼都是不可重用的。
可重用性不是唯一的標準,我們也要考慮重用這些軟體的人。當然,一個容器類庫是可重用的,一個金融方面的架構也是可重用的。但是,我們不希望把它們放進 同一個包中。很多希望重用容器類庫的人可能對於金融架構根本不感興趣。因此,我們希望一個包中的所有類對於同一類使用者來說都是可重用的。我們不希望一個用 戶發現包中所包含的類中,一些是他所需要的,另一些對他卻完全沒用。
1.2 共同重用原則(CRP)
一個包中所有類應該是共同重用的。如果重用了包中的一個類,那麼就重用包中的所有類。
這個原則可以協助我們決定哪些類應該放在同一個包中。它規定了趨向共同重用的類應該屬於同一個包。
類很少會孤立地重用。一般來說,可重用的類需要與作為該可重用抽象一部分的其他類協作。CRP規定了這些類應該屬於同一個包。在這樣的一個包中,我們會看到類之間有很多的互相依賴。
一個簡單的例子是容器類以及與它關聯的迭代器類。這些類彼此之間緊密耦合在一起,因此必須共同重用。所以它們應該在同一個包中
但是,CRP告訴我們的不僅僅是什麼類應該共同放入一個包中。它還告訴我們什麼類不應該放入同一個包中。當一個包使用了另一個包時,它們之間會存在一個 依賴關係。也許一個包僅僅使用了另外一個包中的一個類。然而,那根本不會削弱這兩個包之間的依賴關係。使用者包依然依賴於被使用的包。每當被使用的包發布 時,使用者包必須進行重新驗證和重新發布。即使發布的原因僅僅改變了一個使用者包根本不關心的類,也必須要這樣做。
此 外,包也經常以共用庫、DLL、JAR等物理表示的形式出現。如果被使用的包以JAR的形式發布,那麼使用這個包的代碼就依賴於整個JAR。對JAR的任 何修改——即使所修改的是與使用者代碼無關的類,也會造成這個JAR的一個新版本發布。這個新JAR也要重新發行,並且,使用這個JAR的代碼也要進行重新 驗證。
因此,我想確信當我依賴於一個包時,我將依賴於那個包中的每一個類。換句話說,我想確信我放入一個包中的所有類是不可分開的,僅僅依賴於其中一部分的情況是不可能的。否則,我將要進行不必要的重新驗證和重新發行,並且會白費相當數量的努力。
因此,CRP告訴我們更多的是,什麼類不應該放在一起。CRP規定相互之間沒有緊密聯絡的類不應該放在同一個包中。
1.3 共同封閉原則(CCP)
包中的所有類對於同一類性質的變化應該是共同封閉的。一個變化若對一個包產生影響,則將對包中的所有類產生影響,而對於其他的包不造成任何影響。
這是單一職責原則(SRP)對於包的重新規定。正如SRP規定的一個類不應該包含多個引起變化的原因那樣,這個原則規定了一個包不應該包含多個引起變化的原因。
在大多數的應用中,可維護性的重要性是超過可重用性的。如果一個應用中的代碼必須更改,那麼我們寧願更改都集中在一個包中,而不是分布在多個包中。如果 更改集中在一個單一的包中,那麼我們僅僅需要發布那一個更改了的包。不依賴於那個更改了的包的其他包則不需要重新驗證或重新發布。
CCP鼓勵我們把可能由於同樣的原因而改變的所有類共同聚集在同一個地方。如果兩個類之間有非常緊密的綁定關係,不管是物理上的還是概念上的,那麼它們總會一同進行變化,因而它們應該屬於同一個包中,這樣做會減少軟體的發布、重新驗證、重新發行的工作量。
這個原則和開放-封閉原則(OCP)密切相關。本原則中“封閉”這個詞和OCP中的具有同樣的含義。OCP規定了類對於修改應該是封閉的,對於擴充應該是開放的。但是,正如我們所學到的,100%的封閉是不可能的。應當進行有策略的封閉。我們所設計的系統應該對於我們經曆過的最常見的變化做到封閉。
CCP通過把對於一些確定的變化類型開放的類共同組織到同一個包中,從而增強了上述內容。因而,當需求中的一個變化到來時,那個變化就會很有可能被限制在最小數量的包中。
1.4 包內聚性總結
過去,我們對內聚性的認識要遠比上面3個原則所蘊含的簡單。我們習慣於認為內聚性只不過是指一個模組執行一項並且僅僅一項功能。然而,這3個關於包內聚性的 原則描述了有關內聚性的更豐富的變化。在選擇要共同組織到包中的類時,必須要考慮可重用性與可開發性(develop ability)之間的相反作用力。在這些作用力和應用的需要之間進行平衡不是一件簡單的工作。此外,這個平衡幾乎總是動態。也就是說,今天看起來合適 的劃分到了明年也許就不再合適了。因此,當項目的重心從可開發性向可重用性轉變時,包的組成很可能會變動並隨時間而演化。
2 穩定性:包的耦合性原則
接下來的3個原則用來處理包之間的關係。這裡,我們會再次碰到可開發性與邏輯設計之間的衝突力(tension)。來自技術和行政方面的作用力都會影響到包的組織圖,並且這種作用力還是易變的。
2.1 無環依賴原則(ADP)
在包的依賴圖中,不允許存在環。
考慮圖2.6.2.1-1中的包圖。圖中展示了組成一個應用程式的非常典型的包結構。相對於本例的意圖來說,該應用程式的功能並不重要。重要的是包的依 賴關係結構。請注意,該機構是一個有向圖(directed graph)。其中,包是結點(node),依賴關係是有向邊(directed edge)。
圖2.6.2.1-1 包結構是有向非循環圖
現在,請注意另外一件事情。無論從哪個包開始,都無法沿著依賴關係而迴繞到這個包。該結構中沒有環。它是一個有向非循環圖(DAG)。
當負責MyDialogs的團隊發布了該包的一個新版本時,會很容易找出受影響的包:只需逆著依賴關係指向尋找即可。因此,MyTasks和MyApplication都會受到影響。當前工作於這兩個包的開發人員就要決定何時應該和MyDialogs的新版本整合。
還要注意,當MyDialogs發布時,完全不會影響到系統中許多其他包。它們不知道MyDialogs,並且也不關心何時對MyDialogs進行了更改。這很好。這意味著發布MyDialogs的影響相對較小。
當工作於MyDialogs包的開發人員想要運行該包的測試時,只需把他們的MyDialogs版本和當前正使用的Windows包的版本一起編譯、鏈 接即可。不會涉及到系統中任何其他包。這很好。這意味著工作於MyDialogs的開發人員只需較少的工作即可建立一個測試,而且他們要考慮的變數 (variable)也不多。
在發布整個系統時,是自底向上進行的。首先編譯、測試已經發布Windows包。接著是 MessageWindow和MyDiaolgs。在它們之後是Tasks,然後是TaskWindow和Database。接著是MyTasks,最後 是MyApplication。這個過程非常清楚並且易於處理。我們知道如何去構建系統,因為我們理解系統各個部分的依賴關係。
2.1.1 包依賴關係圖中環造成的影響
如果一個新需求迫使我們更改MyDialogs中的一個類去使用MyApplication中的一個類。這就產生了一個依賴關係環,2.6.2.1.1-1所示:
圖2.6.2.1.1-1 具有依賴環的包圖
這個依賴環會導致一些直接後果。例如,工作於MyTasks包的開發人員知道,為了發布MyTasks包,它們必須得相容Task、 MyDialogs、Database以及Windows。然而,由於依賴關係的存在,它們現在必須也相容MyApplication、 TaskWindow、以及MessageWindow。也就是說,現在MyTasks依賴於系統中所有其他的包。這就致使MyTasks非常難以發布。 MyDialogs有著同樣的問題。事實上,該依賴關係會迫使MyApplication、MyTasks以及MyDialogs總是同時發布。它們實際 上已經變成了同一個大包。於是,在這些包上工作的所有開發人員,他們彼此之間的發布行動要完全一致,因為它們必須都要使用彼此間完全相同的版本。
這還只是部分問題。考慮一下在想要測試MyDialogs包時會發生什麼。我們必須要連結進系統中所有其他的包,包括Database包。這意味著僅僅為了測試MyDialogs就必須做一次完整的構建。這是不可忍受的。
如果想知道為何必須要連結進這麼多不同的庫,以及這麼多其他人的代碼,只需運行一個某個類的簡單單元測試即可,或許這是因為依賴關係圖中存在環的緣故。 這種環使得非常難以對模組進行隔離。單元測試和發布變得非常困難且易於出錯。而且,在C++中,編譯時間會隨模組的數目成幾何級數增長。
此外,如果依賴關係圖中存在環,就很難確定包構建的順序。事實上,也許就不存在恰當的順序。對於像Java一樣要從編譯過的二進位檔案中讀取它們的聲明的語言來說,這會導致一些非常討厭的問題。
2.1.2 解除依賴環
任何情況下,都可以解除包之間的依賴環並把依賴關係圖恢複為一個DAG。有兩個主要的方法:
(1)使用依賴倒置原則(DIP)。針對圖2.6.2.1.2-1這樣的情況,可以建立一個具有MyDialogs需要的介面的抽象基類。然後,把該抽 象基類放進MyDialogs中,並使MyApplication中的類從其繼承。這就倒置了MyDialogs和MyApplication間的依賴關 系,從而解除了依賴關係環。(參見:圖2.6.2.1.2-1)
圖2.6.2.1.2-1 使用依賴倒置解除依賴環
(2)新建立一個MyDialogs和MyApplication都依賴的包。把MyDialogs和MyApplication都依賴的類移到這個新包中。(參見:圖2.6.2.1.2-2)
圖2.6.2.1.2-2 使用新包解除依賴環
第2個解決方案意味著,在需求改變面前,包的結構是不穩定的。事實上,隨著應用程式的增長,包的依賴關係結構會抖動(jitter)和增長。因此,必須 始終要對依賴關係結構中環的情況進行監控。如果出現了環,就必須要使用某種方法把其解除。有時這意味著要建立新的包,致使依賴關係結構增長。
2.1.3 自頂向下設計
討論到現在,我們可以得出一個必然的結論:不能自頂向下設計包的結構。這意味著,包結構不是設計系統時首先考慮的事情之一。事實上,包結構應該時隨著系統的增長、變化而逐步演化的。
也許你會認為這是違反直覺的。我們已經認為像包這樣的大粒度分解同樣也是高層的功能分解。當我們看到一個像包依賴關係結構這樣的大粒度分組時,就會覺得包應該以某種方式描繪了系統的功能。然而,這可能不是包依賴關係圖的一個屬性。
事實上,包的依賴關係圖和描繪應用程式的功能之間幾乎沒有關係。相反,它們是應用程式可構建性的映射圖。這 就是為何不在項目開始時設計它們的原因。在項目開始時,沒有軟體可構建,因此也無需構建映射圖。但是,隨著實現和設計初期累積的類越來越多,對依賴關係進 行管理的需要就不斷增長。此外,我們也想儘可能地保持更改的局部化,所以我們開始關注SRP和CCP,並把可能會一同變化的類放在一起。
隨著應用程式的不斷增長,我們開始關注建立可重用的元素。於是,就開始使用CRP來指導包的組合。最後,當環出現時,就會使用ADP,從而,包的依賴關係圖會出現抖動以及增長。
如果在設計任何類之前試圖去設計包的依賴關係結構,那麼很可能會遭受慘敗。我們對於共同封閉還沒有多少瞭解,也還沒有覺察到任何可重用的元素,從而,幾乎當然會建立產生依賴環的包。所以,
2.2 穩定依賴原則(SDP)
朝著穩定的方向進行依賴。
設計不能是完全固定的。要使設計可維護,某種程度的易變性是必要的。我們通過遵循共同封閉原則(CCP)來達到這個目標。使用這個原則,可以建立對某些變化類型敏感的包。這些包被設計成可變的。我們期望它們的變化。
對於任何包而言,如果期望它是可變的,就不應該讓一個難以改變的包依賴於它!否則,可變的包同樣也會難以更改。
你設計了一個易於更改的包,其他人只要建立一個對它的依賴就可以使它變得難以更改,這就是軟體的反常特性。沒有改變你的模組中任何一行代碼,可是它突然之間就變得難以更改了。通過遵循SDP,我們可以確保那些打算易於更改的模組不會被那些比它們難以更改的模組所依賴。
2.2.1 並非所有的包都應該是穩定的
如果一個系統在所有的包都是最大程度穩定的,那麼該系統就是不能改變的。這不是所希望的情形。事實上,我們希望所設計出來的包結構中,一些包是不穩定的而另外一些是穩定的。圖2.6.2.2.1-1中展示了一個具有3個包的系統的理想配置。
圖2.6.2.2.1-1 理想的包配置
可改變的包位於頂部並依賴於底部穩定的包。把不穩定的包放在圖的頂部是一個有用的約定,因為任何一個向上的箭頭都意味著違反了SDP。
圖2.6.2.2.1-2展示了會違反SDP的做法。我們打算讓Flexible包易於更改。我們希望Flexible是不穩定的。然而,一些工作於包 Stable的開發人員,建立了一個對Flexible的依賴。這違反了SDP。結果,Flexible就不再易於更改了。對Flexible的更改會迫 使我們去處理該更改對Stable及其所有依賴者的影響。
圖2.6.2.2.1-2 違反了SDP
要修正這個問題,我們就必須要以某種方式解除Stable對Flexible的依賴。為什麼會存在這個依賴關係呢?我們假設Flexible中有一個類C被另一個Stable中的類U使用(參見:圖2.6.2.2.1-3)
圖2.6.2.2.1-3 糟糕依賴關係的原因
可以使用DIP來修正這個問題。我們建立一個介面類IU並把它放到包UInterface中。我們確保IU中聲明了U要使用的所有方法。接著,我們讓C 從這個介面繼承(參見:圖2.6.2.2.1-4)。這就解除了Stable對Flexible的依賴並促使這兩個包都依賴於UInterface。 UInterface非常穩定,而Flexible仍保持它必須的不穩定性。
圖2.6.2.2.1-4 使用DIP修正穩定性違規
2.2.2 在哪裡放置高層設計?
系統中的某些軟體不應該經常改變。該軟體代表著系統的高層構架和設計決策。我們希望這些構架決策是穩定的。因此,應該把封裝系統高層設計的軟體放進穩定的包中。不穩定的包中應該只包含那些很可能會改變的軟體。
然而如果把高層設計放進穩定的包中,那麼體現高層設計的原始碼就會難以更改。這會使設計變得不靈活。怎樣才能讓一個具有最高穩定性的包足夠靈活,可以經 受得住變化呢?在OCP中可以找到答案。OCP原則告訴我們,那些足夠靈活可以無需修改即可擴充的類是存在的,並且是所希望的。哪種類符合OCP原則呢? ——抽象類別。
1.1.2.3 穩定抽象原則(SAP)
包的抽象程度應該和其穩定程度一致。
該原則把包的穩定性和抽象性聯絡起來。它規定,一個穩定的包應該也是抽象的,這樣它的穩定性就不會使其無法擴充。另一方面,它規定,一個不穩定的包應該是具體的,因為它的不穩定性使得其內部具體代碼易於更改。
因此,如果一個包是穩定的,那麼它應該也要包含一些抽象類別,這樣就可以對它進行擴充。可擴充的穩定包是靈活的,並且不會過分限制設計。
SAP和SDP結合在一起形成了針對包的DIP原則。這樣說是準確的,因為SDP規定依賴應該朝著穩定的方向進行,而SAP則規定穩定性意味著抽象性。因此,依賴應該朝著抽象的方向進行。
然而,DIP是一個處理類的原則。類沒有灰階(the shades of grey)的概念。一個類要麼是抽象的,要麼不是。SDP和SAP的結合是處理包的,並且允許一個包是部分抽象、部分穩定的。
下一章:物件導向軟體設計原則(五) —— 應用樣本
CodeProject