第1部分:概述
軟件架構(gòu)的終極目標(biāo)時,用最小的人力成本來滿足構(gòu)建和維護(hù)該系統(tǒng)的需求
本書的主題:描述什么是優(yōu)秀的绒北、整潔的軟件架構(gòu)與設(shè)計
兩個價值維度
行為價值:讓機(jī)器按照某種指定方式運轉(zhuǎn)
架構(gòu)價值:軟件系統(tǒng)的靈活性箱硕,即是否容易被修改
如果忽略軟件架構(gòu)的價值蒜魄,系統(tǒng)將會變得越來越難以維護(hù)跋破,終會有一天婉徘,系統(tǒng)將會變得再也無法修改。
第2部分:從基礎(chǔ)構(gòu)建開始:編程范式
結(jié)構(gòu)化編程
- 結(jié)構(gòu)化編程對程序控制權(quán)的直接轉(zhuǎn)移進(jìn)行了限制和規(guī)范
- 用順序結(jié)構(gòu)、分支結(jié)構(gòu)钠乏、循環(huán)結(jié)構(gòu)可構(gòu)造出任何程序
- 結(jié)構(gòu)化編程范式可將模塊遞歸拆分為可推導(dǎo)的單元
- goto有害,某些用法會導(dǎo)致某個模塊無法被遞歸拆分成更小春塌、可證明的單元
面向?qū)ο缶幊?/h4>
- 面向?qū)ο缶幊虒?strong>程序控制權(quán)的間接轉(zhuǎn)移進(jìn)行了限制和規(guī)范
- 面向?qū)ο缶幊叹褪且远鄳B(tài)為手段來對源代碼中的依賴關(guān)系進(jìn)行控制的能力晓避,這種能力讓軟件架構(gòu)師可以構(gòu)建出某種插件式架構(gòu),讓高層策略性組件與底層實現(xiàn)性組件相分離只壳,底層組件可以被編譯成插件俏拱,實現(xiàn)獨立于高層組件的開發(fā)和部署
函數(shù)式編程
函數(shù)式編程對程序中的賦值進(jìn)行了限制和規(guī)范
可變變量導(dǎo)致競爭問題、死鎖問題吼句、并發(fā)更新問題
不可變性方案:
一個架構(gòu)設(shè)計良好的應(yīng)用程序應(yīng)該將狀態(tài)修改的部分和不需要修改狀態(tài)的部分隔離成單獨的組件彰触,然后用合適的機(jī)制來保護(hù)可變量。軟件架構(gòu)師應(yīng)該致力于將大部分處理邏輯都?xì)w于不可變組件中命辖,可變狀態(tài)組件的邏輯應(yīng)該越少越好况毅。
事件溯源:只存儲事務(wù)記錄分蓖,不存儲具體狀態(tài)。當(dāng)需要具體狀態(tài)時尔许,從頭開始計算所有的事務(wù)么鹤。當(dāng)只有CR,沒有UD味廊,自然也就不存在并發(fā)問題蒸甜。
第3部分:設(shè)計原則
SRP:單一職責(zé)原則
- Single Responsibility Principle
- There should never be more than one reason for a class to change
- 單一職責(zé)原則主要討論的是函數(shù)和類之間的關(guān)系--但是它在兩個討論層面上會以不同的形式出現(xiàn)。在組件層面余佛,我們可以將其稱為共同必包原則(Common Closure Principle)柠新,在軟件架構(gòu)層面,它則是用于奠定架構(gòu)邊界的變更軸心(Axis of Change)辉巡。
OCP:開閉原則
- Open-Closed Principle
- 設(shè)計良好的計算機(jī)軟件應(yīng)該易于擴(kuò)展恨憎,同時抗拒修改
- 其主要目標(biāo)是讓系統(tǒng)易于擴(kuò)展,同時限制器每次被修改所影響的范圍郊楣。實現(xiàn)方式是通過將系統(tǒng)劃分為一系列組件憔恳,并且將這些組件間的依賴關(guān)系按層次結(jié)構(gòu)進(jìn)行組織,使得高階組件不會因低階組件被修改而受到影響净蚤。
LSP:里氏替換原則
- Liskov Substitution Principle
- 對于每個類型是S的對象o1钥组,都存在一個類型為T的對象o2,能使操作T類型的程序P在用o2替換o1時行為保持不變今瀑,我們就可以將S稱為T的子類型程梦。
- 正方形/長方形問題:正方形不是長方形的子類,它們具有不同的修改邊長的邏輯
ISP:接口隔離原則
- Interface Segregation Principle
- 避免不必要的依賴
-
DIP:依賴反轉(zhuǎn)原則
Dependency Inversion Principle
具體的編碼守則
應(yīng)在代碼中多使用抽象接口橘荠,盡量避免使用那些多變的具體實現(xiàn)類
不要在具體實現(xiàn)類上創(chuàng)建衍生類
不要覆蓋包含具體實現(xiàn)的函數(shù)
應(yīng)避免在代碼中寫入與任何具體實現(xiàn)相關(guān)的名字作烟,或者是其他容易變動的事物的名字
對易變對象的創(chuàng)建過程做一些處理,通常會選擇用抽象工廠模式來解決源代碼的依賴問題
依賴方向與控制流方向相反
第4部分:組件構(gòu)建原則
組件
- 組件是軟件的部署單元砾医,是整個軟件系統(tǒng)在部署過程中可以獨立完成部署的最小實體。例如衣厘,對于Java來說如蚜,它的組件是jar文件。
組件聚合
哪些類應(yīng)該被組合成一個組件呢影暴?三個與構(gòu)建組件相關(guān)的基本原則:
REP:復(fù)用/發(fā)布等同原則(為復(fù)用性而組合)
軟件復(fù)用的最小粒度應(yīng)等同于其發(fā)布的最小粒度
組件中的類和模塊必須是彼此緊密相關(guān)的
CCP:共同閉包原則(為維護(hù)性而組合)
我們應(yīng)該將那些會同時修改错邦,并且為相同目的而修改的類放到同一個組件中,而將不會同時修改型宙,并且不會為了相同目的而修改的那些類放到不同的組件中撬呢。
SRP原則在組件層面上的再度闡述,可共同概括為:將由于相同原因而修改妆兑,并且需要同時修改的東西放在一起魂拦;將由于不同原因而修改毛仪,并且不同時修改的東西分開。
CRP:共同復(fù)用原則(為避免不必要的發(fā)布而切分)
不要強(qiáng)迫一個組件的用戶依賴他們不需要的東西
和ISP可共同概括為:不要依賴不需要用到的東西
組件聚合張力圖
- REP和CCP是黏合性原則芯勘, 它們會讓組件變得更大箱靴,而CRP原則是排除性原則, 它會盡量讓組件變小荷愕。
- 一般來說衡怀,一個軟件項目的重心會從該三角區(qū)域的右側(cè)開始,先期主要犧牲的是復(fù)用性安疗。然后抛杨,隨著項目逐漸成熟,其他項目會逐漸開始對其產(chǎn)生依賴荐类,項目重心就會逐漸向該三角區(qū)域的左側(cè)滑動怖现。換句話說,一個項目在組件結(jié)構(gòu)設(shè)計上的重心是根據(jù)該項目的開發(fā)時間和成熟度不斷變動的掉冶,我們對組件結(jié)構(gòu)的安排主要與項目開發(fā)的進(jìn)度和它被使用的方式有關(guān)真竖,與項目本身功能的關(guān)系其實很小。
組件耦合
三條主要關(guān)注組件之間關(guān)系的原則:
ADP:無依賴環(huán)原則
組件依賴關(guān)系圖中不應(yīng)該出現(xiàn)環(huán)
循環(huán)依賴會使得組件的獨立維護(hù)厌小、單元測試和發(fā)布流程變得困難
打破依賴循環(huán)的兩種方式:
應(yīng)用依賴反轉(zhuǎn)原則(DIP)恢共,依賴于接口而不是實現(xiàn)
創(chuàng)建一個新的組件,將現(xiàn)有組件互相依賴的類全部放入新組件
SDP:穩(wěn)定依賴原則
依賴關(guān)系必須要指向更穩(wěn)定的方向
任何一個我們預(yù)期會經(jīng)常變更的組件都不應(yīng)該被一個難于修改的組件所依賴璧亚,否則這個多變的組件也將會變得非常難以修改讨韭。(通過遵守穩(wěn)定依賴原則解決)
當(dāng)組件不依賴于任何組件,則不會有任何原因?qū)е滤枰蛔兏Ⅲ覀兎Q它為“獨立”組件
一種計算組件穩(wěn)定性的方式透硝,I指標(biāo):I = Fan-out / (Fan-in + Fan-out)
Fan-in: 入向依賴,組件外部類依賴于組件內(nèi)部類的數(shù)量
Fan-out: 出向依賴疯搅,組件內(nèi)部類依賴于組件外部類的數(shù)量
I 不穩(wěn)定性濒生,指標(biāo)范圍是[0, 1],值越小越穩(wěn)定
每個組件的I指標(biāo)都必須大于其所依賴組件的I指標(biāo)幔欧,即組件結(jié)構(gòu)依賴圖中的I指標(biāo)必須要按其依賴關(guān)系方向遞減罪治。
可使用DIP來解決違反穩(wěn)定依賴原則的組件依賴
SAP:穩(wěn)定抽象原則
一個組件的抽象化程度應(yīng)該與其穩(wěn)定性保持一致
該原則要求穩(wěn)定的組件同時應(yīng)該是抽象的,這樣它的穩(wěn)定性就不會影響到擴(kuò)展性礁蔗;另一方面觉义,該原則也要求一個不穩(wěn)定的組件應(yīng)該包含具體的實現(xiàn)代碼,這樣它的不穩(wěn)定性就可以通過具體的代碼被輕易修改浴井。
對組件抽象化程度的一個衡量方式晒骇,A指標(biāo):A = Na / Nc
Nc: 組件中類的數(shù)量
Na: 組件中抽象類和接口的數(shù)量
A:抽象程度
穩(wěn)定程度與抽象化程度的區(qū)間分析
- 痛苦區(qū):非常穩(wěn)定但也非常具體,難以被修改
- 無用區(qū):抽象但沒有被其他組件依賴
- 最優(yōu)的位置是主序列線的兩端,實際項目中貼近線即可
- 衡量一個組件距離最佳位置的指標(biāo)洪囤,D指標(biāo):
D=|A+I-1|
第5部分:軟件架構(gòu)
什么是軟件架構(gòu)
- 軟件架構(gòu)這項工作的實質(zhì)就是規(guī)劃如何將系統(tǒng)切分成組件徒坡,并安排好組件之間的排列關(guān)系,以及組件之間互相通信的方式箍鼓。
- 設(shè)計軟件架構(gòu)的目的是為了在工作中更好地對這些組件進(jìn)行研發(fā)崭参、部署運行以及運維;最大化程序員的生產(chǎn)力款咖,同時最小化系統(tǒng)的總運營成本何暮。
- 將軟件的高層策略與其底層實現(xiàn)隔離開
- 一個優(yōu)秀的軟件架構(gòu)師應(yīng)該致力于最大化可選項數(shù)量
獨立性
康威定律:任何一個組織在設(shè)計系統(tǒng)時,往往都會復(fù)制出一個與該組織內(nèi)部溝通結(jié)構(gòu)相同的系統(tǒng)铐殃。
將系統(tǒng)正確地劃分為一些隔離良好的組件海洼,以便盡可能長時間地為我們的未來保留盡可能多的可選項
通過采用單一職責(zé)原則(SRP)和共同閉包原則(CCP),以及既定的系統(tǒng)設(shè)計意圖來隔離那些變更原因不同的部分富腊,集成變更原因原因相同的部分
按層解耦:UI界面坏逢、應(yīng)用獨有的業(yè)務(wù)邏輯、領(lǐng)域普適的業(yè)務(wù)邏輯赘被、數(shù)據(jù)庫等是整。
用例的解耦:根據(jù)用例進(jìn)行垂直切片(比如添加訂單和刪除訂單)
解耦模式
源碼層次:源代碼模塊之間的依賴;系統(tǒng)所有的組件都會在同一個地址空間內(nèi)執(zhí)行民假,通過簡單的函數(shù)調(diào)用來進(jìn)行彼此的交互浮入;通常叫做單體結(jié)構(gòu)。
部署層次:部署單元(譬如jar文件羊异、DLL事秀、共享庫等)之間的依賴;大部分組件可能還是依然運行在同一個地址空間內(nèi)野舶,通過彼此的函數(shù)調(diào)用通信易迹,部分嗎可能會運行在同一個處理器下的其他進(jìn)程內(nèi);可以產(chǎn)生出許多可獨立部署的單元平道。
服務(wù)層次:通過網(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行通信睹欲;每個執(zhí)行單元在源碼層和二進(jìn)制層都會是一個獨立的個體。
一個設(shè)計良好的架構(gòu)應(yīng)該允許一個系統(tǒng)從單體結(jié)構(gòu)開始一屋,以單一文件的形式部署窘疮,然后逐漸成長為一組相互同理的可部署單元,甚至是獨立的服務(wù)或者微服務(wù)陆淀。最后還能隨著情況的變化,允許系統(tǒng)逐漸回退到單體結(jié)構(gòu)先嬉。
劃分邊界
- 邊界的作用是將軟件分割成各種元素轧苫,以便約束邊界兩側(cè)之間的依賴關(guān)系
- 盡可能地推遲細(xì)節(jié)性的決策(采用的框架、數(shù)據(jù)庫、web服務(wù)器含懊、工具庫身冬、依賴注入等),并將這種推遲所產(chǎn)生的影響降低到最低
- 過早的細(xì)節(jié)性決策會浪費大量人力
- 邊界線應(yīng)該畫在那些不相關(guān)的事情中間岔乔,比如GUI酥筝、業(yè)務(wù)邏輯、數(shù)據(jù)庫
- 數(shù)據(jù)庫可以采用多種實現(xiàn)雏门,而業(yè)務(wù)邏輯并不需要關(guān)心這件事嘿歌。這意味著我們可以將與數(shù)據(jù)庫相關(guān)的決策延后,先專注編寫業(yè)務(wù)邏輯的代碼茁影,進(jìn)行測試宙帝,直到不得不選擇數(shù)據(jù)庫為止。
- 插件式架構(gòu)
- 為了在軟件架構(gòu)中畫邊界線募闲,我們需要先將系統(tǒng)分割成組件步脓,其中一部分是系統(tǒng)的核心業(yè)務(wù)邏輯組件,而另一部分則是與核心業(yè)務(wù)邏輯無關(guān)但負(fù)責(zé)提供必要功能的插件浩螺。然后通過對源代碼的修改靴患,讓這些非核心組件依賴于系統(tǒng)的核心業(yè)務(wù)邏輯組件。這也是一種對依賴反轉(zhuǎn)原則(DIP)和穩(wěn)定抽象原則(SAP)的具體應(yīng)用要出,依賴箭頭應(yīng)該由底層具體實現(xiàn)細(xì)節(jié)指向高層抽象的方向鸳君。
邊界剖析
邊界形式
單體結(jié)構(gòu):低層客戶端調(diào)用高層服務(wù)函數(shù)
部署層次的組件:動態(tài)鏈接庫
線程?
本地進(jìn)程
服務(wù)
策略和層次
- 策略:描述計算部分的業(yè)務(wù)邏輯厨幻、描述計算報告的格式相嵌、描述如何校驗輸入數(shù)據(jù)等
- 層次:一條策略具體系統(tǒng)的輸入/輸出越遠(yuǎn),它所屬的層次就越高
- 在一個設(shè)計良好的架構(gòu)中况脆,依賴關(guān)系的方向通常取決于它們所關(guān)聯(lián)的組件層次饭宾。一般來說,低層組件被設(shè)計為依賴于高層組件格了。
錯誤:
function encrypt() {
while(true)
writeChar(translate(readChar()))
}
高層組件encrypt()依賴于低層組件中的函數(shù)readChar()和writeChar()
正確:
Encrypt類依賴于Char Reader接口和Char Writer接口看铆,由具體類實現(xiàn)接口方法。這樣低層組件(實現(xiàn)類)就變成依賴于高層組件(Encrypt類加兩個接口)
業(yè)務(wù)邏輯
- 關(guān)鍵業(yè)務(wù)邏輯和關(guān)鍵業(yè)務(wù)數(shù)據(jù)是緊密相關(guān)的盛末,所以它們很適合被放在同一個對象中處理弹惦,這種對象也被叫做“業(yè)務(wù)實體(Entity)”
- 業(yè)務(wù)實體獨自代表了整個業(yè)務(wù)邏輯,它與數(shù)據(jù)庫悄但、用戶界面棠隐、第三方框架等內(nèi)容無關(guān)
- 用例更靠近系統(tǒng)的輸入和輸出,屬于低層概念檐嚣;而業(yè)務(wù)實體是一個可以適用于多個應(yīng)用情景的一般化概念助泽,相對地離系統(tǒng)的輸入和輸出更遠(yuǎn)。所以用例依賴于業(yè)務(wù)實體,而業(yè)務(wù)實體并不依賴于用例嗡贺。
- 不要選擇直接在數(shù)據(jù)結(jié)構(gòu)中使用對業(yè)務(wù)實體對象的引用隐解!這兩個對象存在的意義是非常不一樣的,而且這兩個對象會以不同的原因诫睬、不同的速率發(fā)生變更煞茫。整合在一起是對共同閉包原則(CCP)和單一職責(zé)原則(SRP)的違反。
尖叫的軟件架構(gòu)
- 軟件的系統(tǒng)架構(gòu)應(yīng)該為該系統(tǒng)的用例提供支持摄凡。 -- 《Object Oriented Software Engineering, A Use Case Driven Approach》
- 一個良好的架構(gòu)設(shè)計應(yīng)該圍繞著用例來展開续徽,這樣的架構(gòu)設(shè)計可以在脫離框架、工具以及使用環(huán)境的情況下完整地描述用例架谎。
- 避免讓框架主導(dǎo)我們的架構(gòu)設(shè)計
- 在不依賴任何框架的情況下針對用例進(jìn)行單元測試炸宵。另外,我們運行測試的時候不應(yīng)該運行web服務(wù)谷扣,也不應(yīng)該需要連接數(shù)據(jù)庫土全。
整潔架構(gòu)
各類架構(gòu)(六邊形架構(gòu)、DCI架構(gòu)会涎、BCE架構(gòu)等)都具有同一個設(shè)計目標(biāo):按照不同關(guān)注點對軟件進(jìn)行切割裹匙。也就是說,這些架構(gòu)都會將軟件切割成不同的層末秃,至少有一層是只包含該軟件的業(yè)務(wù)邏輯的概页,而用戶接口、系統(tǒng)接口則屬于其他層练慕。
按照這些架構(gòu)設(shè)計出來的系統(tǒng)惰匙,通常都具有以下特點:
獨立于框架
可被測試
獨立于UI
獨立于數(shù)據(jù)集
獨立于任何外部機(jī)構(gòu)
將上述所有架構(gòu)的設(shè)計理念綜合稱為一個獨立的理念:
依賴關(guān)系規(guī)則
通常越靠近中心,其所在的軟件層次就越高铃将∠罟恚基本上,外層圓代表的是機(jī)制劲阎,內(nèi)層圓代表的是策略绘盟。
貫穿整個架構(gòu)設(shè)計的規(guī)則:源碼中的依賴關(guān)系必須只指向同心圓的內(nèi)層,即由低層機(jī)制指向高層策略悯仙。
業(yè)務(wù)實體:封裝了該應(yīng)用中最通用龄毡、最高層的業(yè)務(wù)邏輯,它們應(yīng)該屬于系統(tǒng)中最不容易受外界影響而變動的部分锡垄。
用例:通常包含的是特定應(yīng)用場景下的業(yè)務(wù)邏輯沦零,這里面封裝并實現(xiàn)了整個系統(tǒng)的所有用例。這些用例引導(dǎo)了數(shù)據(jù)在業(yè)務(wù)實體之間的流入/流出货岭,并指揮著業(yè)務(wù)實體利用其中的關(guān)鍵業(yè)務(wù)邏輯來實現(xiàn)用例的設(shè)計目標(biāo)路操。
接口適配器:
通常是一組數(shù)據(jù)轉(zhuǎn)換器序攘,它們負(fù)責(zé)將數(shù)據(jù)從對用例和業(yè)務(wù)實體而言最方便操作的格式,轉(zhuǎn)換為外部系統(tǒng)(譬如數(shù)據(jù)庫以及web)和持久層框架最方便操作的格式寻拂。
如果采用的是SQL數(shù)據(jù)庫,那么所有的SQL語句都應(yīng)該被限制在這一層的代碼中丈牢。
這層也會負(fù)責(zé)將來自外部服務(wù)的數(shù)據(jù)轉(zhuǎn)換成系統(tǒng)內(nèi)用例和業(yè)務(wù)實體所需的格式
框架和驅(qū)動程序:最外層的模型層一般是由工具祭钉、數(shù)據(jù)庫、web框架等組成己沛。在這一層中慌核,我們通常只需要編寫一些于內(nèi)層溝通的黏合性代碼。
真正的架構(gòu)可能會超過四層申尼,但是源碼層面的依賴關(guān)系一定要指向同心圓的內(nèi)層垮卓。
通常采用依賴反轉(zhuǎn)原則(DIP)來解決控制流和依賴方向的相反性。
展示器和謙卑對象
- 謙卑對象模式:將容易測試的行為和難以測試的行為拆分成兩組模塊或類师幕,并將它們隔離粟按。包含難以測試的行為的模塊被稱為謙卑(Humble)組。 (具體可參考 xUnit Test Patterns: Refractoring Test Code)
- 謙卑組的代碼通常應(yīng)該越簡單越好
- 強(qiáng)大的可測試性是一個架構(gòu)設(shè)計是否優(yōu)秀的顯著衡量標(biāo)準(zhǔn)之一
- 因為跨邊界通信肯定需要用到某種簡單的數(shù)據(jù)結(jié)構(gòu)霹粥,而邊界會自然而然地將系統(tǒng)分割成難以測試的部分與容易測試的部分灭将,所以通過在系統(tǒng)的邊界處運用對象模式,我們可以大幅地提高整個系統(tǒng)的可測試性后控。
不完全邊界
為了解決YAGNI原則(You aren't going to need it, 不要預(yù)測未來的需要)和架構(gòu)師工作本身(預(yù)見性設(shè)計)之間的矛盾庙曙,需要引入不完全邊界的概念。
幾種不完全邊界策略浩淘,除此外還有許多其他實現(xiàn)方式捌朴,根據(jù)不同的場景進(jìn)行選擇
方式一:將系統(tǒng)分割成一系列可以獨立編譯、獨立部署的組件之后张抄,再把它們構(gòu)建成一個組件砂蔽。這樣可以省去多組件管理這部分的工作,比如版本號管理和發(fā)布管理等等欣鳖。
方式二:單向邊界察皇。單側(cè)組件提供接口進(jìn)行隔離,而不是采用雙向反向接口泽台。
方式三:門戶模式
架構(gòu)師的職責(zé)之一就是預(yù)判未來哪里有可能需要設(shè)置架構(gòu)邊界什荣,并決定應(yīng)該以完全形式還是不完全形式來實現(xiàn)它們。
層次與邊界
- 架構(gòu)邊界可以存在于任何地方怀酷,必須要小心審視究竟在什么地方才需要設(shè)計架構(gòu)邊界稻爬。另外,必須弄清楚完全實現(xiàn)這些邊界將會帶來多大的成本蜕依。
- 權(quán)衡利弊桅锄,反復(fù)進(jìn)行
Main組件
- Main組件是整個系統(tǒng)中的一個低層模塊琉雳,它處于整潔架構(gòu)的最外圈,主要負(fù)責(zé)為系統(tǒng)加載所有必要的信息友瘤,然后再將控制權(quán)轉(zhuǎn)交回系統(tǒng)的高層組件翠肘。
服務(wù):宏觀與微觀
面向服務(wù)的“架構(gòu)”以及微服務(wù)“架構(gòu)”本身只是一種比函數(shù)調(diào)用方式成本稍高,分割應(yīng)用程序行為的一種形式辫秧,與系統(tǒng)架構(gòu)無關(guān)束倍。
服務(wù)的一些謬論
解耦合的謬論:服務(wù)強(qiáng)耦合于數(shù)據(jù)結(jié)構(gòu)
獨立開發(fā)的謬論:大型系統(tǒng)一樣可以采用單體模式,或者組件模式來構(gòu)建盟戏,不一定非得服務(wù)化绪妹。
系統(tǒng)的架構(gòu)是由系統(tǒng)內(nèi)部的架構(gòu)邊界,以及邊界之間的依賴關(guān)系所定義的柿究,與系統(tǒng)中各組件之間的調(diào)用和通信方式無關(guān)邮旷。
測試邊界
- 測試代碼也是系統(tǒng)的一部分
- 測試代碼始終都是向內(nèi)依賴于被測試部分的代碼,而且系統(tǒng)中沒有其他組件依賴于它們蝇摸。
- 如果測試代碼與系統(tǒng)是強(qiáng)耦合的婶肩,它就得隨著系統(tǒng)變更而變更。修改以嘎通用的系統(tǒng)組件可能會導(dǎo)致成千上百個測試出現(xiàn)問題貌夕,我們通常將這類問題稱為脆弱的測試問題(fragile tests problem)狡孔。
- 軟件設(shè)計的第一條原則:不要依賴于多變的東西
整潔的嵌入式架構(gòu)
- 在軟件和固件之間添加硬件抽象層(HAL),使得硬件發(fā)生變化時蜂嗽,軟件代碼仍能使用
第6部分:實現(xiàn)細(xì)節(jié)
數(shù)據(jù)庫只是實現(xiàn)細(xì)節(jié)
- 數(shù)據(jù)的組織結(jié)構(gòu)苗膝,數(shù)據(jù)的模型,都是系統(tǒng)架構(gòu)中的重要部分植旧,但是從磁盤上存儲/讀取數(shù)據(jù)的機(jī)制和手段卻沒那么重要辱揭。
web是實現(xiàn)細(xì)節(jié)
- GUI只是一個實現(xiàn)細(xì)節(jié)。而Web則是GUI的一種病附,所以也是一種實現(xiàn)細(xì)節(jié)问窃。作為一名軟件架構(gòu)師,我們需要將這類細(xì)節(jié)與核心業(yè)務(wù)邏輯隔離開來完沪。
應(yīng)用程序框架是實現(xiàn)細(xì)節(jié)
使用框架的風(fēng)險
框架自身的架構(gòu)設(shè)計很多時候并不是特別正確
隨著產(chǎn)品的成熟域庇,功能要求很可能超出框架所能提供的范圍
框架本身可能朝著我們不需要的方向演進(jìn)
未來我們可能會想要切換到一個更新、更好的框架上
框架作為架構(gòu)最外圈的一個實現(xiàn)細(xì)節(jié)來使用覆积,不要讓它們進(jìn)入內(nèi)圈
拾遺
- 本章由Simon Brown撰寫
- 四種封裝方式的訪問限制(從左到右分別是按層封裝听皿、按功能封裝、端口和適配器宽档、按組件封裝)尉姨。
- Simon Brown對組件的定義是
函數(shù)式編程對程序中的賦值進(jìn)行了限制和規(guī)范
可變變量導(dǎo)致競爭問題、死鎖問題吼句、并發(fā)更新問題
不可變性方案:
一個架構(gòu)設(shè)計良好的應(yīng)用程序應(yīng)該將狀態(tài)修改的部分和不需要修改狀態(tài)的部分隔離成單獨的組件彰触,然后用合適的機(jī)制來保護(hù)可變量。軟件架構(gòu)師應(yīng)該致力于將大部分處理邏輯都?xì)w于不可變組件中命辖,可變狀態(tài)組件的邏輯應(yīng)該越少越好况毅。
事件溯源:只存儲事務(wù)記錄分蓖,不存儲具體狀態(tài)。當(dāng)需要具體狀態(tài)時尔许,從頭開始計算所有的事務(wù)么鹤。當(dāng)只有CR,沒有UD味廊,自然也就不存在并發(fā)問題蒸甜。
Dependency Inversion Principle
具體的編碼守則
應(yīng)在代碼中多使用抽象接口橘荠,盡量避免使用那些多變的具體實現(xiàn)類
不要在具體實現(xiàn)類上創(chuàng)建衍生類
不要覆蓋包含具體實現(xiàn)的函數(shù)
應(yīng)避免在代碼中寫入與任何具體實現(xiàn)相關(guān)的名字作烟,或者是其他容易變動的事物的名字
對易變對象的創(chuàng)建過程做一些處理,通常會選擇用抽象工廠模式來解決源代碼的依賴問題
依賴方向與控制流方向相反
哪些類應(yīng)該被組合成一個組件呢影暴?三個與構(gòu)建組件相關(guān)的基本原則:
REP:復(fù)用/發(fā)布等同原則(為復(fù)用性而組合)
軟件復(fù)用的最小粒度應(yīng)等同于其發(fā)布的最小粒度
組件中的類和模塊必須是彼此緊密相關(guān)的
CCP:共同閉包原則(為維護(hù)性而組合)
我們應(yīng)該將那些會同時修改错邦,并且為相同目的而修改的類放到同一個組件中,而將不會同時修改型宙,并且不會為了相同目的而修改的那些類放到不同的組件中撬呢。
SRP原則在組件層面上的再度闡述,可共同概括為:將由于相同原因而修改妆兑,并且需要同時修改的東西放在一起魂拦;將由于不同原因而修改毛仪,并且不同時修改的東西分開。
CRP:共同復(fù)用原則(為避免不必要的發(fā)布而切分)
不要強(qiáng)迫一個組件的用戶依賴他們不需要的東西
和ISP可共同概括為:不要依賴不需要用到的東西
組件聚合張力圖
三條主要關(guān)注組件之間關(guān)系的原則:
ADP:無依賴環(huán)原則
組件依賴關(guān)系圖中不應(yīng)該出現(xiàn)環(huán)
循環(huán)依賴會使得組件的獨立維護(hù)厌小、單元測試和發(fā)布流程變得困難
打破依賴循環(huán)的兩種方式:
應(yīng)用依賴反轉(zhuǎn)原則(DIP)恢共,依賴于接口而不是實現(xiàn)
創(chuàng)建一個新的組件,將現(xiàn)有組件互相依賴的類全部放入新組件
SDP:穩(wěn)定依賴原則
依賴關(guān)系必須要指向更穩(wěn)定的方向
任何一個我們預(yù)期會經(jīng)常變更的組件都不應(yīng)該被一個難于修改的組件所依賴璧亚,否則這個多變的組件也將會變得非常難以修改讨韭。(通過遵守穩(wěn)定依賴原則解決)
當(dāng)組件不依賴于任何組件,則不會有任何原因?qū)е滤枰蛔兏Ⅲ覀兎Q它為“獨立”組件
一種計算組件穩(wěn)定性的方式透硝,I指標(biāo):I = Fan-out / (Fan-in + Fan-out)
Fan-in: 入向依賴,組件外部類依賴于組件內(nèi)部類的數(shù)量
Fan-out: 出向依賴疯搅,組件內(nèi)部類依賴于組件外部類的數(shù)量
I 不穩(wěn)定性濒生,指標(biāo)范圍是[0, 1],值越小越穩(wěn)定
每個組件的I指標(biāo)都必須大于其所依賴組件的I指標(biāo)幔欧,即組件結(jié)構(gòu)依賴圖中的I指標(biāo)必須要按其依賴關(guān)系方向遞減罪治。
可使用DIP來解決違反穩(wěn)定依賴原則的組件依賴
SAP:穩(wěn)定抽象原則
一個組件的抽象化程度應(yīng)該與其穩(wěn)定性保持一致
該原則要求穩(wěn)定的組件同時應(yīng)該是抽象的,這樣它的穩(wěn)定性就不會影響到擴(kuò)展性礁蔗;另一方面觉义,該原則也要求一個不穩(wěn)定的組件應(yīng)該包含具體的實現(xiàn)代碼,這樣它的不穩(wěn)定性就可以通過具體的代碼被輕易修改浴井。
對組件抽象化程度的一個衡量方式晒骇,A指標(biāo):A = Na / Nc
Nc: 組件中類的數(shù)量
Na: 組件中抽象類和接口的數(shù)量
A:抽象程度
穩(wěn)定程度與抽象化程度的區(qū)間分析
D=|A+I-1|
康威定律:任何一個組織在設(shè)計系統(tǒng)時,往往都會復(fù)制出一個與該組織內(nèi)部溝通結(jié)構(gòu)相同的系統(tǒng)铐殃。
將系統(tǒng)正確地劃分為一些隔離良好的組件海洼,以便盡可能長時間地為我們的未來保留盡可能多的可選項
通過采用單一職責(zé)原則(SRP)和共同閉包原則(CCP),以及既定的系統(tǒng)設(shè)計意圖來隔離那些變更原因不同的部分富腊,集成變更原因原因相同的部分
按層解耦:UI界面坏逢、應(yīng)用獨有的業(yè)務(wù)邏輯、領(lǐng)域普適的業(yè)務(wù)邏輯赘被、數(shù)據(jù)庫等是整。
用例的解耦:根據(jù)用例進(jìn)行垂直切片(比如添加訂單和刪除訂單)
解耦模式
源碼層次:源代碼模塊之間的依賴;系統(tǒng)所有的組件都會在同一個地址空間內(nèi)執(zhí)行民假,通過簡單的函數(shù)調(diào)用來進(jìn)行彼此的交互浮入;通常叫做單體結(jié)構(gòu)。
部署層次:部署單元(譬如jar文件羊异、DLL事秀、共享庫等)之間的依賴;大部分組件可能還是依然運行在同一個地址空間內(nèi)野舶,通過彼此的函數(shù)調(diào)用通信易迹,部分嗎可能會運行在同一個處理器下的其他進(jìn)程內(nèi);可以產(chǎn)生出許多可獨立部署的單元平道。
服務(wù)層次:通過網(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行通信睹欲;每個執(zhí)行單元在源碼層和二進(jìn)制層都會是一個獨立的個體。
一個設(shè)計良好的架構(gòu)應(yīng)該允許一個系統(tǒng)從單體結(jié)構(gòu)開始一屋,以單一文件的形式部署窘疮,然后逐漸成長為一組相互同理的可部署單元,甚至是獨立的服務(wù)或者微服務(wù)陆淀。最后還能隨著情況的變化,允許系統(tǒng)逐漸回退到單體結(jié)構(gòu)先嬉。
邊界形式
單體結(jié)構(gòu):低層客戶端調(diào)用高層服務(wù)函數(shù)
部署層次的組件:動態(tài)鏈接庫
線程?
本地進(jìn)程
服務(wù)
錯誤:
function encrypt() {
while(true)
writeChar(translate(readChar()))
}
高層組件encrypt()依賴于低層組件中的函數(shù)readChar()和writeChar()
正確:
Encrypt類依賴于Char Reader接口和Char Writer接口看铆,由具體類實現(xiàn)接口方法。這樣低層組件(實現(xiàn)類)就變成依賴于高層組件(Encrypt類加兩個接口)
各類架構(gòu)(六邊形架構(gòu)、DCI架構(gòu)会涎、BCE架構(gòu)等)都具有同一個設(shè)計目標(biāo):按照不同關(guān)注點對軟件進(jìn)行切割裹匙。也就是說,這些架構(gòu)都會將軟件切割成不同的層末秃,至少有一層是只包含該軟件的業(yè)務(wù)邏輯的概页,而用戶接口、系統(tǒng)接口則屬于其他層练慕。
按照這些架構(gòu)設(shè)計出來的系統(tǒng)惰匙,通常都具有以下特點:
獨立于框架
可被測試
獨立于UI
獨立于數(shù)據(jù)集
獨立于任何外部機(jī)構(gòu)
將上述所有架構(gòu)的設(shè)計理念綜合稱為一個獨立的理念:
依賴關(guān)系規(guī)則
通常越靠近中心,其所在的軟件層次就越高铃将∠罟恚基本上,外層圓代表的是機(jī)制劲阎,內(nèi)層圓代表的是策略绘盟。
貫穿整個架構(gòu)設(shè)計的規(guī)則:源碼中的依賴關(guān)系必須只指向同心圓的內(nèi)層,即由低層機(jī)制指向高層策略悯仙。
業(yè)務(wù)實體:封裝了該應(yīng)用中最通用龄毡、最高層的業(yè)務(wù)邏輯,它們應(yīng)該屬于系統(tǒng)中最不容易受外界影響而變動的部分锡垄。
用例:通常包含的是特定應(yīng)用場景下的業(yè)務(wù)邏輯沦零,這里面封裝并實現(xiàn)了整個系統(tǒng)的所有用例。這些用例引導(dǎo)了數(shù)據(jù)在業(yè)務(wù)實體之間的流入/流出货岭,并指揮著業(yè)務(wù)實體利用其中的關(guān)鍵業(yè)務(wù)邏輯來實現(xiàn)用例的設(shè)計目標(biāo)路操。
接口適配器:
通常是一組數(shù)據(jù)轉(zhuǎn)換器序攘,它們負(fù)責(zé)將數(shù)據(jù)從對用例和業(yè)務(wù)實體而言最方便操作的格式,轉(zhuǎn)換為外部系統(tǒng)(譬如數(shù)據(jù)庫以及web)和持久層框架最方便操作的格式寻拂。
如果采用的是SQL數(shù)據(jù)庫,那么所有的SQL語句都應(yīng)該被限制在這一層的代碼中丈牢。
這層也會負(fù)責(zé)將來自外部服務(wù)的數(shù)據(jù)轉(zhuǎn)換成系統(tǒng)內(nèi)用例和業(yè)務(wù)實體所需的格式
框架和驅(qū)動程序:最外層的模型層一般是由工具祭钉、數(shù)據(jù)庫、web框架等組成己沛。在這一層中慌核,我們通常只需要編寫一些于內(nèi)層溝通的黏合性代碼。
真正的架構(gòu)可能會超過四層申尼,但是源碼層面的依賴關(guān)系一定要指向同心圓的內(nèi)層垮卓。
通常采用依賴反轉(zhuǎn)原則(DIP)來解決控制流和依賴方向的相反性。
為了解決YAGNI原則(You aren't going to need it, 不要預(yù)測未來的需要)和架構(gòu)師工作本身(預(yù)見性設(shè)計)之間的矛盾庙曙,需要引入不完全邊界的概念。
幾種不完全邊界策略浩淘,除此外還有許多其他實現(xiàn)方式捌朴,根據(jù)不同的場景進(jìn)行選擇
方式一:將系統(tǒng)分割成一系列可以獨立編譯、獨立部署的組件之后张抄,再把它們構(gòu)建成一個組件砂蔽。這樣可以省去多組件管理這部分的工作,比如版本號管理和發(fā)布管理等等欣鳖。
方式二:單向邊界察皇。單側(cè)組件提供接口進(jìn)行隔離,而不是采用雙向反向接口泽台。
方式三:門戶模式
架構(gòu)師的職責(zé)之一就是預(yù)判未來哪里有可能需要設(shè)置架構(gòu)邊界什荣,并決定應(yīng)該以完全形式還是不完全形式來實現(xiàn)它們。
面向服務(wù)的“架構(gòu)”以及微服務(wù)“架構(gòu)”本身只是一種比函數(shù)調(diào)用方式成本稍高,分割應(yīng)用程序行為的一種形式辫秧,與系統(tǒng)架構(gòu)無關(guān)束倍。
服務(wù)的一些謬論
解耦合的謬論:服務(wù)強(qiáng)耦合于數(shù)據(jù)結(jié)構(gòu)
獨立開發(fā)的謬論:大型系統(tǒng)一樣可以采用單體模式,或者組件模式來構(gòu)建盟戏,不一定非得服務(wù)化绪妹。
系統(tǒng)的架構(gòu)是由系統(tǒng)內(nèi)部的架構(gòu)邊界,以及邊界之間的依賴關(guān)系所定義的柿究,與系統(tǒng)中各組件之間的調(diào)用和通信方式無關(guān)邮旷。
使用框架的風(fēng)險
框架自身的架構(gòu)設(shè)計很多時候并不是特別正確
隨著產(chǎn)品的成熟域庇,功能要求很可能超出框架所能提供的范圍
框架本身可能朝著我們不需要的方向演進(jìn)
未來我們可能會想要切換到一個更新、更好的框架上
框架作為架構(gòu)最外圈的一個實現(xiàn)細(xì)節(jié)來使用覆积,不要讓它們進(jìn)入內(nèi)圈
在一個執(zhí)行環(huán)境(應(yīng)用程序)中的、一個干凈吗冤、良好的接口背后的一系列相關(guān)功能的結(jié)合又厉。
與Bob大叔的定義稍有不同
組件是部署單元九府。組件是系統(tǒng)中能夠部署的最小單位,對應(yīng)在java里就是jar文件覆致。