由于自己身處SAAS行業(yè)慕蔚,在經(jīng)歷了幾輪復(fù)雜需求的蹂躪之后彼绷,我一直試圖尋找一種解法巍佑,可以盡量cover住復(fù)雜多變的需求。在過去的一年中寄悯,通過反復(fù)閱讀和實(shí)踐萤衰,似乎讓我對此有了一些清晰的思路,所以我想寫一點(diǎn)東西總結(jié)一下自己的這一年里的思考猜旬。
在我們的項(xiàng)目初期脆栋,項(xiàng)目的規(guī)模可能比較小洒擦,代碼量很少椿争,我們的代碼或許還能整理的比較干凈,就像這幾組交換機(jī)的網(wǎng)線一樣熟嫩,比較有條理秦踪。
但是隨著功能復(fù)雜之后,項(xiàng)目也隨之變得龐大,整個(gè)代碼就可能會和這個(gè)機(jī)房一樣椅邓,非常的混亂舍扰。在經(jīng)歷幾次這種狀況之后,于是我便在想希坚,究竟是什么問題導(dǎo)致了這種混亂边苹。
首先來看一段代碼(Kotlin Code
):
fun executeRequest(request: Request) : String {
// 校驗(yàn)身份
val isValidate = validateRequest(request)
if( isValidate ) {
return "Request is not valid"
}
// 處理業(yè)務(wù)
dealBiz(request)
try {
// 存儲數(shù)據(jù)
saveToDB(request)
} catch (exception:Exception) {
return "Occur error when save results to DB"
}
// 發(fā)送消息
val isSendSuccess = sendMessage(request)
if (isSendSuccess == false) {
return "message send unsuccessfully."
}
return "success"
}
這是我們比較常見的一些代碼結(jié)構(gòu),其實(shí)看起來問題也不算很大裁僧,但是隨著業(yè)務(wù)復(fù)雜个束,業(yè)務(wù)邏輯的控制和控制邏輯耦合的很厲害,閱讀這種"面條代碼"的成本越來越高聊疲。每一位新進(jìn)入項(xiàng)目的伙伴猶如進(jìn)入了一個(gè)“代碼迷宮”茬底。來來回回去尋找自己需要的那一段代碼,實(shí)際上這個(gè)時(shí)候已經(jīng)形成了:
只有上帝和我能看得懂的“上帝代碼”了获洲。
這顯然是我們不愿見到的代碼阱表,在左耳聽風(fēng)專欄里有一篇《編程的本質(zhì)》里講到:
有效地分離 Logic、Control 和 Data 是寫出好程序的關(guān)鍵所在贡珊。
那什么又是Logic
,Control
,Data
呢最爬?
- Logic : 就是一般的業(yè)務(wù)代碼,類似上面代碼中的
dealBiz()
,sendMessage()
等等 - Control : 對業(yè)務(wù)邏輯的流程控制门岔,比如遍歷數(shù)據(jù)爱致、查找數(shù)據(jù)、多線程寒随、并發(fā)糠悯、異步等等
- Data :函數(shù)和程序之間傳遞的這部分信息
如果有效地將這幾種代碼分離,代碼可讀性將會大大提升妻往。通過這種拆分互艾,我們也降低除了自己之外的維護(hù)者閱讀代碼翻譯業(yè)務(wù)內(nèi)容的成本。通過分離讯泣,我們可以將代碼寫成這樣:
fun executeRequest(request: Request) : String {
return Result
.of(request)
.flatMap { validateRequest(request) }
.flatMap { dealBiz(request) }
.flatMap { saveToDB(request) }
.flatMap { sendMessage(request) }
.fold(
success = { return "success" },
failure = { return it.message }
)
}
當(dāng)然這里說了如何寫細(xì)節(jié)的代碼纫普,那么代碼架構(gòu)又如何去做才可能保證可以應(yīng)對這么多的變化?
一般的項(xiàng)目中我們把一個(gè)軟件系統(tǒng)進(jìn)行分層判帮,這是我們目前做工程項(xiàng)目的一個(gè)共識局嘁,我們最初學(xué)習(xí)的分層架構(gòu)就是經(jīng)典的三層架構(gòu)了溉箕。它自頂向下分成三層:
- 用戶界面層(User Interface Layer)
- 業(yè)務(wù)邏輯層(Business Logic Layer)
- 數(shù)據(jù)訪問層(Data Access Layer)
到數(shù)據(jù)訪問層這塊晦墙,其實(shí)很多系統(tǒng)已經(jīng)變成了面向數(shù)據(jù)編程,最終做成了“數(shù)據(jù)庫管理系統(tǒng)”肴茄。按照傳統(tǒng)的三層模型晌畅,用戶界面的開發(fā)依賴Service層,而Service層又依賴著DAO寡痰,DAO對應(yīng)著數(shù)據(jù)庫抗楔。大家相互依賴棋凳,業(yè)務(wù)邏輯一旦修改,就意味著要從DAO層開始修改连躏,數(shù)據(jù)庫也跟著被修改剩岳,而往往隨著我們開發(fā)的深入,業(yè)務(wù)的模型會被不斷調(diào)整入热,這樣數(shù)據(jù)庫可能就要頻繁的變動拍棕。代碼也開始變得復(fù)雜... ...
而在領(lǐng)域驅(qū)動設(shè)計(jì)中提出了另外一種四層架構(gòu),在此之前勺良,我想先分享《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計(jì)》一書講的六邊形模型绰播。
我們在設(shè)計(jì)系統(tǒng)的時(shí)候,往往過于關(guān)注數(shù)據(jù)庫尚困,Http接口等基礎(chǔ)設(shè)施的設(shè)計(jì)蠢箩,而忽略了我們需要關(guān)注的業(yè)務(wù)。在復(fù)雜系統(tǒng)中事甜,最容易變化的也是業(yè)務(wù)形態(tài)谬泌,產(chǎn)品經(jīng)常會要求改來改去,因?yàn)闃I(yè)務(wù)本身就在不斷地演進(jìn)逻谦,如果我們一開始就基于數(shù)據(jù)庫作所有的設(shè)計(jì)呵萨,那么勢必一旦遇上業(yè)務(wù)的修改,庫表肯定也需要對應(yīng)先進(jìn)行變化跨跨。假如我們?nèi)谌肓呅渭軜?gòu)潮峦,將數(shù)據(jù)庫和暴露的Controller
都視為是基礎(chǔ)設(shè)施,先去關(guān)注業(yè)務(wù)的模型和代碼勇婴,Class
的修改比要數(shù)據(jù)庫改起來要簡單的多忱嘹。另外一方面,也大大提高了程序的可測試性:在沒有準(zhǔn)備一堆基礎(chǔ)設(shè)施(數(shù)據(jù)庫耕渴,接口拘悦,異步通知等等)情況下,可以先測試邏輯的完整性橱脸。
另外础米,有時(shí)候隨著業(yè)務(wù)增長有的基礎(chǔ)設(shè)施是會需要進(jìn)行替換的,采用六邊形架構(gòu)之后添诉,這種更換的成本就會降低屁桑。另外如果出現(xiàn)需要使用Web Service的客戶,我們也不必糾結(jié)于之前的HTTP接口栏赴,直接開出一套新的協(xié)議代碼供客戶使用蘑斧,而不會糾結(jié)領(lǐng)域部分代碼有邏輯上的缺失。
采用六邊形架構(gòu)之后,我們的領(lǐng)域模型也會更加獨(dú)立竖瘾,更精簡沟突,在適應(yīng)新的需求時(shí)修改也會更容易。在《架構(gòu)整潔之道》之中提到的“整潔架構(gòu)”也與“六邊形架構(gòu)”大同小異捕传。
其實(shí)這兩種架構(gòu)也是依賴倒置原則很好的實(shí)現(xiàn):
- 高層模塊不應(yīng)該依賴低層模塊惠拭,兩者都應(yīng)該依賴其抽象
- 抽象不應(yīng)該依賴細(xì)節(jié)
- 細(xì)節(jié)應(yīng)該依賴抽象
此時(shí)再回顧原來定義的四層架構(gòu):
- 用戶展示層
- 應(yīng)用服務(wù)層
- 領(lǐng)域?qū)?/li>
- 基礎(chǔ)設(shè)施層
他們的依賴關(guān)系如圖:
這樣我們可以將業(yè)務(wù)核心代碼放入領(lǐng)域?qū)?/code>之中,要應(yīng)對的各個(gè)場景代碼放入應(yīng)用服務(wù)層中庸论。將協(xié)議轉(zhuǎn)換求橄、中間件和數(shù)據(jù)庫的適配都放入基礎(chǔ)設(shè)施層里。在應(yīng)用層與
Controller
之間的那些VO
作為用戶展示層葡公,以做出整潔架構(gòu)罐农。
當(dāng)我們開始學(xué)習(xí)Java的時(shí)候,都知道Java是一門面向?qū)ο蟮恼Z言催什,我們本可以將現(xiàn)實(shí)世界翻譯到代碼的世界之中涵亏,但實(shí)際上我們往往在項(xiàng)目中只會將對象定義成貧血模型扬卷,最終寫成面向過程的代碼吨些。如何做才能讓這個(gè)復(fù)雜的世界反應(yīng)到代碼里呢?
讓我們再從需求說起连锯,對于一個(gè)復(fù)雜的軟件旋圆,任何一個(gè)項(xiàng)目的參與者(包括初創(chuàng)的成員)宠默,都很難靠自己就看清整個(gè)項(xiàng)目的全貌,我們猶如圖中的盲人灵巧,大家可能最后對項(xiàng)目的理解都是不一致的搀矫。此時(shí)每一位參與者都猶如“盲人摸象”中的“盲人”,對需求(大象)只有片面的理解刻肄,于是乎瓤球,有的人覺得大象是水管的形狀,有的人覺得大象是扇子一樣的形狀敏弃,有的人說大象長得跟柱子一樣... ...
通過討論卦羡,我們會對自我的認(rèn)知進(jìn)行一些修正,最終大致得出一個(gè)需求的全貌麦到。
比如我們要去識別一些系統(tǒng)邊界绿饵,在DDD的戰(zhàn)略設(shè)計(jì)中非常強(qiáng)調(diào)劃分界限上下文。比如我作為一個(gè)個(gè)體瓶颠,在不同的場景中拟赊,我的身份、角色都不大相同步清。
猶如在上圖中要门,在“地鐵”廓啊、“家庭”和“公司”中,我的身份是不一樣的炒瘟,但是我依舊是我,找出業(yè)務(wù)的場景第步,也就意味著我找到了系統(tǒng)的邊界。通過分析場景識別邊界來找出系統(tǒng)的核心領(lǐng)域和支撐領(lǐng)域廓推,以此來最終確定系統(tǒng)的數(shù)量,降低系統(tǒng)的耦合翩隧。
我們還可以用四色建模方法來識別出我們系統(tǒng)中發(fā)生的整個(gè)流程,發(fā)現(xiàn)究竟是誰通過什么方式觸發(fā)了什么事情专缠,最終又影響了哪些對象淑仆。
最終通過我們找出的事件蔗怠,整理出一個(gè)能夠讓我們進(jìn)行溝通的模型寞射。在我們的模型被構(gòu)建得相對完善之時(shí)怠惶,其實(shí)代碼也差不多已經(jīng)被構(gòu)建出來了策治,因?yàn)檫@個(gè)時(shí)候再去回想面向?qū)ο?/strong>的設(shè)計(jì),我們發(fā)現(xiàn)模型即代碼,代碼即模型珊燎。
一直以來我都希望通過一些好的“工程實(shí)踐”來提高團(tuán)隊(duì)的效率以及我們的代碼質(zhì)量悔政,我想這也是思考這些架構(gòu)的意義吧谋国。我想用《架構(gòu)整潔之道》中的一句話來做本文的總結(jié):
軟件架構(gòu)的最終目標(biāo)是芦瘾,用最小的人力成本來滿足構(gòu)建和維護(hù)系統(tǒng)的需求。
正如《人月神話》里說的一樣缅糟,軟件工程里沒有“銀彈”祷愉,即使做了整潔架構(gòu)也無法避免需求的變化和延期谣辞,只是希望當(dāng)我們身處需求的困境中時(shí),仍能給自己以更多的選擇句占。
參考資料&活動 :
- 《領(lǐng)域驅(qū)動設(shè)計(jì)》
- 《架構(gòu)整潔之道》
- 《實(shí)現(xiàn)領(lǐng)域驅(qū)動設(shè)計(jì)》
- 從三明治到六邊形架構(gòu) http://insights.thoughtworkers.org/from-sandwich-to-hexagon/
- 運(yùn)用四色建模法進(jìn)行領(lǐng)域分析 https://www.infoq.cn/articles/xh-four-color-modeling
- 領(lǐng)域驅(qū)動戰(zhàn)略設(shè)計(jì)實(shí)踐 https://gitbook.cn/gitchat/column/5b3235082ab5224deb750e02
- 左耳聽風(fēng) https://time.geekbang.org/column/48
- 領(lǐng)域驅(qū)動設(shè)計(jì)中國峰會DDD-China