架構(gòu)整潔之道,看這一篇就夠了!
本篇文章我們將從軟件系統(tǒng)的價(jià)值出發(fā),首先認(rèn)識(shí)架構(gòu)工作的價(jià)值和目標(biāo), 接下來(lái)依次了解架構(gòu)設(shè)計(jì)的基礎(chǔ)、指導(dǎo)思想(設(shè)計(jì)原則)、組件拆分的方法和粒度、組件之間依賴設(shè)計(jì)、組件邊界多種解耦方式以及取舍、降低組件之間通信成本的方法,從而最終指導(dǎo)我們做出正確的架構(gòu)決策和架構(gòu)設(shè)計(jì)。
一、軟件系統(tǒng)的價(jià)值
架構(gòu)是軟件系統(tǒng)的一部分,所以要明白架構(gòu)的價(jià)值,首先要明確軟件系統(tǒng)的價(jià)值。軟件系統(tǒng)的價(jià)值有兩方面,行為價(jià)值和架構(gòu)價(jià)值。
行為價(jià)值是軟件的核心價(jià)值,包括需求的實(shí)現(xiàn),以及可用性保障(功能性 bug 、性能、穩(wěn)定性)。這幾乎占據(jù)了我們90%的工作內(nèi)容,支撐業(yè)務(wù)先贏是我們工程師的首要責(zé)任。如果業(yè)務(wù)是明確的、穩(wěn)定的,架構(gòu)的價(jià)值就可以忽略不計(jì),但業(yè)務(wù)通常是不明確的、飛速發(fā)展的,這時(shí)架構(gòu)就無(wú)比重要,因?yàn)榧軜?gòu)的價(jià)值就是讓我們的軟件(Software)更軟(Soft)??梢詮膬煞矫胬斫猓?/p>
- 當(dāng)需求變更時(shí),所需的軟件變更必須簡(jiǎn)單方便。
- 變更實(shí)施的難度應(yīng)該和變更的范疇(scope)成等比,而與變更的具體形狀(shape)無(wú)關(guān)。
當(dāng)我們只關(guān)注行為價(jià)值,不關(guān)注架構(gòu)價(jià)值時(shí),會(huì)發(fā)生什么事情?這是書中記錄的一個(gè)真實(shí)案例,隨著版本迭代,工程師團(tuán)隊(duì)的規(guī)模持續(xù)增長(zhǎng),但總代碼行數(shù)卻趨于穩(wěn)定,相對(duì)應(yīng)的,每行代碼的變更成本升高、工程師的生產(chǎn)效率降低。從老板的視角,就是公司的成本增長(zhǎng)迅猛,如果營(yíng)收跟不上就要開始賠錢啦。
可見(jiàn)架構(gòu)價(jià)值重要性,接下來(lái)從著名的緊急重要矩陣出發(fā),看我們?nèi)绾翁幚砗眯袨閮r(jià)值和架構(gòu)價(jià)值的關(guān)系。
重要緊急矩陣中,做事的順序是這樣的:1.重要且緊急 > 2.重要不緊急 > 3.不重要但緊急 > 4.不重要且不緊急。實(shí)現(xiàn)行為價(jià)值的需求通常是 PD 提出的,都比較緊急,但并不總是特別重要;架構(gòu)價(jià)值的工作內(nèi)容,通常是開發(fā)同學(xué)提出的,都很重要但基本不是很緊急,短期內(nèi)不做也死不了。所以行為價(jià)值的事情落在1和3(重要且緊急、不重要但緊急),而架構(gòu)價(jià)值落在2(重要不緊急)。我們開發(fā)同學(xué),在低頭敲代碼之前,一定要把雜糅在一起的1和3分開,把我們架構(gòu)工作插進(jìn)去。
二、架構(gòu)工作的目標(biāo)
前面講解了架構(gòu)價(jià)值,追求架構(gòu)價(jià)值就是架構(gòu)工作的目標(biāo),說(shuō)白了,就是用最少的人力成本滿足構(gòu)建和維護(hù)該系統(tǒng)的需求,再細(xì)致一些,就是支撐軟件系統(tǒng)的全生命周期,讓系統(tǒng)便于理解、易于修改、方便維護(hù)、輕松部署。對(duì)于生命周期里的每個(gè)環(huán)節(jié),優(yōu)秀的架構(gòu)都有不同的追求:
- 開發(fā)階段:組件不要使用大量復(fù)雜的腳手架;不同團(tuán)隊(duì)負(fù)責(zé)不同的組件,避免不必要的協(xié)作。
- 部署階段:部署工作不要依賴成堆的腳本和配置文件;組件越多部署工作越繁重,而部署工作本身是沒(méi)有價(jià)值的,做的越少越好,所以要減少組件數(shù)量。
- 運(yùn)行階段:架構(gòu)設(shè)計(jì)要考慮到不同的吞吐量、不同的響應(yīng)時(shí)長(zhǎng)要求;架構(gòu)應(yīng)起到揭示系統(tǒng)運(yùn)行的作用:用例、功能、行為設(shè)置應(yīng)該都是對(duì)開發(fā)者可見(jiàn)的一級(jí)實(shí)體,以類、函數(shù)或模塊的形式占據(jù)明顯位置,命名能清晰地描述對(duì)應(yīng)的功能。
- 維護(hù)階段:減少探秘成本和風(fēng)險(xiǎn)。探秘成本是對(duì)現(xiàn)有軟件系統(tǒng)的挖掘工作,確定新功能或修復(fù)問(wèn)題的***位置和方式。風(fēng)險(xiǎn)是做改動(dòng)時(shí),可能衍生出新的問(wèn)題。
三、編程范式
其實(shí)所謂架構(gòu)就是限制,限制源碼放在哪里、限制依賴、限制通信的方式,但這些限制比較上層。編程范式是最基礎(chǔ)的限制,它限制我們的控制流和數(shù)據(jù)流:結(jié)構(gòu)化編程限制了控制權(quán)的直接轉(zhuǎn)移,面向?qū)ο缶幊滔拗屏丝刂茩?quán)的間接轉(zhuǎn)移,函數(shù)式編程限制了賦值,相信你看到這里一定一臉懵逼,啥叫控制權(quán)的直接轉(zhuǎn)移,啥叫控制權(quán)的間接轉(zhuǎn)移,不要著急,后邊詳細(xì)講解。
這三個(gè)編程范式最近的一個(gè)也有半個(gè)世紀(jì)的歷史了,半個(gè)世紀(jì)以來(lái)沒(méi)有提出新的編程范式,以后可能也不會(huì)了。因?yàn)榫幊谭妒降囊饬x在于限制,限制了控制權(quán)轉(zhuǎn)移限制了數(shù)據(jù)賦值,其他也沒(méi)啥可限制的了。很有意思的是,這三個(gè)編程范式提出的時(shí)間順序可能與大家的直覺(jué)相反,從前到后的順序?yàn)椋汉瘮?shù)式編程(1936年)、面向?qū)ο缶幊?1966年)、結(jié)構(gòu)化編程(1968年)。
1.結(jié)構(gòu)化編程
結(jié)構(gòu)化編程證明了人們可以用順序結(jié)構(gòu)、分支結(jié)構(gòu)、循環(huán)結(jié)構(gòu)這三種結(jié)構(gòu)構(gòu)造出任何程序,并限制了 goto 的使用。遵守結(jié)構(gòu)化編程,工程師就可以像數(shù)學(xué)家一樣對(duì)自己的程序進(jìn)行推理證明,用代碼將一些已證明可用的結(jié)構(gòu)串聯(lián)起來(lái),只要自行證明這些額外代碼是確定的,就可以推導(dǎo)出整個(gè)程序的正確性。
前面提到結(jié)構(gòu)化編程對(duì)控制權(quán)的直接轉(zhuǎn)移進(jìn)行了限制,其實(shí)就是限制了 goto 語(yǔ)句。什么叫做控制權(quán)的直接轉(zhuǎn)移?就是函數(shù)調(diào)用或者 goto 語(yǔ)句,代碼在原來(lái)的流程里不繼續(xù)執(zhí)行了,轉(zhuǎn)而去執(zhí)行別的代碼,并且你指明了執(zhí)行什么代碼。為什么要限制 goto 語(yǔ)句?因?yàn)?goto 語(yǔ)句的一些用法會(huì)導(dǎo)致某個(gè)模塊無(wú)法被遞歸拆分成更小的、可證明的單元。而采用分解法將大型問(wèn)題拆分正是結(jié)構(gòu)化編程的核心價(jià)值。
其實(shí)遵守結(jié)構(gòu)化編程,工程師們也無(wú)法像數(shù)學(xué)家那樣證明自己的程序是正確的,只能像物理學(xué)家一樣,說(shuō)自己的程序暫時(shí)沒(méi)被證偽(沒(méi)被找到bug)。數(shù)學(xué)公式和物理公式的***區(qū)別,就是數(shù)學(xué)公式可被證明,而物理公式無(wú)法被證明,只要目前的實(shí)驗(yàn)數(shù)據(jù)沒(méi)把它證偽,我們就認(rèn)為它是正確的。程序也是一樣,所有的 test case 都通過(guò)了,沒(méi)發(fā)現(xiàn)問(wèn)題,我們就認(rèn)為這段程序是正確的。
2.面向?qū)ο缶幊?/strong>
面向?qū)ο缶幊贪ǚ庋b、繼承和多態(tài),從架構(gòu)的角度,這里只關(guān)注多態(tài)。多態(tài)讓我們更方便、安全地通過(guò)函數(shù)調(diào)用的方式進(jìn)行組件間通信,它也是依賴反轉(zhuǎn)(讓依賴與控制流方向相反)的基礎(chǔ)。
在非面向?qū)ο蟮木幊陶Z(yǔ)言中,我們?nèi)绾卧诨ハ嘟怦畹慕M件間實(shí)現(xiàn)函數(shù)調(diào)用?答案是函數(shù)指針。比如采用C語(yǔ)言編寫的操作系統(tǒng)中,定義了如下的結(jié)構(gòu)體來(lái)解耦具體的IO設(shè)備, IO 設(shè)備的驅(qū)動(dòng)程序只需要把函數(shù)指針指到自己的實(shí)現(xiàn)就可以了。
struct FILE { void (*open)(char* name, int mode); void (*close)(); int (*read)(); void (*write)(char); void (*seek)(long index, int mode);}
這種通過(guò)函數(shù)指針進(jìn)行組件間通信的方式非常脆弱,工程師必須嚴(yán)格按照約定初始化函數(shù)指針,并嚴(yán)格地按照約定來(lái)調(diào)用這些指針,只要一個(gè)人沒(méi)有遵守約定,整個(gè)程序都會(huì)產(chǎn)生極其難以跟蹤和消除的 Bug。所以面向?qū)ο缶幊滔拗屏撕瘮?shù)指針的使用,通過(guò)接口-實(shí)現(xiàn)、抽象類-繼承等多態(tài)的方式來(lái)替代。
前面提到面向?qū)ο缶幊虒?duì)控制權(quán)的間接轉(zhuǎn)移進(jìn)行了限制,其實(shí)就是限制了函數(shù)指針的使用。什么叫做控制權(quán)的間接轉(zhuǎn)移?就是代碼在原來(lái)的流程里不繼續(xù)執(zhí)行了,轉(zhuǎn)而去執(zhí)行別的代碼,但具體執(zhí)行了啥代碼你也不知道,你只調(diào)了個(gè)函數(shù)指針或者接口。
3.函數(shù)式編程
函數(shù)式編程有很多種定義很多種特性,這里從架構(gòu)的角度,只關(guān)注它的沒(méi)有副作用和不修改狀態(tài)。函數(shù)式編程中,函數(shù)要保持獨(dú)立,所有功能就是返回一個(gè)新的值,沒(méi)有其他行為,尤其是不得修改外部變量的值。前面提到函數(shù)式編程對(duì)賦值進(jìn)行了限制,指的就是這個(gè)特性。
在架構(gòu)領(lǐng)域所有的競(jìng)爭(zhēng)問(wèn)題、死鎖問(wèn)題、并發(fā)問(wèn)題都是由可變變量導(dǎo)致的。如果有足夠大的存儲(chǔ)量和計(jì)算量,應(yīng)用程序可以用事件溯源的方式,用完全不可變的函數(shù)式編程,只通過(guò)事務(wù)記錄從頭計(jì)算狀態(tài),就避免了前面提到的幾個(gè)問(wèn)題。目前要讓一個(gè)軟件系統(tǒng)完全沒(méi)有可變變量是不現(xiàn)實(shí)的,但是我們可以通過(guò)將需要修改狀態(tài)的部分和不需要修改的部分分隔成單獨(dú)的組件,在不需要修改狀態(tài)的組件中使用函數(shù)式編程,提高系統(tǒng)的穩(wěn)定性和效率。
綜上,沒(méi)有結(jié)構(gòu)化編程,程序就無(wú)法從一塊塊可證偽的邏輯搭建,沒(méi)有面向?qū)ο缶幊?,跨越組件邊界會(huì)是一個(gè)非常麻煩而危險(xiǎn)的過(guò)程,而函數(shù)式編程,讓組件更加高效而穩(wěn)定。沒(méi)有編程范式,架構(gòu)設(shè)計(jì)將無(wú)從談起。
四、設(shè)計(jì)原則
和編程范式相比,設(shè)計(jì)原則和架構(gòu)的關(guān)系更加緊密,設(shè)計(jì)原則就是架構(gòu)設(shè)計(jì)的指導(dǎo)思想,它指導(dǎo)我們?nèi)绾螌?shù)據(jù)和函數(shù)組織成類,如何將類鏈接起來(lái)成為組件和程序。反向來(lái)說(shuō),架構(gòu)的主要工作就是將軟件拆解為組件,設(shè)計(jì)原則指導(dǎo)我們?nèi)绾尾鸾?、拆解的粒度、組件間依賴的方向、組件解耦的方式等。
設(shè)計(jì)原則有很多,我們進(jìn)行架構(gòu)設(shè)計(jì)的主導(dǎo)原則是 OCP(開閉原則),在類和代碼的層級(jí)上有:SRP(單一職責(zé)原則)、LSP(里氏替換原則)、ISP(接口隔離原則)、DIP(依賴反轉(zhuǎn)原則);在組件的層級(jí)上有:REP(復(fù)用、發(fā)布等同原則)、 CCP(共同閉包原則)、CRP(共同復(fù)用原則),處理組件依賴問(wèn)題的三原則:無(wú)依賴環(huán)原則、穩(wěn)定依賴原則、穩(wěn)定抽象原則。
1.OCP(開閉原則)
設(shè)計(jì)良好的軟件應(yīng)該易于擴(kuò)展,同時(shí)抗拒修改。這是我們進(jìn)行架構(gòu)設(shè)計(jì)的主導(dǎo)原則,其他的原則都為這條原則服務(wù)。
2.SRP(單一職責(zé)原則)
任何一個(gè)軟件模塊,都應(yīng)該有且只有一個(gè)被修改的原因,“被修改的原因“指系統(tǒng)的用戶或所有者,翻譯一下就是,任何模塊只對(duì)一個(gè)用戶的價(jià)值負(fù)責(zé)。該原則指導(dǎo)我們?nèi)绾尾鸱纸M件。
舉個(gè)例子,CTO 和 COO 都要統(tǒng)計(jì)員工的工時(shí),當(dāng)前他們要求的統(tǒng)計(jì)方式可能是相同的,我們復(fù)用一套代碼,這時(shí) COO 說(shuō)周末的工時(shí)統(tǒng)計(jì)要乘以二,按照這個(gè)需求修改完代碼,CTO 可能就要過(guò)來(lái)罵街了。當(dāng)然這是個(gè)非常淺顯的例子,實(shí)際項(xiàng)目中也有很多代碼服務(wù)于多個(gè)價(jià)值主體,這帶來(lái)很大的探秘成本和修改風(fēng)險(xiǎn)。
另外當(dāng)一份代碼有多個(gè)所有者時(shí),就會(huì)產(chǎn)生代碼合并沖突的問(wèn)題。
3.LSP(里氏替換原則)
當(dāng)用同一接口的不同實(shí)現(xiàn)互相替換時(shí),系統(tǒng)的行為應(yīng)該保持不變。該原則指導(dǎo)的是接口與其實(shí)現(xiàn)方式。
你一定很疑惑,實(shí)現(xiàn)了同一個(gè)接口,他們的行為也肯定是一致的呀,還真不一定。假設(shè)認(rèn)為矩形的系統(tǒng)行為是:面積=寬*高,讓正方形實(shí)現(xiàn)矩形的接口,在調(diào)用 setW 和 setH 時(shí),正方形做的其實(shí)是同一個(gè)事情,設(shè)置它的邊長(zhǎng)。這時(shí)下邊的單元測(cè)試用矩形能通過(guò),用正方形就不行,實(shí)現(xiàn)同樣的接口,但是系統(tǒng)行為變了,這是違反 LSP 的經(jīng)典案例。
Rectangle r = ...r.setW(5);r.setH(2);assert(r.area() == 10);
4.ISP(接口隔離原則)
不依賴任何不需要的方法、類或組件。該原則指導(dǎo)我們的接口設(shè)計(jì)。
當(dāng)我們依賴一個(gè)接口但只用到了其中的部分方法時(shí),其實(shí)我們已經(jīng)依賴了不需要的方法或類,當(dāng)這些方法或類有變更時(shí),會(huì)引起我們類的重新編譯,或者引起我們組件的重新部署,這些都是不必要的。所以我們***定義個(gè)小接口,把用到的方法拆出來(lái)。
5.DIP(依賴反轉(zhuǎn)原則)
跨越組建邊界的依賴方向永遠(yuǎn)與控制流的方向相反。該原則指導(dǎo)我們?cè)O(shè)計(jì)組件間依賴的方向。
依賴反轉(zhuǎn)原則是個(gè)可操作性非常強(qiáng)的原則,當(dāng)你要修改組件間的依賴方向時(shí),將需要進(jìn)行組件間通信的類抽象為接口,接口放在邊界的哪邊,依賴就指向哪邊。
6.REP(復(fù)用、發(fā)布等同原則)
軟件復(fù)用的最小粒度應(yīng)等同于其發(fā)布的最小粒度。直白地說(shuō),就是要復(fù)用一段代碼就把它抽成組件。該原則指導(dǎo)我們組件拆分的粒度。
7.CCP(共同閉包原則)
為了相同目的而同時(shí)修改的類,應(yīng)該放在同一個(gè)組件中。CCP 原則是 SRP 原則在組件層面的描述。該原則指導(dǎo)我們組件拆分的粒度。
對(duì)大部分應(yīng)用程序而言,可維護(hù)性的重要性遠(yuǎn)遠(yuǎn)大于可復(fù)用性,由同一個(gè)原因引起的代碼修改,***在同一個(gè)組件中,如果分散在多個(gè)組件中,那么開發(fā)、提交、部署的成本都會(huì)上升。
8.CRP(共同復(fù)用原則)
不要強(qiáng)迫一個(gè)組件依賴它不需要的東西。CRP 原則是 ISP 原則在組件層面的描述。該原則指導(dǎo)我們組件拆分的粒度。
相信你一定有這種經(jīng)歷,集成了組件A,但組件A依賴了組件B、C。即使組件B、C 你完全用不到,也不得不集成進(jìn)來(lái)。這是因?yàn)槟阒挥玫搅私M件A的部分能力,組件A中額外的能力帶來(lái)了額外的依賴。如果遵循共同復(fù)用原則,你需要把A拆分,只保留你要用的部分。
REP、CCP、CRP 三個(gè)原則之間存在彼此競(jìng)爭(zhēng)的關(guān)系,REP 和 CCP 是黏合性原則,它們會(huì)讓組件變得更大,而 CRP 原則是排除性原則,它會(huì)讓組件變小。遵守REP、CCP 而忽略 CRP ,就會(huì)依賴了太多沒(méi)有用到的組件和類,而這些組件或類的變動(dòng)會(huì)導(dǎo)致你自己的組件進(jìn)行太多不必要的發(fā)布;遵守 REP 、CRP 而忽略 CCP,因?yàn)榻M件拆分的太細(xì)了,一個(gè)需求變更可能要改n個(gè)組件,帶來(lái)的成本也是巨大的。
優(yōu)秀的架構(gòu)師應(yīng)該能在上述三角形張力區(qū)域中定位一個(gè)最適合目前研發(fā)團(tuán)隊(duì)狀態(tài)的位置,例如在項(xiàng)目早期,CCP比REP更重要,隨著項(xiàng)目的發(fā)展,這個(gè)最合適的位置也要不停調(diào)整。
9.無(wú)依賴環(huán)原則
健康的依賴應(yīng)該是個(gè)有向無(wú)環(huán)圖(DAG),互相依賴的組件,實(shí)際上組成了一個(gè)大組件,這些組件要一起發(fā)布、一起做單元測(cè)試。我們可以通過(guò)依賴反轉(zhuǎn)原則 DIP 來(lái)解除依賴環(huán)。
10.穩(wěn)定依賴原則
依賴必須要指向更穩(wěn)定的方向。
這里組件的穩(wěn)定性指的是它的變更成本,和它變更的頻繁度沒(méi)有直接的關(guān)聯(lián)(變更的頻繁程度與需求的穩(wěn)定性更加相關(guān))。影響組件的變更成本的因素有很多,比如組件的代碼量大小、復(fù)雜度、清晰度等等,最最重要的因素是依賴它的組件數(shù)量,讓組件難以修改的一個(gè)最直接的辦法就是讓很多其他組件依賴于它!
組件穩(wěn)定性的定量化衡量指標(biāo)是:不穩(wěn)定性(I) = 出向依賴數(shù)量 / (入向依賴數(shù)量 + 出向依賴數(shù)量)。如果發(fā)現(xiàn)違反穩(wěn)定依賴原則的地方,解決的辦法也是通過(guò) DIP 來(lái)反轉(zhuǎn)依賴。
11.穩(wěn)定抽象原則
一個(gè)組件的抽象化程度應(yīng)該與其穩(wěn)定性保持一致。為了防止高階架構(gòu)設(shè)計(jì)和高階策略難以修改,通常抽象出穩(wěn)定的接口、抽象類為單獨(dú)的組件,讓具體實(shí)現(xiàn)的組件依賴于接口組件,這樣它的穩(wěn)定性就不會(huì)影響它的擴(kuò)展性。
組件抽象化程度的定量化描述是:抽象程度(A)= 組件中抽象類和接口的數(shù)量 / 組件中類的數(shù)量。
將不穩(wěn)定性(I)作為橫軸,抽象程度(A)作為縱軸,那么最穩(wěn)定、只包含抽象類和接口的組件應(yīng)該位于左上角(0,1),最不穩(wěn)定、只包含具體實(shí)現(xiàn)類,沒(méi)有任何接口的組件應(yīng)該位于右下角(1,0),他們連線就是主序列線,位于線上的組件,他們的穩(wěn)定性和抽象程度相匹配,是設(shè)計(jì)良好的組件。位于(0,0)周圍區(qū)域的組件,它們是非常穩(wěn)定(注意這里的穩(wěn)定指的是變更成本)并且非常具體的組件,因?yàn)樗麄兊某橄蟪潭鹊停瑳Q定了他們經(jīng)常改動(dòng)的命運(yùn),但是又有許多其他組件依賴他們,改起來(lái)非常痛苦,所以這個(gè)區(qū)域叫做痛苦區(qū)。右上角區(qū)域的組件,沒(méi)有其他組件依賴他們,他們自身的抽象程度又很高,很有可能是陳年的老代碼,所以這個(gè)區(qū)域叫做無(wú)用區(qū)。
另外,可以用點(diǎn)距離主序列線的距離 Z 來(lái)表示組件是否遵循穩(wěn)定抽象原則,Z 越大表示組件越違背穩(wěn)定依賴原則。
五、架構(gòu)工作的基本方針
了解了編程范式和設(shè)計(jì)原則,接下來(lái)我們看看如何應(yīng)用他們拆分組件、處理組件依賴和組件邊界。架構(gòu)工作有兩個(gè)方針:
盡可能長(zhǎng)時(shí)間地保留盡可能多的可選項(xiàng)。這里的可選項(xiàng)指的是無(wú)關(guān)緊要的細(xì)節(jié)設(shè)計(jì),比如具體選用哪個(gè)存儲(chǔ)方式、哪種數(shù)據(jù)庫(kù),或者采用哪種 Web 框架。業(yè)務(wù)代碼要和這些可選項(xiàng)解耦,數(shù)據(jù)庫(kù)或者框架應(yīng)該做到像插件一樣切換,業(yè)務(wù)層對(duì)這個(gè)切換的過(guò)程應(yīng)該做到完全無(wú)感。
低層次解耦方式能解決的,不要用高層次解耦方式。組件之間的解耦方式后邊細(xì)講,這里強(qiáng)調(diào)的是邊界處理越完善,開發(fā)和部署成本越高。所以不完全邊界能解決的,不要用完全邊界,低層次解耦能解決的,不要用高層次解耦。
六、組件拆分
首先要給組件下個(gè)定義:組件是一組描述如何將輸入轉(zhuǎn)化為輸出的策略語(yǔ)句的集合,在同一個(gè)組件中,策略的變更原因、時(shí)間、層次相同。
從定義就可以看出,組件拆分需要在兩個(gè)維度進(jìn)行:按層次拆分、按變更原因拆分。
這里的變更原因就是業(yè)務(wù)用例,按變更原因進(jìn)行組件拆分的例子是:訂單組件、聊天組件。按層次拆分,可以拆為:業(yè)務(wù)實(shí)體、用例、接口適配器、框架與驅(qū)動(dòng)程序。
業(yè)務(wù)實(shí)體:關(guān)鍵業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)邏輯的集合,與界面無(wú)關(guān)、與存儲(chǔ)無(wú)關(guān)、與框架無(wú)關(guān),只有業(yè)務(wù)邏輯。
用例:特定場(chǎng)景下的業(yè)務(wù)邏輯,可以理解為 輸入 + 業(yè)務(wù)實(shí)體 + 輸出 = 用例。
接口適配器:包含整個(gè)整個(gè)MVC,以及對(duì)存儲(chǔ)、設(shè)備、界面等的接口聲明和使用。
一條策略距離系統(tǒng)的輸入、輸出越遠(yuǎn),它的層次越高,所以業(yè)務(wù)實(shí)體是***的層,框架與驅(qū)動(dòng)程序是***的層。
七、組件依賴處理
前面拆好了組件分好了層,依賴就很好處理了:依賴關(guān)系與數(shù)據(jù)流控制流脫鉤,而與組件所在層次掛鉤,始終從低層次指向高層次,如下圖。越具體的策略處在的層級(jí)越低,越插件化。切換數(shù)據(jù)庫(kù)是框架驅(qū)動(dòng)層的事情,接口適配器完全無(wú)感知,切換展示器是接口適配器層面的事情,用例完全無(wú)感知,而切換用例也不會(huì)影響到業(yè)務(wù)實(shí)體。
八、組件邊界處理
一個(gè)完整的組件邊界包括哪些內(nèi)容?首先跨越組件邊界進(jìn)行通信的兩個(gè)類都要抽象為接口,另外需要聲明專用的輸入數(shù)據(jù)模型、聲明專用的返回?cái)?shù)據(jù)模型,想一想每次進(jìn)行通信時(shí)都要進(jìn)行的數(shù)據(jù)模型轉(zhuǎn)換,就能理解維護(hù)一個(gè)組件邊界的成本有多高。
除非必要,我們應(yīng)該盡量使用不完全邊界來(lái)降低維護(hù)組件邊界的成本。不完全邊界有三種方式:
省掉***一步:聲明好接口,做好分割后,仍然放在一個(gè)組件中,等到時(shí)機(jī)成熟時(shí)再拆出來(lái)獨(dú)立編譯部署。
單向邊界:正常的邊際至少有兩個(gè)接口,分別抽象調(diào)用方和被調(diào)用方。這里只定義一個(gè)接口,高層次組件用接口調(diào)用低層次組件,而低層次組件直接引用高層次組件的類。
門戶模式:控制權(quán)的間接轉(zhuǎn)移不用接口和實(shí)現(xiàn)去做,而是用門戶類去做,用這種方式連接口都不用聲明了。
除了完全邊界和不完全邊界的區(qū)分,邊界的解耦方式也可以分為3個(gè)層次:
源碼層次:做了接口、類依賴上的解耦,但是放在同一個(gè)組件中,通常放在不同的路徑下。和不完全邊界的省略***一步一樣。
部署層次:拆分為可以獨(dú)立部署的不同組件,比如 iOS 的靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù),真正運(yùn)行時(shí)處于同一臺(tái)物理機(jī)器上,組件之間通常通過(guò)函數(shù)調(diào)用通訊。
服務(wù)層次:運(yùn)行在不同的機(jī)器上,通過(guò) url 、網(wǎng)絡(luò)數(shù)據(jù)包等方式進(jìn)行通訊。
從上到下,(開發(fā)、部署)成本依次升高,如果低層次的解耦已經(jīng)滿足需要,不要進(jìn)行高層次的解耦。