軟件架構編年史:包和命名空間

覃宇,Android 開發者 / ThoughtWorks 技術教練 // 譯者,熱衷於探究軟件開發的方方面面,從端到雲,從工具到實踐。喜歡通過翻譯來學習和分享知識,譯作有《Kotlin 實戰》、《領域驅動設計精粹》、《Serverless 架構:無服務器應用與 AWS Lambda》和《雲原生安全與 DevOps 保障》。

一個系統的架構是它的高層級的視圖,是系統的大局觀,是粗線條的系統設計。架構的決策就是系統結構上的決策,這些決策影響着全部代碼,決定了系統中其它部分的基礎。

除了其它用處以外,架構決定了系統的:

換句話說,這些設計決策在系統演進的過程中更難改變,它們是支撐特性開發的基礎。

◐ 意大利麪架構

我參與的有些項目結構完全是隨意的,又不能體現架構也不能反映領域。如果我的問題是 “這個值對象應該放在哪裏?”,答案就是 “隨便放在 src 目錄裏就好了”。如果我的問題是 “完成這個邏輯的服務在哪裏”,答案是 “用 IDE 搜索吧”。這意味着完全沒有思考該如何組織代碼。

這裏的隱患很大,因爲完全沒有使用包來實現模塊化,高級別的代碼關係和流向完全不遵守任何邏輯結構,將導致高耦合低內聚的模塊,實際上可能根本就沒有模塊劃分,本來應該屬於某個模塊的代碼散落在整個代碼庫中。這樣的代碼庫就是所謂的意大利麪代碼,或者是意大利麪架構!

◐ 可維護的代碼庫

擁有可維護的代碼庫意味着我們能以最小的代碼修改獲得最大的概念變化。換句話說,如果我們需要修改一個代碼單元,其它代碼單元的修改應該儘可能地少。

這帶來了明顯的優勢:

封裝 、低耦合和高內聚是保持代碼隔離的核心原則,使可維護的代碼庫成爲可能。

封裝

封裝是隱藏一個類的內部表示和實現的過程。

也就是說,實現被隱藏了,這樣類的內部結構可以隨意的改變,而不會影響使用這個類的其它類的實現。

低耦合

耦合涉及代碼單元之間的關係。如果一個模塊的修改會導致另一個模塊的修改,我們就說這兩個模塊高度耦合。如果一個模塊可以獨立於其它任何模塊,我們就說它是松耦合的。通過提供穩定的接口來有效地對其它模塊隱藏實現,可以達成松耦合的目標。

低耦合的優點

高內聚

內聚指的是對模塊內的功能相關性有多強的度量。低內聚指的是模塊擁有一些不同的不相關的職責。高內聚指的是模塊所擁有的功能在許多方面很相似。

高內聚的優點

◐ 對結構的影響

上述這些原則適用於類,然而,它們一樣適用於類的組合。類的組合通常被叫做包,但我們可以分得更細一些,如果分組是出於純粹功能方面的考慮(如 ORM)我們會稱之爲模塊,如果是出於領域方面的考慮(如 AccountManagement)則稱之爲組件。這些定義與 Bass、Clements 和 Kazman 在他們的著作 Software Architecture in Practice 裏的描述一致。

我們能夠並且應該讓包做到高內聚和低耦合,因爲這樣我們才能做到:

◐ 概念封裝

我覺得如果我們的項目結構能以某種方式既體現出架構也體現出領域的話,我們的代碼庫的可維護性可以得到極大地提升。實際上現在我敢篤定這也是唯一可行的方式(當我們面對大中型企業應用時)。

代碼庫如果組織得當,特定代碼單元只有一處位置可供它存放。我們可能並不知道到具體的位置,但一定只有一條邏輯路徑可以讓我們順藤摸瓜找到它。

包的定義

將類劃分成包可以讓我們在更高的抽象級別來思考設計。其目標是將你的應用中的類按照某種條件進行分片,然後將這些分片分配到包中。這些包之間的關係表達出了應用高級別的組織方式。—— Robert C. Martin 1996, Granularity pp. 3

將概念上相關的代碼定義成包,我們需要達成的目標。這些包十分重要,因爲它們定義了概念上相關且獨立於其它包的代碼單元,還有這些包之間的關係。

這樣做的目的是:

◐ 分包的原則

我們要遵循 Robert C. Martin 在 1996 年和 1997 年提出的包劃分原則以及其他的一些原則來達成目標,主要有 CCP (Common Closure Principle,共同封閉原則), the CRP (Common Reuse Principle,共同重用原則) 和 SDP (Stable Dependencies Principle,穩定依賴原則)。

Robert C. Martin 提出的包劃分原則:

包內聚原則

包耦合原則

要想合理地運用 SDP,我們應該定義出代碼的概念單元(組件)和組件的分層,這樣我們才能搞清楚那些組件應該瞭解(依賴)其它組件。

然而,如果這些組件的邊界不夠清晰,我們就會把本該互不相干的代碼代碼單元混在一起,讓它們耦合在一起變成意大利麪式代碼,最後將無法維護。

要讓這些邊界能清楚地呈現出來,我們需要把概念上相關的類放在同一個包中,就像我們把概念上相關的方法放在同一個類中一樣。在包這個級別,我們只能用一些名字在領域中有一定含義 (例如,UserManagement、Orders、Payments 等) 的文件夾來區分它們。在最底層的級別,即包內的葉子節點,我們纔會在必要時按照功能作用區分類(例如,Entity、Factory、Repository 等)。

下面這個問題可以幫助我們反思如何設計出低耦合的組件:

“如果我想去掉一個業務概念,是不是刪除掉它的組件根目錄就能把這個業務概念的所有代碼刪除而且應用的剩餘部分還不會被破壞?”

如果答案是肯定的,那麼我們就有了一個解耦得不錯的組件。

例如,在命令總線架構中,命令和處理器離開對方就無法工作,它們在概念上和功能上都綁定在一起,因此,如果我們需要去掉該邏輯就要將它們一起去掉。如果它們在同一個位置,我們只用刪除一個文件夾就好 (我們並非真的要刪除代碼,只是藉助這種思維方式來幫我們得到解耦和內聚的代碼)。所以,遵循 CCP 和 CRP 原則,命令應該和它的處理器放在同一個文件夾中。

任何代碼只能存在於一個邏輯上的位置,即使對項目中的新手和初級開發者來說,這個位置也是十分明瞭的。這能避免自相矛盾、令人費解、重複的代碼和開發者的挫敗感。如果因爲無法在代碼本該在的位置找到它,和 / 或難以理解哪些代碼和手頭上正在處理的代碼有關,而導致我們需要去搜尋這些代碼... 那麼我們的項目結構就很糟糕,甚至是更壞的情況,架構很糟糕。

◐ 尖叫架構

尖叫架構是 Robert C. Martin 的想法,它基本上表明瞭這樣一個觀點,架構應該清楚地告訴我們系統是做什麼的:即它的主要領域。那麼源代碼文件夾裏出現的第一級目錄自然就應該和領域概念有關,即最頂層的限界上下文 (例如,患者、醫生、預約等)。它們應該和系統使用的工具(例如,Doctrine、MySQL、Symfony、Redis 等) 無關,和系統的功能塊 (例如,資源庫、製圖、控制器等) 無關,和傳達機制無關(HTTP、控制檯等)。

你的架構應該呈現給人的應該是系統,而不是系統使用的框架。如果你構建的是一個醫療保健系統,那麼新程序員看到源代碼倉庫後的第一映像應該是:“哦,這是一個醫療保健系統”。—— Robert C. Martin 2011, Screaming Architecture

這實際上是一種更簡單地理解他十五年前發表的包劃分原則的方法,這些原則之前我已經闡述過了。這種分包的風格又叫做 “按特性分包”。

◐ 延伸閱讀

◐ 引用來源

☼ 素履之往:2021 年 4 月 29 日攝於成都盛隆大廈旁一個尋常普通的小區。小區內樹木森然,可以消暑。觀賞風景,不一定要去名山勝地,關鍵在於心境。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FAJUsCwHoNZalOvs3vOdlQ