降低復(fù)雜度提升效率,DDD在攜程用車/租車訂單系統(tǒng)重構(gòu)中的實踐
隨著歷史業(yè)務(wù)不斷迭代和業(yè)務(wù)場景越來越復(fù)雜,攜程用車、租車(簡稱兩車)面臨歷史技術(shù)債和系統(tǒng)復(fù)雜度越來越高帶來的理解、維護、迭代困難等問題,我們開始尋求如何更有效的降低復(fù)雜度和提升效率的方法。
本文描述了兩車如何利用DDD(Domain-driven Design,領(lǐng)域驅(qū)動設(shè)計)方法論降低系統(tǒng)復(fù)雜度以及在重構(gòu)歷史系統(tǒng)中的取舍和思考。對于復(fù)雜業(yè)務(wù)場景下的領(lǐng)域驅(qū)動設(shè)計具有借鑒意義。
一、案例介紹
攜程用車訂單相關(guān)業(yè)務(wù)包括接送機、包車、打車這些產(chǎn)線,訂單相關(guān)的功能包括訂單狀態(tài)管理、支付狀態(tài)管理、供應(yīng)商訂單狀態(tài)管理、履約狀態(tài)管理,其中履約狀態(tài)中包含司機相關(guān)狀態(tài),完成訂單需要將額外費用結(jié)清。
攜程租車訂單相關(guān)功能包括訂單狀態(tài)管理、支付狀態(tài)管理、押金扣款記錄、供應(yīng)商訂單狀態(tài)管理、履約狀態(tài)管理,其中履約狀態(tài)主要是取車和還車相關(guān)狀態(tài)。
訂單和相關(guān)實體如下圖所示:
二、問題分析
由于兩車業(yè)務(wù)存在一些差異,為了讀者更容易理解,因此將抽取共性問題來說明。
2.1 溝通困難
關(guān)于溝通困難,我們發(fā)現(xiàn)整個開發(fā)過程中,溝通實際上是一個非常消耗時間的事情,需求方需要和產(chǎn)品溝通,產(chǎn)品要和研發(fā)人員溝通,研發(fā)開發(fā)過程中發(fā)現(xiàn)一些忽略的細(xì)節(jié)需要產(chǎn)品確認(rèn),來回之間耗費了大量時間。如果是跨團隊溝通,這樣的問題會更加復(fù)雜,以下總結(jié)了一些常見的場景:
- 產(chǎn)品不關(guān)心研發(fā)的實現(xiàn),但是覺得需求很簡單或者很復(fù)雜。
- 研發(fā)開發(fā)過程中發(fā)現(xiàn)一些忽略的細(xì)節(jié)需要產(chǎn)品確認(rèn),產(chǎn)品要找需求方確認(rèn)。
- 歷史邏輯沒人知道,需求評審的時候無法發(fā)現(xiàn)問題,做到最后發(fā)現(xiàn)有問題。
- 跨團隊之間不了解對方的業(yè)務(wù),需要反復(fù)溝通確認(rèn)。
- 遇到同一個名詞不同的理解導(dǎo)致無效溝通。
- 一個需求到底該哪個域來實現(xiàn)是我們在實踐中經(jīng)常反復(fù)探討的問題。
- ...
例如訂單和供應(yīng)商訂單在不同的團隊內(nèi)都叫訂單,在溝通中針對“訂單”的討論就會產(chǎn)生歧義。
2.2 業(yè)務(wù)邊界不清晰
設(shè)計之初,訂單被各調(diào)用方當(dāng)作了對外輸出的數(shù)據(jù)源頭,數(shù)據(jù)需求方只要調(diào)訂單詳情即可獲取全量數(shù)據(jù),這為以后訂單的迭代帶來了相當(dāng)大的隱患。訂單在自己的業(yè)務(wù)模型中加入大量不涉及自身業(yè)務(wù)的冗余字段,在系統(tǒng)的演進過程中,由于無腦插入他方業(yè)務(wù)字段使得訂單自己也要維護相關(guān)的邏輯(解釋和修改),導(dǎo)致各方對訂單的耦合日益加深,導(dǎo)致訂單服務(wù)的發(fā)布變成高風(fēng)險行為,甚至一個無關(guān)訂單業(yè)務(wù)的相關(guān)字段修改也可能導(dǎo)致系統(tǒng)故障。
例如訂單上關(guān)于供應(yīng)商的相關(guān)數(shù)據(jù),用戶訂單有一份,采購訂單也有一份,當(dāng)采購要修改供應(yīng)商的相關(guān)邏輯時要用戶訂單也一起修改,而用戶訂單必須排查和推動相關(guān)使用到這個字段的業(yè)務(wù)方切換替代方案。
2.3 面對業(yè)務(wù)變化修改困難
隨著歷史業(yè)務(wù)迭代,訂單中耦合了許多非訂單關(guān)注的業(yè)務(wù)邏輯。例如歷史上給用戶發(fā)消息通知是根據(jù)用戶訂單狀態(tài)變化觸發(fā)的,由于和通知平臺交互,因此訂單要提供通知相關(guān)的所有參數(shù),等于訂單依賴通知相關(guān)的模版,明顯存在核心依賴非核心的問題。而此時如果我們提出需求,要對于某些通知平臺發(fā)送失敗的消息進行重發(fā),邏輯似乎也只能做到訂單上,不論怎么看都很不優(yōu)雅。
三、解決方案
3.1 回歸業(yè)務(wù)本質(zhì)——挖掘愿景
為了解決業(yè)務(wù)歸屬問題和明確系統(tǒng)發(fā)展方向,避免將資源投入那些非核心的功能,我們需要明確當(dāng)前項目它是什么,目標(biāo)是什么。因此我們需要為系統(tǒng)準(zhǔn)備一份愿景,它將指導(dǎo)我們在未來的迭代中不迷失方向。這個愿景相當(dāng)于我們的產(chǎn)品定位,是我們的系統(tǒng)和其它系統(tǒng)不同之處,也是當(dāng)前系統(tǒng)的邊界。
愿景就像手電筒中發(fā)出的光,在光暗之間是我們系統(tǒng)的邊界,系統(tǒng)的未來也在光的方向中。
輸出一個愿景說明有很多方式,為了簡化落地的門檻,我們采取麥肯錫“電梯演講”的方式,圍繞機會、挑戰(zhàn)、優(yōu)勢、劣勢給出一組結(jié)果,由領(lǐng)域?qū)<液烷_發(fā)團隊一起進行頭腦風(fēng)暴,實際上這也是DDD統(tǒng)一語言的開始,我們必須從愿景開始就達(dá)成一致。
友情提醒
Eric Even在他的書中曾提到過一種模式:領(lǐng)域愿景描述(Domain Vision Statement)
“由于一開始項目的模型通常不存在,但是需求是早已定下的重點,為了我們在后續(xù)階段清楚了解系統(tǒng)的價值,以價值作為我們的導(dǎo)向。”
我們在研究領(lǐng)域愿景描述時發(fā)現(xiàn)要寫出一份合格的文檔并不容易,因為它缺乏明確的規(guī)范和套路,Eric也只是給了我們幾個案例體會,不得不說雖然寫出來容易,但是要做到合格還是有門檻的。因此我們退一步,回到Eric說的愿景說明來:“很多項目團隊都會編寫‘愿景說明’以便管理。最好的愿景說明會展示出應(yīng)用程序為組織帶來的具體價值。”
3.2 高效溝通——利用事件風(fēng)暴統(tǒng)一語言
說到統(tǒng)一語言,最經(jīng)典的例子應(yīng)該是傳話游戲,一句話從最初的人口中說出,經(jīng)歷中間多人轉(zhuǎn)述,最后可能完全變成另一種意思。
為了快速實現(xiàn)統(tǒng)一語言,我們在訂單重構(gòu)中花了比較多的時間進行事件風(fēng)暴。事件風(fēng)暴有以下幾點優(yōu)勢:
- 事件風(fēng)暴圍繞業(yè)務(wù)流程進行討論,使在場的每一個人都通過多條流程深入了解業(yè)務(wù)實體的變化。
- 事件風(fēng)暴聚集了“領(lǐng)域?qū)<摇保a(chǎn)品、開發(fā)、測試等,本質(zhì)也是一場集合集體智慧的頭腦風(fēng)暴,所有人在事件風(fēng)暴中達(dá)成業(yè)務(wù)共識。
- 事件風(fēng)暴集合了所有人的領(lǐng)域知識,同樣是一場領(lǐng)域知識的分享會。
原本事件風(fēng)暴是以工作坊的方式在線下組織,這樣大家的參與感更強烈。但是由于成本和線上辦公的興起,我們在在線工作坊的實踐會更多一些。這里推薦兩個工具,一個是行知蜂(BeeArt),另一個是可畫(canva),都支持多人在線協(xié)作。
事件風(fēng)暴其實非常簡單,就是業(yè)務(wù)流程+業(yè)務(wù)用例,將業(yè)務(wù)流程橫向展開,通過用例將業(yè)務(wù)中的名詞狀態(tài)變化一一列舉。其中色塊的大小和顏色可以參考www.eventstorming.com,但是我認(rèn)為只要能夠統(tǒng)一大家的認(rèn)知,顏色是次要的。
經(jīng)過我們的嘗試,先列舉業(yè)務(wù)中單據(jù)的狀態(tài)變化,后補全觸發(fā)狀態(tài)變化的動作和角色效率會更高一些。關(guān)鍵是將大家認(rèn)知中的不同事物相同名詞、不同名詞相同事物識別出來,利于后續(xù)建模。
通過事件風(fēng)暴,我們主要關(guān)注以下幾種情況:
- 溝通中那些脫離當(dāng)前領(lǐng)域就難以理解的詞匯;
- 相同名詞,含義不同的;
- 名詞不同,含義相同的。
將以上三種情況涉及的名詞動詞總結(jié)成統(tǒng)一語言表,特別是第三種情況恰恰是我們劃分限界上下文的關(guān)鍵依據(jù)。例如我們在聊支付單時發(fā)現(xiàn)存在兩種支付單,一個是包含我們業(yè)務(wù)的支付單,它需要記錄當(dāng)前支付的場景并包含一定的業(yè)務(wù)規(guī)則;另一個是支付平臺的支付單,每次支付都會生成一個支付單,它可以認(rèn)為是和更抽象的訂單相關(guān)(例如會員訂單、優(yōu)惠券訂單)。
于是我們提取了費項記錄這個概念,表達(dá)一筆訂單可以有多個費項記錄,用于區(qū)分我們的支付
單和支付中臺的支付單之間的差別。
3.3 自上而下細(xì)化邊界——子域劃分
傳統(tǒng)面向過程的開發(fā)方法面對復(fù)雜系統(tǒng)通常會采用DFD數(shù)據(jù)流圖的方式進行拆分,在DDD中則是提出了子域的概念。我們總是會聽到領(lǐng)域(Domain)和子域(Sub Domain),不論是Eric的DDD還是IDDD中都大量使用這些概念,但是我們會發(fā)現(xiàn)他們并未向我們解釋清楚子域是如何劃分而來的。
對于一個已有的系統(tǒng)而言,我們可以根據(jù)康威定律得出:團隊邊界=系統(tǒng)邊界,因此可以認(rèn)為每個團隊負(fù)責(zé)的部分就是天然的子域。由于目前訂單團隊本就分為用戶訂單組和采購派發(fā)組,因此我們可以初步得出一個領(lǐng)域劃分:
此時我們根據(jù)愿景,可以明確兩個子域各自的職責(zé):用戶訂單子域負(fù)責(zé)提供用戶訂單流程的查看和管理,并且負(fù)責(zé)在需要的環(huán)節(jié)主動通知用戶;采購訂單子域則負(fù)責(zé)真正定后履約流程的流轉(zhuǎn),包括供應(yīng)商和行前行中行后的狀態(tài)更新。
最后支付使用的是攜程金融的能力,由于支付平臺的能力在攜程內(nèi)部是統(tǒng)一的,因此我們認(rèn)為支付平臺屬于通用域。
3.4 自下而上抽象概念——限界上下文
領(lǐng)域的概念相對而言還是模糊的,因此Eric提出了DDD中最重要的概念:限界上下文。而限界上下文并非憑空而來,而是需要對我們在事件風(fēng)暴中得到的名詞進行歸納而來。
首先我們列舉了用戶訂單域的各種用例,包括下用戶單、支付訂單、修改訂單、取消訂單等。
通過建模法歸納模型,例如在訂單流程中我們存在多場景的支付,同時又依賴支付平臺的支付單,因此我們得到了維護支付單狀態(tài)的支付費項記錄,它既維護了支付單相關(guān)的信息,也維護了當(dāng)前訂單系統(tǒng)內(nèi)關(guān)于支付的業(yè)務(wù)邏輯。
最后我們根據(jù)業(yè)務(wù)相關(guān)性對得出的實體進行歸納,結(jié)合我們的愿景得出三個上下文,分別是:
- 用戶訂單狀態(tài)上下文:負(fù)責(zé)管理用戶訂單狀態(tài)管理;
- 支付費項上下文:負(fù)責(zé)訂單支付相關(guān)狀態(tài)管理;
- 用戶通知上下文:負(fù)責(zé)對用戶進行多種方式的通知。
3.5 挖掘業(yè)務(wù)變化的瓶頸——上下文依賴關(guān)系
實際上限界上下文可以拆到很細(xì)的粒度,但是我們應(yīng)該遵循“奧康姆剃刀”的規(guī)則,盡量設(shè)置合理的數(shù)量,拆分的要有理有據(jù)。我們可以先看看原來的系統(tǒng)上下文依賴關(guān)系:
根據(jù)Eric對上下文關(guān)系的總結(jié),我們可以得出消息中心作為攜程的消息中臺,不會為了某個業(yè)務(wù)線做特殊邏輯,因此是很明顯的遵奉者(Conformist)。此時消息相關(guān)的處理耦合在訂單內(nèi)部,如果發(fā)送消息沒有業(yè)務(wù)邏輯那么采取防腐層(ACL)的方式是比較常見的。
由于我們已經(jīng)識別到用戶通知存在業(yè)務(wù)邏輯,因此訂單直接和消息中心交互顯得奇怪,而且訂單作為核心域,本來就應(yīng)該盡量不依賴其它域,對此我們進行了如下設(shè)計:
這樣用戶訂單上下文更加內(nèi)聚,而用戶通知也更加易于迭代。
四、收益總結(jié)
4.1 業(yè)務(wù)邏輯耦合降低
通過上下文拆分和職責(zé)的明確,由各領(lǐng)域維護自己的數(shù)據(jù)和領(lǐng)域知識,使得訂單不再維護這些字段,而由數(shù)據(jù)寫入的業(yè)務(wù)方去維護,后續(xù)有和訂單無關(guān)的業(yè)務(wù)邏輯變更時訂單無需改動。
4.2 團隊效率提高
隨著上下文拆分和康威定律的應(yīng)用,各團隊職責(zé)和各自的領(lǐng)域形成映射,過去由于團隊職責(zé)劃分不清,經(jīng)常為某功能誰做來爭論不休的問題也得到了解決。
4.3 性能和穩(wěn)定性提高
通過上下文拆分后,訂單實體從之前的780多個字段簡化到200多個字段,大大降低了訂單的維護成本,存儲數(shù)據(jù)量減少,原來的業(yè)務(wù)邏輯也由每個寫入方自己進行維護,接口性能p95 寫由68ms優(yōu)化到12ms ,讀從63ms降低到5ms。
4.4 數(shù)據(jù)一致性
由于過去業(yè)務(wù)方寫入數(shù)據(jù)到訂單可能由于網(wǎng)絡(luò)抖動等原因?qū)懭胧』驑I(yè)務(wù)方寫錯數(shù)據(jù)導(dǎo)致修復(fù)數(shù)據(jù)需要兩邊一起改,現(xiàn)在業(yè)務(wù)方將數(shù)據(jù)存放在自己的領(lǐng)域內(nèi),不再存入訂單,避免了數(shù)據(jù)不一致和字段寫錯等問題的產(chǎn)生。
4.5 人力成本大幅下降
產(chǎn)研溝通涉及的相關(guān)方大量減少,鏈路縮短。刨去原本因為業(yè)務(wù)邏輯耦合導(dǎo)致訂單跟著修改的人力成本,整體人力成本小項目下降70%,大項目更是下降80%。
五、遇到的問題和方案探索
落地DDD實際上是一個非常困難的過程,我們必須面對缺乏領(lǐng)域?qū)<遥瑯I(yè)務(wù)需求多且急,團隊對DDD理解不深等諸多問題。對此我們總結(jié)出以下幾點經(jīng)驗:
5.1 領(lǐng)域?qū)<译y尋
領(lǐng)域?qū)<沂荄DD中最重要的角色之一,沒有領(lǐng)域?qū)<椅覀兙蜔o法獲取知識,就沒有后續(xù)的建模等等。但是實際工作中要尋找一個嚴(yán)格意義上的領(lǐng)域?qū)<沂抢щy且成本高昂的,因此我們需要尋求一些其它方式曲線救國,例如該領(lǐng)域的資深研發(fā),資深QA等,同時我們兩車還采取互相借鑒的方式,雖然業(yè)務(wù)不完全相同,但是領(lǐng)域上也有互通之處。
5.2 業(yè)務(wù)需求多且急
實際工作中我們經(jīng)常忙于各種業(yè)務(wù)項目,關(guān)鍵是還很急。這就很容易導(dǎo)致我們沒辦法專注于DDD改造,怎么辦呢?我們的方案是在有時間的時候把方向定下來,提前進行設(shè)計,然后在做業(yè)務(wù)項目時將這些設(shè)計逐步進行實現(xiàn)。
5.3 團隊對DDD理解不深
為了提高團隊對DDD的理解,我們專門成立了DDD培訓(xùn)小組,將我們的一些落地經(jīng)驗整理成規(guī)范和最佳實踐。同時在落地時由培訓(xùn)小組的同學(xué)進行把關(guān),避免大家走彎路。
以上就是此次分享的全部內(nèi)容,如果后續(xù)大家有什么疑問可以在下方留言,如果后續(xù)有機會我們會在疑惑較多的點進行再次分享。希望大家通過這篇文章得到一些想要的收獲!