再讀整潔架構之道(四)元件構建原則
第三篇主要敘述如何設計模組和類,在本篇將會更近一步,敘述如何進行元件的設計。
元件是軟體的部署單元,是整個軟體系統在部署過程中可以獨立完成部署的最小實體,元件可以被單獨開發,元件化的外掛式架構已經成為我們習以為常的軟體構建形式了。
元件聚合
元件聚合告訴我們哪些模組和類應該組合在一起形成元件。主要有三個原則:
- 複用/釋出等同原則;
- 共同閉包原則;
- 共同複用原則;
複用/釋出等同原則REP
REP指出軟體複用的最小粒度應等同於其釋出的最小粒度。
REP是從程式碼複用角度考慮的,它規定了具備相同主題和功能的程式碼可以進行組合形成元件。REP建議軟體複用應該按照元件來進行的。
通俗來講,某個元件打包釋出後可能會形成版本號或唯一標識,我們通過引入現成程式碼庫的方式複用程式碼,引入的單位就是元件打包的結果,引入的憑據就是版本號。
共同閉包CCP
CCP指出我們應該將那些會同時修改,並且為相同目的而修改的類放到同一個元件中,而將不會同時修改,並且不會為了相同目的而修改的那些類放到不同的元件中。
CCP是從程式碼維護角度考慮的,對於由於同一原因/修改目的程式碼應該組合形成元件,這樣就可以有效地降低因軟體釋出、驗證及部署所帶來的工作壓力。
前面說過,CCP是SRP的元件版,SRP和CCP都可以用以下的語言概括:
將由於相同原因而修改,並且需要同時修改的“東西”放在一起。將由於不同原因而修改,並且不同時修改的東西分開。
- 在SRP中,東西是指函式和其他組成類或模組的成分;
- 在CCP中,東西是指構成元件的類和模組。
共同複用CRP
CRP指出不要強迫一個元件的使用者依賴他們不需要的東西。
CRP規定了對於使用場景不同、使用頻次不同的程式碼需要進行拆分到不同的元件中。CRP 是為了避免不必要的切分,是ISP介面隔離原則的普適版。
CRP是介面隔離的普適版,ISP和CRP都可以用以下的語言概括:
不要依賴不需要用到的“東西”。
- 在ISP中,東西是指包含不需要的方法的函式/類;
- 在CCP中,東西是指包含不需要的函式的類/模組;
思考
那麼REP、CCP、CRP三者之間有什麼關係呢?
實際上三者存在著競爭關係。
REP和CCP是黏合性原則,它們指導我們應該把哪些類/模組放在一起形成元件,而CRP屬於排他性原則,它知道我們哪些類/模組不應該放在一起。

- 如果架構師只關注REP和CCP,那麼會發現軟體依賴了的元件中包含了很多並不需要的部分,這樣會導致太多不必要的釋出;
- 如果架構師只關注CRP和CCP,那麼會發現軟體的複用會變得非常困難;
- 如果架構師只關注REP和CRP,那麼會發現如果部分類/模組需要變更,很多相關模組也不可避免的要跟隨變更;
軟體架構師的任務就是要在這三個原則中間進行取捨,元件的構成安排應隨著專案重心的不同,以及研發性與複用性的不同而不斷演化,因此確定專案朝著哪個方向演進是要動態判斷的。
元件耦合
元件耦合告訴我們如何安排元件之間的關係。它包含三個原則:
- 無依賴環原則ADP;
- 穩定依賴原則SDP;
- 穩定抽象原則SAP;
無依賴環原則ADP
ADP告訴我們元件依賴關係圖中不應該出現環。
版本號控制機制解決了一覺醒來綜合徵的問題,而此機制需要遵循ADP,首先讓我們讀一下作者是怎麼介紹一覺醒來綜合徵的:
當你花了一整天的時間,好不容易搞定了一段程式碼,第二天上班時卻發現這段程式碼莫名其妙地又不能工作了。這大機率是其他工作人員修改了你專案所依賴的元件。
要想解決這個問題一般有兩種方案:
- 每週構建
- 版本號控制機制
每週構建:每個人先在自己的程式碼倉工作,每週固定時間(比如週五)再進行專案構建並處理可能的衝突問題。
這種方式的侷限性很明顯:
- 隨著專案越來越大,整合工作會越來越難以按時完成
- 整個專案會變得越來越難以構建與測試,團隊反饋週期會越來越長,研發質量自然也會越來越差
因此版本號控制機制需要被引入。
版本號控制機制:每當一個元件釋出新版本時,其他依賴這個元件的團隊都可以自主決定是否立即採用新版本。
版本號控制機制不允許元件結構依賴關係圖中出現環,也就是要遵守無依賴換原則ADP,否則一覺醒來綜合徵是不可避免的。為什麼呢?
這是因為如果存在依賴環路,環路里的所有元件被組合成了一個更大的元件,這就要求他們都必須使用相同的版本,這樣其他的元件才能成功的完成依賴。同樣,存在依賴換的系統在測試時也會成為一個棘手的問題,雖然打樁mock技術已經被廣泛應用,但同時為環路中的元件重複打樁也已經十分的不優雅了。
那麼如何消除迴圈依賴呢?方案有兩個:
- 使用依賴反轉建立介面;
- 建立新元件。
應用DIP解決迴圈依賴很好理解,下面講述一個在工作中遇到的案例來說明如何使用建立新元件來打破迴圈依賴。
現在有這樣的一個場景,團隊正在使用領域驅動設計的方法設計系統架構,現在實體Entity Dog需要依賴Dog Repo完成Save儲存操作。此時Entity模組依賴了Repo模組,很不幸的是在Entity模組中我們還定義了Dog PO(簡直糟透了),Repo的Save又需要使用PO來完成物件的轉換並存儲。此時Entity和Repo之間就發生了迴圈依賴,幸運的是,在Go中模組之間的相互依賴是不允許的,檢查起會提醒“迴圈匯入”錯誤,怎麼處理這個問題呢?
答案是我們需要新建立一個PO模組,並把Entity中的PO移動到模組,Repo中依賴PO的函式也要劃分到PO中,這樣就消除了依賴環。使用依賴注入方法也是一個選擇,如果你使用SprintBoot,@AutoWired即可解決此問題,但其本質也是建立了一個新元件——依賴池,消除了依賴環。
類似的例子還有很多很多,是否還記得我們使用anaconda下載Python庫時遇到的庫版本衝突問題……
在這裡多插一句,筆者在設計元件結構的時候很自然的就想到要和系統功能對應,也就是說,元件應該是和系統功能一一對應的,這樣元件依賴圖就和系統功能模組劃分也一一對應起來。想當然認為在系統設計的一開始元件依賴結構圖也就產出了。
但作者特別提到,自頂向下的設計是不可能的。讓我們看看書中是怎麼描述的:
[!NOTE]
元件結構圖必須隨著軟體系統的變化而變化和擴張,而不可能在系統構建的最初就被完美設計出。因為元件依賴結構圖並不和功能一一對應,它更像是應用程式在構建性與維護性方面的一張地圖。
被設計並實現出來的模組越來越多,專案中就逐漸出現了要對元件依賴關係進行管理的需求,我們希望將專案變更所影響的範圍被限制得越小越好,因此需要應用單一職責原則(SRP)和共同閉包原則(CCP)來將經常同時被變更的類聚合在一起。
元件結構圖中的一個重要目標是指導如何隔離頻繁的變更。我們不希望那些頻繁變更的元件影響到其他本來應該很穩定的元件。
另外,隨著應用程式的增長,建立可重用元件的需要也會逐漸重要起來。這時CRP又會開始影響元件的組成。最後當迴圈依賴出現時,隨著無迴圈依賴原則(ADP)的應用,元件依賴關係會產生相應的抖動和擴張。
穩定依賴原則SDP
SDP告訴我們依賴關係必須要指向更穩定的方向。通常來說,要做到軟體架構的越底層越穩定。
任何一個我們預期會經常變更的元件都不應該被一個難於修改的元件所依賴,否則這個多變的元件也將會變得非常難以被修改。這就是軟體開發的困難之處,我們精心設計的一個容易被修改的元件很可能會由於別人的一條簡單依賴而變得非常難以被修改,通過遵循SDP,這樣的問題就能夠被避免。
判斷元件是否穩定,可以使用穩定性指標。作者定義穩定性指標可以通過如下公式計算: $$ I=\frac{Fan-out}{Fan-in+Fan-out} $$ 其中Fan-in為元件的入依賴數量,Fan-out為出依賴數量,這意味著一個元件的出依賴越少越穩定,因為如果出依賴為 0 意味著沒有因素可以讓元件改變。
穩定依賴原則(SDP)的要求就是要讓每個元件的I指標都必須大於其所依賴元件的I指標(即被指向的元件更穩定)。
SDP並不是要求所有元件都應該是穩定的,我們設計元件架構圖的目的就是要決定應該讓哪些元件穩定,讓哪些元件不穩定,所有元件都穩定的架構是不靈活的,不靈活的架構沒有足夠的架構價值。
穩定抽象原則SAP
SAP指出一個元件的抽象化程度應該與其穩定性保持一致。
高層策略等應該存在穩定的元件中,但這樣做會導致高層策略很難修改,幸運的是開閉原則 OCP 告訴我們穩定的元件可以被設計成容易擴充套件的,抽象類具有穩定抽象能力,可以設計一個抽象類具有較好的穩定性並且易於擴充套件和修改。
抽象類是介面和類之間的緩衝地帶。
穩定抽象原則(SAP)為元件的穩定性與它的抽象化程度建立了一種關聯。原則要求穩定的元件同時應該是抽象的,這樣它的穩定性就不會影響到擴充套件性。同時一個不穩定的元件應該包含具體的實現程式碼,這樣它的不穩定性就可以通過具體的程式碼被輕易修改。因此,如果一個元件想要成為穩定元件,那麼它就應該由介面和抽象類組成,以便將來做擴充套件。
和穩定性類似,我們也可以使用抽象程度指標來衡量元件的抽象程度: $$ A=\frac{N_c}{N_a} $$ 其中Nc是指元件中類的數量,而Na是元件中抽象類和介面的數量。A指標的取值範圍是從0到1,值為0代表元件中沒有任何抽象類,值為1就意味著元件中只有抽象類。
思考🤔
SDP和SAP有什麼關聯嗎?
第一點,從SDP、SAP與DIP上說
實際上,SDP+SAP=元件DIP。因為SDP要求的是讓依賴關係指向更穩定的方向,而SAP則告訴我們穩定性本身就隱含了對抽象化的要求,即依賴關係應該指向更抽象的方向。
怎麼理解這句話呢?
可以想到,DIP在類上的作用是為了保證靈活性,SDP+SAP實際上就是為了保證元件在穩定的基礎上足夠靈活,並且 DIP 和 SDP+SAP 在軟體架構上的影響都是具體實現類指向抽象類/介面,因此從作用上看SDP+SAP=元件DIP。
但是DIP和SDP+SAP還是有不同之處,對類來說,設計是沒有灰色地帶的,一個類要麼是抽象類,要麼就不是。SDP與SAP這對原則是應用在元件層面上的,我們要允許一個元件部分抽象,部分穩定。
其次,從SAP和SDP的目的達成來說
此外,SDP指出依賴應該指向穩定的位置,SAP要求我們在設計元件時,高層的策略應該設計在抽象的元件,以便我們在架構設計時將策略與細節分開,同時便於我們進行外掛式開發,SDP和SAP他們在目的上是一致的。
看到了吧,這裡又提到了外掛式開發這個詞,上次是在哪裡提到的?對,依賴反轉DIP,這也說明了SAP+SDP確實類似於元件DIP。
主序列
把元件穩定性指標I與元件抽象程度指標A結合在一起,畫出一張圖,a=-i+1這條直線就叫做主序列線。

整個區域可以分為三個區域:
- 痛苦區:靠近(0,0)的區域,在這個區域內的元件十分穩定但同時又十分的具體,因此不易改變,如資料庫表;工具庫也是處於痛苦區的元件,雖然其 I 指標為 1(工具庫依賴非常多的元件,因此其不穩定),但不能被修改,否則很多程式碼會出現問題;
- 無用區:處於(1,1)附近的元件,該位置上的元件通常是無限抽象的,但是沒有被其他元件依賴,這樣的元件往往無法使用,對於這個區域中的軟體元件來說,其原始碼或者類中的設計問題通常是由於歷史原因造成的,比如忘記清除的舊程式碼;
- 主序列線區:在整條主序列線上的區域,元件所能處於最優的位置是線的兩端(0, 1)與(1, 0)。一個優秀的軟體架構師應該爭取將自己設計的大部分元件儘可能地推向這兩個位置。
怎麼衡量元件的綜合性能呢?答案是計算它與主序列線的距離,來量化一個系統設計與主序列的契合程度,這個值為D值,值為0意味著元件是直接位於主序列線上的,值為1則意味著元件在距離主序列最遠的位置。 $$ D=|A+I-1| $$ 這樣我們就可以用D指標大於0多少來指導元件的重構與重新設計。
除此以外,D還可以有更多的作用,比如計算設計中所有元件的D指標的平均值和方差,用統計學的方法來量化分析一個系統設計:
- 對於一個良好的系統設計來說,D指標的平均值和方差都應該接近於0;
- 方差作為元件的“達標紅線”來使用,我們可以通過它找出系統設計中那些不合常規的元件;
- 還可以按照時間追蹤 D 的方差值,來觀察隨著時間的變化,軟體系統架構的穩定和抽象變化。
(第四篇完)