這一條款實際應該取這個名字:“應該建立大小合理而且包含少量公用類型的程式集”。但這太沉長了,所以就以我認為最常見的錯誤來命名:開發人員總是把所有的東西,除了廚房裡水溝以外(譯註:誇張說法,kitchen sink可能是個口語詞,沒能查到是什麼意思,所以就直譯了。),都放到一個程式集。這不利於重用其中的組件,也不利於系統中小部份的更新。很多以二進位組件形式存在的小程式集可以讓這些都變得簡單。
然而這個標題對於程式集的內聚來說也很醒目的。程式集的內聚性是指概念單元到單個組件的職責程度。彙總組件可以簡單的用一句話概括,你可以從很多.Net的FCL程式集中看到這些。有兩個簡單的例子:System.Collections程式集就是負責為相關對象的有序集提供資料結構,而System.Windows.Forms程式集則提供Windows控制項類的模型。Web form和Windows Form在不同的程式集中,因為它們不相關。你應該用同樣的方式,用簡單的一句話來描述你的程式集。不要玩花樣:一個MyApplication程式集提供了你想要的一切內容。是的,這也是簡單的一句,但這也太刁懶了吧,而且你很可能在My2ndApplication(我想你很可能會要重用到其中的一些內容。這裡“其中的一些內容”應該放到一個獨立的程式集中。)程式集並不須要使用所有的功能。
你不應該只用一個公用類來建立一個程式程式集。應該有一個折衷的方法,如果你太偏激,建立了太多的程式集,你就失去了使用封裝的一些好處:首先就是你失去了使用內部類型的機會,內部類型是在一個程式集中與封裝(打包)無關的公用類(參見原則33)(譯註:簡單的說,內部類型就是只能在一個公用的程式集中訪問類,程式集以外限制訪問)。JIT編譯器可以在一個程式集內有很的內聯效率,這比起在多程式集中穿梭效率要高得多。這就是說,在一個程式集中放置一些相關的類型對你是有好處的。我們的目標就是為我們的組件建立大小最合適的程式集。這一目標很容易實現,就是一個組件應該只有一個職責。
在某些情況下,一個程式集就是類的二進位表現形式,我們用類來封裝演算法和儲存資料。只有公用的介面才能成為“官方”的合約,也就是只有公用介面才能被使用者訪問。同樣,程式集為相關類提供二進位的包,在這個程式集以外,只有公用和受保護的類是可見的。工具類可以是程式集的內部類。確實,它們對於私人的嵌套類來說它們應該具有更更寬的存取範圍,但你有一個機制可以共用組件內部通用的實現,而不用暴露這個實現給所有的使用者。那就是封裝相關類,然後從程式集中分離成多個程式。
其實,使用多程式集可以讓很多不同布署選項變得很簡單。考慮一個三層應用程式,一部份程式以智能用戶端的形式在運行,而另一部份則是在伺服器上運行。你在用戶端上提供了一些驗證原則,用於確保使用者反饋的資料輸入和修改是正確的。而在伺服器上你又要重複這些原則,而且複合一些驗證以保證驗證更嚴格。而這些在伺服器端的業務原則應該是一個完整的集合,而在每個用戶端上只是一個子集。
確實,你也可以通過重用源檔案來為用戶端和伺服器的業務原則建立不同的程式集,但這對你的布署機制來說會成為一個複雜的問題。當你更新這些業務原則時,你就有兩個安裝要完成。相反,你可以從嚴格的伺服器端驗證中分離一部分驗證,封裝成不同的程式集放置到用戶端。這樣,你就重用封裝成程式集的二進位對象。這比起重用代碼或者資源,重新編譯成多個程式集要好得多。
做為一個程式,應該是一個包含相關功能的組織圖庫。這已經是大家熟悉的了,但在實際操作中卻很難實現。實際上,對於一個分布式應用程式,你可能不能提前知道哪些類應該同時分布到伺服器和用戶端上。即使可能,服務端和用戶端的功能也有可能是流動的;你將來很有可能要面臨兩邊都要處理的地步。通過儘可能能的讓程式集小,你就有可能更簡單的重新布署伺服器和用戶端。程式集是應用程式的二進位塊,對於一個工作的應用程式來說,很容易添加一個新的組件外掛程式。如果你不小心出了什麼錯誤,建立過多的程式集要比個別很太的程式要容易處理得多。
我經常程式集和二進位組件類似的看作是Lego。你可以很容易的抽出一個Lego然後用另一個代替。同樣的,對於有相同介面的程式集來說,你應該可以很容易的把它抽出來然後用一個新的來替換。而且程式其它部份應該可以繼續像往常一樣運行。這和Lego有點像,如果你的所有參數和傳回值都是介面,那麼任何一個程式集就可以很容易的用另一個有相同介面的來代替(參見條款19)。
更小的程式集同樣可以讓你對程式啟動時的開銷進行分期處理。更大的程式要花上更多的CUP時間來載入,以及更多的時間來編譯必須的IL到機器指令。應該只在啟動時JIT一些必須的內容,而程式集是整個載入的,而且CLR要為程式集中的每個方法儲存一個存根。
稍微休息一下,而且確保我們不會走到極端。這一原則是確保你不會建立出單個單片電路的程式,而是建立基於二進位的整體系統,而且是可重用的組件。不要參考這一原則而走到另一個極端。一個基於太多小程式集的大型應用程式的開銷是相關的。如果你的程式使用了太多的程式集,那麼在程式集之間的穿梭會產生更多的開銷。在載入更多的程式集並轉化IL為機器指令時,CLR的載入器有一點額外的工作要完成,那就是調整函數入口地址。
同樣,以程式集之間穿梭時,安全性檢查也會成為一個額外的開銷。同一個程式集中的所有的代碼具有相同的信任層級(並不是同樣的存取層級,而是可信層級)。 無論何時,只要代碼訪問超出了一個程式集,CLR都要完成一些安全驗證。程式花在程式集間穿梭的時間越少,相對程式的效率就更高。
這些與效能相關的說明並沒有一個是勸阻你把一個大程式集分離成小程式集的。效能的損失是其次的,C#和.Net的設計是以組件為核心思想的,更好的伸縮性通常更有價值。
那麼,你決定一個程式集中放多少代碼或者多少類呢?更重要的是,你是如何決定哪些代碼應該在一個程式集中?這很大程度上取決於實際的應用程式,因此這並沒有一個確論。我這裡有一個推薦:通過觀察所有的公用類開始,用一個公用基類合并這些類到一個程式集中。然後添加一些工具類到這個程式集中,這些工具類主要是負責提供所有相關類的功能。把相關的公用介面封裝到一個獨立的程式集中。最後一步,查看那些在應用程式中橫向訪問的對象,這些是有可能成為廣泛使用的工具程式集的候選對象,它們可能會包含在應用程式的工具庫中。
最後的結果就是,你的組件只在一個簡單的相關集合中,這個集合中只有一些必須的公用類,以及一些工具類來支援它們。這樣,你就建立了一個足夠小的程式集,而且很容易從更新和重用中得到好處,同時也在最小化多個程式集相關的開銷。一個設計好的內聚組件可以用一句話來概括。例如,“Common.Storage.dll 用管理所有離線使用者資料緩衝以及使用者佈建。”就描述了一低內聚的組件。相反,做兩個組件:“Common.Data.dll 管理離線資料緩衝。Common.Settings.dll 系統管理使用者設定。” 當你把它們分開後,你可能還要使用一個第三方組件:“Common.EncryptedStorage.dll 為本地加密儲存管理檔案系統IO” ,這樣你就可以獨立的更新這三個組件了。
小,是一個相對的條件。Mscorlib.dll就大概有2MB,System.Web. RegularExpressions.dll卻只有56KB。但它們都滿足小的核心設計目標,重用程式集:它們都包含相關類和介面的集合。絕對大小的不同應該根據功能的不同來決定:mscorlib.dll包含了所有應用程式中要使用的最底層的類。而System.Web.RegularExpressions.dll卻很特殊,它只包含一些在Web控制項中要使用的Regex類。這就建立了兩種不同類型的組件:一個就是小,而大的程式集則是集中在特殊的功能上,廣泛應用的程式集包含通用的功能。不論哪種情況,應該它們儘可能合理的小,直到不能再小。