這一原則實際應該取這個名字:“應該創建大小合理而且包含少量公共 類型的程序集”。但這太沉長了,所以就以我認為最常見的錯誤來命名: 開發人員總是把所有的東西,除了廚房裡水溝以外(譯注:誇張說法,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控件中要使用的正則表達式類。這就創建了兩種不同類型 的組件:一個就是小,而大的程序集則是集中在特殊的功能上,廣泛應用的程序 集包含通用的功能。不論哪種情況,應該它們盡可能合理的小,直到不能再小。
返回教程目錄