軟件架構編年史:包和命名空間
本文轉載自微信公眾號「逸言」,作者覃宇 。轉載本文請聯(lián)系逸言公眾號。
一個系統(tǒng)的架構是它的高層級的視圖,是系統(tǒng)的大局觀,是粗線條的系統(tǒng)設計。架構的決策就是系統(tǒng)結構上的決策,這些決策影響著全部代碼,決定了系統(tǒng)中其它部分的基礎。
除了其它用處以外,架構決定了系統(tǒng)的:
- 組件
- 組件之間的關系
- 指導對組件以及及其之間關系進行設計和演進的原則
換句話說,這些設計決策在系統(tǒng)演進的過程中更難改變,它們是支撐特性開發(fā)的基礎。
意大利面架構
我參與的有些項目結構完全是隨意的,又不能體現(xiàn)架構也不能反映領域。如果我的問題是“這個值對象應該放在哪里?”,答案就是“隨便放在 src 目錄里就好了”。如果我的問題是“完成這個邏輯的服務在哪里”,答案是“用 IDE 搜索吧”。這意味著完全沒有思考該如何組織代碼。
這里的隱患很大,因為完全沒有使用包來實現(xiàn)模塊化,高級別的代碼關系和流向完全不遵守任何邏輯結構,將導致高耦合低內(nèi)聚的模塊,實際上可能根本就沒有模塊劃分,本來應該屬于某個模塊的代碼散落在整個代碼庫中。這樣的代碼庫就是所謂的意大利面代碼,或者是意大利面架構!
可維護的代碼庫
擁有可維護的代碼庫意味著我們能以最小的代碼修改獲得最大的概念變化。換句話說,如果我們需要修改一個代碼單元,其它代碼單元的修改應該盡可能地少。
這帶來了明顯的優(yōu)勢:
- 因為影響的代碼少,修改會更容易;
- 因為影響的代碼少,修改會更快;
- 因為修改的代碼少,出現(xiàn)問題的幾率會降低;
封裝 、低耦合和高內(nèi)聚是保持代碼隔離的核心原則,使可維護的代碼庫成為可能。
封裝
封裝是隱藏一個類的內(nèi)部表示和實現(xiàn)的過程。
也就是說,實現(xiàn)被隱藏了,這樣類的內(nèi)部結構可以隨意的改變,而不會影響使用這個類的其它類的實現(xiàn)。
低耦合
耦合涉及代碼單元之間的關系。如果一個模塊的修改會導致另一個模塊的修改,我們就說這兩個模塊高度耦合。如果一個模塊可以獨立于其它任何模塊,我們就說它是松耦合的。通過提供穩(wěn)定的接口來有效地對其它模塊隱藏實現(xiàn),可以達成松耦合的目標。
低耦合的優(yōu)點
- 可維護性 - 修改被限制在一個模塊中
- 可測試性 - 單元測試涉及的模塊盡可能地少
- 可讀性 - 需要仔細分析的類盡可能地少
高內(nèi)聚
內(nèi)聚指的是對模塊內(nèi)的功能相關性有多強的度量。低內(nèi)聚指的是模塊擁有一些不同的不相關的職責。高內(nèi)聚指的是模塊所擁有的功能在許多方面很相似。
高內(nèi)聚的優(yōu)點
- 可讀性 - (緊密) 相關的功能包含在一個模塊中
- 可維護性 - 只用在一個模塊內(nèi)調(diào)試代碼
- 可重用 - 類的功能十分聚焦,不會充斥許多無用的函數(shù)
對結構的影響
上述這些原則適用于類,然而,它們一樣適用于類的組合。類的組合通常被叫做包,但我們可以分得更細一些,如果分組是出于純粹功能方面的考慮(如ORM)我們會稱之為模塊,如果是出于領域方面的考慮(如AccountManagement)則稱之為組件。這些定義與 Bass、Clements 和 Kazman 在他們的著作 Software Architecture in Practice 里的描述一致。
我們能夠并且應該讓包做到高內(nèi)聚和低耦合,因為這樣我們才能做到:
- 修改一個包而不會影響其它的包,減少出現(xiàn)的問題;
- 修改一個包而不需要修改其它的包,加快交付的節(jié)奏;
- 讓團隊專注于特定的包,帶來更快、更健壯和設計更優(yōu)的變化;
- 團隊開發(fā)活動之間的依賴和沖突更少,提升產(chǎn)能。
- 更仔細地斟酌組件之間的關系,讓我們更好地將應用作為一個整體建模 ,交付質(zhì)量更高的系統(tǒng)。
概念封裝
我覺得如果我們的項目結構能以某種方式既體現(xiàn)出架構也體現(xiàn)出領域的話,我們的代碼庫的可維護性可以得到極大地提升。實際上現(xiàn)在我敢篤定這也是唯一可行的方式(當我們面對大中型企業(yè)應用時)。
代碼庫如果組織得當,特定代碼單元只有一處位置可供它存放。我們可能并不知道到具體的位置,但一定只有一條邏輯路徑可以讓我們順藤摸瓜找到它。
包的定義
將類劃分成包可以讓我們在更高的抽象級別來思考設計。其目標是將你的應用中的類按照某種條件進行分片,然后將這些分片分配到包中。這些包之間的關系表達出了應用高級別的組織方式。—— Robert C. Martin 1996, Granularity pp. 3
將概念上相關的代碼定義成包,我們需要達成的目標。這些包十分重要,因為它們定義了概念上相關且獨立于其它包的代碼單元,還有這些包之間的關系。
這樣做的目的是:
- 理解代碼單元之間的關系
- 維護代碼單元之間的邏輯關系
- 實現(xiàn)高內(nèi)聚低耦合的代碼包
- 在不影響/極少影響應用的情況下重構代碼包
- 在不影響/極少影響應用的情況下替換代碼包的實現(xiàn)
分包的原則
我們要遵循 Robert C. Martin 在 1996 年和 1997 年提出的包劃分原則以及其他的一些原則來達成目標,主要有 CCP (Common Closure Principle,共同封閉原則), the CRP (Common Reuse Principle,共同重用原則) 和 SDP (Stable Dependencies Principle,穩(wěn)定依賴原則)。
Robert C. Martin 提出的包劃分原則:
包內(nèi)聚原則
- REP – 重用發(fā)布等價原則:重用的粒度等價于發(fā)布的粒度
- CCP – 共同封閉原則:一起被修改的類應該放在一個包里
- CRP – 共同重用原則:一起被重用的類應該放在一個包里
包耦合原則
- ADP – 無環(huán)依賴原則:包的依賴圖中不能出現(xiàn)循環(huán)
- SDP – 穩(wěn)定依賴原則:依賴應該朝著穩(wěn)定的方向前進
- SAP – 穩(wěn)定抽象原則:抽象的級別越高,穩(wěn)定性就越高
要想合理地運用 SDP,我們應該定義出代碼的概念單元(組件)和組件的分層,這樣我們才能搞清楚那些組件應該了解(依賴)其它組件。
然而,如果這些組件的邊界不夠清晰,我們就會把本該互不相干的代碼代碼單元混在一起,讓它們耦合在一起變成意大利面式代碼,最后將無法維護。
要讓這些邊界能清楚地呈現(xiàn)出來,我們需要把概念上相關的類放在同一個包中,就像我們把概念上相關的方法放在同一個類中一樣。在包這個級別,我們只能用一些名字在領域中有一定含義(例如,UserManagement、Orders、Payments 等)的文件夾來區(qū)分它們。在最底層的級別,即包內(nèi)的葉子節(jié)點,我們才會在必要時按照功能作用區(qū)分類(例如,Entity、Factory、Repository 等)。
下面這個問題可以幫助我們反思如何設計出低耦合的組件:
“如果我想去掉一個業(yè)務概念,是不是刪除掉它的組件根目錄就能把這個業(yè)務概念的所有代碼刪除而且應用的剩余部分還不會被破壞?”
如果答案是肯定的,那么我們就有了一個解耦得不錯的組件。
例如,在命令總線架構中,命令和處理器離開對方就無法工作,它們在概念上和功能上都綁定在一起,因此,如果我們需要去掉該邏輯就要將它們一起去掉。如果它們在同一個位置,我們只用刪除一個文件夾就好(我們并非真的要刪除代碼,只是借助這種思維方式來幫我們得到解耦和內(nèi)聚的代碼)。所以,遵循 CCP 和 CRP 原則,命令應該和它的處理器放在同一個文件夾中。
任何代碼只能存在于一個邏輯上的位置,即使對項目中的新手和初級開發(fā)者來說,這個位置也是十分明了的。這能避免自相矛盾、令人費解、重復的代碼和開發(fā)者的挫敗感。如果因為無法在代碼本該在的位置找到它,和/或難以理解哪些代碼和手頭上正在處理的代碼有關,而導致我們需要去搜尋這些代碼...那么我們的項目結構就很糟糕,甚至是更壞的情況,架構很糟糕。
尖叫架構
尖叫架構是 Robert C. Martin 的想法,它基本上表明了這樣一個觀點,架構應該清楚地告訴我們系統(tǒng)是做什么的:即它的主要領域。那么源代碼文件夾里出現(xiàn)的第一級目錄自然就應該和領域概念有關,即最頂層的限界上下文(例如,患者、醫(yī)生、預約等)。它們應該和系統(tǒng)使用的工具(例如,Doctrine、MySQL、Symfony、Redis 等)無關,和系統(tǒng)的功能塊(例如,資源庫、制圖、控制器等)無關,和傳達機制無關(HTTP、控制臺等)。
你的架構應該呈現(xiàn)給人的應該是系統(tǒng),而不是系統(tǒng)使用的框架。如果你構建的是一個醫(yī)療保健系統(tǒng),那么新程序員看到源代碼倉庫后的第一映像應該是:“哦,這是一個醫(yī)療保健系統(tǒng)”。—— Robert C. Martin 2011, Screaming Architecture
這實際上是一種更簡單地理解他十五年前發(fā)表的包劃分原則的方法,這些原則之前我已經(jīng)闡述過了。這種分包的風格又叫做“按特性分包”。
延伸閱讀
- 2008 – Johannes Brodwall – Package by feature
- 2012 -Johannes Brodwall – How Changing Java Package Names Transformed my System Architecture
- 2012 – sivaprasadreddy.k – Is package by feature approach good?
- 2013 – Lahlali Issam – Lessons to Learn from the Hibernate Core Implementation
- 2013 – Manu Pk – Package your classes by Feature and not by Layers
- 2015 – Simon Brown – Package by component and architecturally-aligned testing
- 2015 – César Ferreira – Package by features, not layers
- 2017* – javapractices.com – Package by feature, not layer
引用來源
- 1996 – Robert C. Martin – Granularity
- 1997 – Robert C. Martin – Stability
- 2009 – 500internalservererror – What do low coupling and high cohesion mean? What does the principle of encapsulation mean?
- 2011 – Robert C. Martin – Screaming Architecture
覃宇,Android開發(fā)者/ThoughtWorks技術教練//譯者,熱衷于探究軟件開發(fā)的方方面面,從端到云,從工具到實踐。喜歡通過翻譯來學習和分享知識,譯作有《Kotlin實戰(zhàn)》、《領域驅動設計精粹》、《Serverless架構:無服務器應用與AWS Lambda》和《云原生安全與DevOps保障》。