重构:改善饿了么交易系统的设计思路
文?|?盛赫
叮~,您有新的餓了么訂單,正在阿里云上被接單。
這篇文章成型于交易系統(tǒng)重構(gòu)一期之后,主要是反思其過程中做決策的思路,我沒有使用「架構(gòu)」這個(gè)詞語,是因?yàn)樗o人的感受充滿權(quán)利和神秘感,談?wù)摗讣軜?gòu)」讓人有一種正在進(jìn)行責(zé)任重大的決策或者深度技術(shù)分析的感覺。
如畢玄在系統(tǒng)設(shè)計(jì)的套路這篇文章里所提:
回顧了下自己做過的幾個(gè)系統(tǒng)的設(shè)計(jì),發(fā)現(xiàn)現(xiàn)在自己在做系統(tǒng)設(shè)計(jì)的時(shí)候確實(shí)是會(huì)按照一個(gè)套路去做,這個(gè)套路就是:系統(tǒng)設(shè)計(jì)的目的->系統(tǒng)設(shè)計(jì)的目標(biāo)->圍繞目標(biāo)的核心設(shè)計(jì)->圍繞核心設(shè)計(jì)形成的設(shè)計(jì)原則->各子系統(tǒng),模塊的詳細(xì)設(shè)計(jì)
在進(jìn)行系統(tǒng)設(shè)計(jì)時(shí),摸清楚目的,并形成可衡量的目標(biāo)是第一步。
"Soft" ware
?Software 拆開來分別是 soft ware ,即靈活的產(chǎn)品。? -- 鮑勃大叔
重構(gòu)前的交易系統(tǒng)第一版的代碼可以追溯到 8 年前,這期間也經(jīng)歷過拆解重構(gòu),17 年我來到時(shí),主要系統(tǒng)是這樣:
?
這套系統(tǒng)馱著業(yè)務(wù)從百萬級(jí)訂單跑到了千萬級(jí)訂單,從壓測表現(xiàn)來看,它可以再支撐業(yè)務(wù)多翻幾倍的量,也就是說如果沒有啥變化,它可以繼續(xù)穩(wěn)定運(yùn)行著,但如果發(fā)生點(diǎn)變化呢,答案可能就不這么肯定了。
在我入職的這兩年里,系統(tǒng)承載的業(yè)務(wù)迭增變化:從單一的餐飲外賣到與新零售及品牌餐飲三方并行,又從到家模式衍生至到店,隨之而來的是業(yè)務(wù)持續(xù)不斷的差異化定制,還有并行上線的要求。另一面,隨著公司組織架構(gòu)變化,有的項(xiàng)目需要三地協(xié)同推進(jìn)才能完成,溝通協(xié)作成本翻倍提升。幾方面結(jié)合起來,導(dǎo)致開發(fā)沒有精力對(duì)大部分系統(tǒng)的演進(jìn)都進(jìn)行完善的規(guī)劃。
幾個(gè)月前,業(yè)務(wù)提了一個(gè)簡單的需求:對(duì)交易的評(píng)價(jià)做自動(dòng)審核并進(jìn)行相應(yīng)的處罰。當(dāng)時(shí)評(píng)價(jià)核心“域模型”是這樣的:
設(shè)計(jì)自身的優(yōu)劣這里暫不進(jìn)行討論,只是舉例說明為了滿足這個(gè)訴求,會(huì)涉及多個(gè)評(píng)價(jià)子模塊要改動(dòng),開發(fā)評(píng)估下來的工作量遠(yuǎn)遠(yuǎn)超出了預(yù)期,業(yè)務(wù)方對(duì)此不滿意,類似的沖突在其他系統(tǒng)里也經(jīng)常出現(xiàn)。但實(shí)際上,團(tuán)隊(duì)里沒人偷懶,和之前一樣努力工作,只是不管投入了多少個(gè)人時(shí)間,救了多少次火,加了多少次班,產(chǎn)出始終上不去,因?yàn)殚_發(fā)大部分時(shí)間都在系統(tǒng)的修修補(bǔ)補(bǔ)上,而不是真正完成實(shí)際的新功能,一直在拆東墻補(bǔ)西墻,周而往復(fù)。
為什么會(huì)導(dǎo)致這樣的結(jié)果,我想應(yīng)該是因?yàn)榇蟛糠窒到y(tǒng)已經(jīng)演變到很難響應(yīng)需求的變更了,業(yè)務(wù)認(rèn)為的小小變更,對(duì)開發(fā)來說都是系統(tǒng)的一次大手術(shù),但系統(tǒng)本不應(yīng)該往這個(gè)方向發(fā)展的,它和 hardware 有著巨大的區(qū)別就在于:變更對(duì)軟件來說應(yīng)該是簡單靈活的。
所以我們思考設(shè)計(jì)的核心目標(biāo):“采用好的軟件架構(gòu)來節(jié)省項(xiàng)目構(gòu)建和維護(hù)的人力成本,讓每一次變更都短小簡單,易于實(shí)施,并且避免缺陷,用最小的成本,最大程度地滿足功能性和靈活性的要求”。
Source code is the design
提到軟件設(shè)計(jì),大家腦袋里可能會(huì)想到一幅幅結(jié)構(gòu)清晰的架構(gòu)圖,認(rèn)為關(guān)于軟件架構(gòu)的所有奧秘都隱藏在圖里了,但經(jīng)歷過一些項(xiàng)目后發(fā)現(xiàn),這往往是不夠的。Jack ? Reeves 在 1992 年發(fā)表了一篇論文《源代碼即設(shè)計(jì)》,他在文中提出一個(gè)觀點(diǎn):
高層結(jié)構(gòu)的設(shè)計(jì)不是完整的軟件設(shè)計(jì),它只是細(xì)節(jié)設(shè)計(jì)的一個(gè)結(jié)構(gòu)框架。在嚴(yán)格地驗(yàn)證高層設(shè)計(jì)方面,我們的能力是非常有限的。詳細(xì)設(shè)計(jì)最終會(huì)對(duì)高層設(shè)計(jì)造成的影響至少和其他的因素一樣多(或者應(yīng)該允許這種影響)。對(duì)設(shè)計(jì)的各個(gè)方面進(jìn)行改進(jìn),是一個(gè)應(yīng)該貫穿整個(gè)設(shè)計(jì)周期的過程。
在踩過一些坑之后,這種強(qiáng)調(diào)詳細(xì)設(shè)計(jì)重要性的觀點(diǎn)在我看來很實(shí)在接地氣,簡單來說:“自頂向下的設(shè)計(jì)通常是不靠譜的,編碼即是設(shè)計(jì)過程的一部分”,個(gè)人認(rèn)為:系統(tǒng)設(shè)計(jì)應(yīng)該是從下到上,隨著抽象層次的提升,不斷演化而得到良好的高層設(shè)計(jì)。
編程范式
從下向上,那就應(yīng)該從編碼開始審視,餓了么交易系統(tǒng)最開始是由 Python 編寫,? Python 足夠靈活,可以非常快速的產(chǎn)出 mvp 的系統(tǒng)版本,這也和當(dāng)時(shí)的公司發(fā)展?fàn)顟B(tài)相關(guān): 產(chǎn)品迭代迅速,新項(xiàng)目的壓力很大。
最近這次重構(gòu),順應(yīng)集團(tuán)趨勢,我們使用 Java 來進(jìn)行編寫,不過在這之前有一個(gè)小插曲:17 年底,因?yàn)轭A(yù)估到當(dāng)前系統(tǒng)框架在單量到達(dá)下一個(gè)量級(jí)時(shí)會(huì)遇到瓶頸,所以針對(duì)一些新業(yè)務(wù)逐漸開始使用 Go 語言編寫,但在這個(gè)過程里,經(jīng)常會(huì)聽到一些言論:用 Go 來寫業(yè)務(wù)不舒服。為什么會(huì)不舒服?大致是因?yàn)闆]有框架,沒有泛型,沒有 try catch ,確實(shí),在解決業(yè)務(wù)問題的這個(gè)大的上下文中, Go 語言不是最優(yōu)的選擇,但語法簡單,可以極大程度的避免普通程序員出錯(cuò)的概率。
那么 Python 呢,任何事物都有雙刃劍,雖然 Python 具有強(qiáng)表達(dá)力,但是靈活性也把很多人慣壞了,代碼寫的糙,動(dòng)態(tài)語言寫太多坑也多,容易出錯(cuò),在大項(xiàng)目上的工程管理和維護(hù)上有一定劣勢,所以 rails 作者提到:“靈活性被過分高估——約束才是解放”也有一定道理。
為避免引起語言戰(zhàn),這里不過多討論,只是想引出:我從 C++ 寫到 Go ,又從 Python 寫到 Java ,在這個(gè)過程里體會(huì)到--編程范式也許是學(xué)習(xí)任何一門編程語言時(shí)要理解的最重要的術(shù)語,簡單來說它是程序員看待程序應(yīng)該具有的觀點(diǎn),但卻容易被忽視。交易老系統(tǒng)的代碼,不管是針對(duì)什么業(yè)務(wù)邏輯,幾乎都是OPP一桿到底,類似的代碼在系統(tǒng)里隨處可見。
我們好像完全遺忘了 OOP ,這項(xiàng)古老的技藝被淡化了,我這里不是說一定要 OOP 就是完美的,準(zhǔn)確來說我是“面向問題”范式的擁躉者,比如, Java從骨子里就是要 OOP ,但是業(yè)務(wù)流程不一定需要 OOP 。一些交易業(yè)務(wù)就是第一步怎么樣,第二步怎么樣,采取 OPP 的范式就是好的解法。這時(shí),弄很復(fù)雜的類設(shè)計(jì)有時(shí)并不必要,反而還會(huì)帶來麻煩。
此外,同一個(gè)問題還可以拆解為不同的層次,不同的層次可以使用各自適合的方式。比如高層的可以 OOP ,具體到某個(gè)執(zhí)行邏輯里可以用 FP ,比如:針對(duì)訂單的金額計(jì)算,我們用 Go 寫了一版FP的底層計(jì)算服務(wù),性能高、語法簡單以及出錯(cuò)少等是語言附帶的優(yōu)點(diǎn),核心還是因?yàn)樵擃悊栴}自身適合。
然而,當(dāng)面向整個(gè)交易領(lǐng)域時(shí),針對(duì)繁復(fù)多樣的業(yè)務(wù)場景,合理運(yùn)用 OOP 的設(shè)計(jì)思想已經(jīng)被證明確實(shí)可以支撐起復(fù)雜龐大的軟件設(shè)計(jì),所以我們作出第一個(gè)決策:采用以 OOP 為主的“混合”范式。
原則和模式
?
The difference between a bad programmer and a?good one is whether he considers his code or his?
data structures more important. Bad programmers?worry about the code. Good programmers worry about?data structures and their relationships.?--?Linus Torvalds
不管是采用哪種編程范式、編程語言,構(gòu)造出來的基礎(chǔ)模塊就像蓋樓的磚頭,如果磚頭質(zhì)量不好,最終大樓也不會(huì)牢固,引用里的一大段話, relationships 才是我最想強(qiáng)調(diào)的:我理解它是指類之間的交互關(guān)系,“關(guān)系”的好壞通常等價(jià)于軟件設(shè)計(jì)的優(yōu)劣,設(shè)計(jì)不好的軟件結(jié)構(gòu)大都有些共同特征:
-
僵化性:難以對(duì)軟件進(jìn)行改動(dòng),一般會(huì)引發(fā)連鎖改動(dòng),比如下單時(shí)增加一個(gè)新的營銷類型,訂單中心和相關(guān)上下游都要感知到并去做改動(dòng)。
-
脆弱性:簡單的改動(dòng)會(huì)引發(fā)其他意想不到的問題,甚至概念完全不相關(guān)。
-
牢固性:設(shè)計(jì)中有對(duì)其他系統(tǒng)有用的部分,但是拆出來的風(fēng)險(xiǎn)和成本很高,比如訂單中心針對(duì)外賣場景的支付能力并不能支持會(huì)員卡等虛擬商品的支付需求。
-
不必要的復(fù)雜性:這個(gè)通常是指過度設(shè)計(jì)。
-
晦澀性:隨時(shí)間演化,模塊難以理解,代碼越來越難讀懂,比如購物車階段的核心代碼已經(jīng)長成了一個(gè)近千行的大函數(shù)。
-
...
采取合適的范式后,我們需要向上抽一個(gè)層次,來關(guān)注代碼之上的邏輯,多年軟件工程的發(fā)展沉淀下來了一些基本原則和模式,并被證明可以指導(dǎo)我們?nèi)绾伟褦?shù)據(jù)和函數(shù)封裝起來,然后再把它們組織起來成為程序。
SOLID
有人將這些原則重新排列下順序,將首字母組成 SOLID ,分別是:SRP、OCP、LSP、ISP、DIP。這里針對(duì)其中幾個(gè)原則來舉些例子。
SRP(單一職責(zé)):這個(gè)原則很簡單,即任何一個(gè)軟件模塊都應(yīng)該只對(duì)一類用戶負(fù)責(zé),所以代碼和數(shù)據(jù)應(yīng)該因?yàn)楹湍骋活愑脩絷P(guān)系緊密而被組織到一起。實(shí)際上我們大部分的工作就是在發(fā)現(xiàn)職責(zé),然后拆開他們。
我認(rèn)為該原則的核心在于用戶的定義,18 年去聽 Qcon 時(shí),聽到俞軍的分享,其中一段正好可以拿來詮釋什么是用戶,俞軍說:“用戶不是人,是需求的集合”。在我們重構(gòu)的過程中,曾經(jīng)對(duì)交易系統(tǒng)里的交付環(huán)節(jié)有過爭論,目前餓了么支持商家自配和平臺(tái)托管以及選擇配送(比如跑腿),這幾類配送的算價(jià)方式,配送邏輯,和使用場景都不一樣,所以我們基于此做了拆解,一開始大家都認(rèn)同這種分解方式。
但后來商戶群體調(diào)整了,新零售商戶和餐飲商戶進(jìn)行分拆,對(duì)應(yīng)著業(yè)務(wù)方的運(yùn)營方式也開始出現(xiàn)差異,導(dǎo)致在每個(gè)配送方式下也有了不同訴求,伴隨這些變化,最后我們選擇做了第二次拆解。
對(duì)于單一職責(zé),這里有個(gè)小 tips :大家如果實(shí)在不好分析的話,可以多觀察那些因?yàn)榉种Ш喜⒍a(chǎn)生沖突的代碼,因?yàn)檫@很可能是因?yàn)獒槍?duì)不同需求,大家同時(shí)改了同一個(gè)模塊。
DIP(依賴倒置):有人說依賴反轉(zhuǎn)是 OOP 和 OPP 的分水嶺,因?yàn)樵谶^程化設(shè)計(jì)里所創(chuàng)建的依賴關(guān)系,策略是依賴于細(xì)節(jié)的--也就是高層依賴于底層,但這通常會(huì)讓策略因?yàn)榧?xì)節(jié)改變而受到影響,舉個(gè)例子:在外賣場景下,一旦用戶因?yàn)槟承┰蚴詹坏讲土?#xff0c;商戶會(huì)賠代金券安撫用戶,此時(shí) OPP 可以這樣做:
而過一陣子,因?yàn)榇鹑ǔ2荒芸绲晔褂?#xff0c;平臺(tái)想讓用戶繼續(xù)復(fù)購,就想通過賠付通用紅包來挽留,這個(gè)時(shí)候就需要改動(dòng)老的代碼,通過增加對(duì)紅包賠付邏輯的依賴,才可以來滿足訴求。
但如果換個(gè)方式,采用 DIP 的話,問題也許可以被更優(yōu)雅的解決了:
當(dāng)然這個(gè)示例是簡化后的版本,實(shí)際工作里還有很多更加復(fù)雜的場景存在,但本質(zhì)都是一樣:采用 OOP 倒置了策略對(duì)細(xì)節(jié)的依賴,使細(xì)節(jié)依賴于抽象,并且常常是客戶擁有服務(wù)接口,這個(gè)過程中的核心是需要我們做好抽象。
OCP(開閉原則):如果仔細(xì)分析,會(huì)發(fā)現(xiàn)這個(gè)原則其實(shí)是我們一開始定的系統(tǒng)設(shè)計(jì)的目標(biāo),也是其他原則最終想達(dá)成的目的,比如:通過 SRP ,把每個(gè)業(yè)務(wù)線的模塊拆解出來,將變動(dòng)隔離,但是平臺(tái)還要做一定的抽象,將核心業(yè)務(wù)流程沉淀下來,并開放出去每個(gè)業(yè)務(wù)線自己定義,這時(shí)候就又會(huì)應(yīng)用到 DIP 。
其他的幾個(gè)原則就不舉例子了,當(dāng)然除了 SOLID ,還有其他類型的原則,比如 IoC :用外賣交易平臺(tái)舉例子,商戶向用戶賣飯,一手交錢一手交貨,所以,基本上來說用戶和商戶必需強(qiáng)耦合(必需見面)。這個(gè)時(shí)候,餓了么平臺(tái)出來做擔(dān)保,用戶把錢先墊到平臺(tái),平臺(tái)讓商家接單然后出餐,用戶收到餐后,平臺(tái)再把錢打給商家。這就是反轉(zhuǎn)控制,買賣雙方把對(duì)對(duì)方的直接依賴和控制,反轉(zhuǎn)到了讓對(duì)方來依賴一個(gè)標(biāo)準(zhǔn)的交易模型的接口。
可以發(fā)現(xiàn)只要總結(jié)規(guī)律,總會(huì)出現(xiàn)這樣或那樣的原則,但每個(gè)的原則的使用都不是一勞永逸的--需要不斷根據(jù)實(shí)際的需求變化做代碼調(diào)整,原則也不是萬金油,不能無條件使用,否則會(huì)因?yàn)檫^分遵循也會(huì)帶來不必要的復(fù)雜性,比如經(jīng)常見到一些使用了工廠模式的代碼,里面一個(gè) new 其實(shí)就是違反了 DIP ,所以適度即可。
演進(jìn)到模式
這里的模式就是我們常說的設(shè)計(jì)模式,用演進(jìn)這個(gè)詞,是因?yàn)槲矣X得模式不是起點(diǎn),而是設(shè)計(jì)的終點(diǎn)。《設(shè)計(jì)模式》這本書的內(nèi)容不是作者的發(fā)明創(chuàng)造,而是其從大量實(shí)際的系統(tǒng)里提取出來的,它們大都是早已存在并已經(jīng)廣泛使用的做法,只不過沒有被系統(tǒng)的梳理。換句話說,只要遵循前面敘述的某些原則,這些模式完全可能會(huì)自然在系統(tǒng)代碼中體現(xiàn)出來,在《敏捷軟件開發(fā)》這本書里,就特意有一個(gè)章節(jié),描述了一段代碼隨著調(diào)整慢慢演進(jìn)到了觀察者模式的過程。
擁有模式固然是好的,比如搜索系統(tǒng)里,通過 Template Method 模式,定義一套完整的搜索參數(shù)解析模版,只需要增加配置就可以定制不同的查詢?cè)V求。這里最想強(qiáng)調(diào)的是不要設(shè)計(jì)模式驅(qū)動(dòng)編程,拿交易系統(tǒng)里的狀態(tài)機(jī)來舉例子(狀態(tài)機(jī)簡直太常見了,簡單如家里使用的臺(tái)燈,都有一個(gè)開和關(guān)的狀態(tài),只是交易場景下會(huì)更加復(fù)雜),在餐飲外賣交易有如下的狀態(tài)流轉(zhuǎn)模型:
實(shí)現(xiàn)這樣的一個(gè)有限狀態(tài)機(jī),最直接的方式是使用嵌套 switch/case 語句,簡略的代碼比如:
public class Order {// Statespublic static final int ACCEPT = 5;public static final int SETTLED = 9;..// Eventspublic static final int ARRIVED = 1; // 訂單送達(dá)public void event(int event) {switch (state) {case ACCEPT:switch (event) {case ARRIVED:state = SETTLED;//to do actionbreakcase }}} }因?yàn)槭呛唽懥肆鞒?#xff0c;所以上面的代碼看起來還是挺能接受的,但是對(duì)于訂單狀態(tài)這么復(fù)雜的狀態(tài)機(jī),這個(gè) switch/case 語句會(huì)無限膨脹,可讀性很差,另一個(gè)問題是狀態(tài)的邏輯和動(dòng)作沒有拆開,《設(shè)計(jì)模式》提供了一個(gè) State 模式,具體做法是這樣:
這個(gè)模式確實(shí)分離了狀態(tài)機(jī)的動(dòng)作和邏輯,但是隨著狀態(tài)的增加,不斷增加 State 的類會(huì)讓系統(tǒng)變得異常復(fù)雜,而且對(duì) OCP 的支持也不好:對(duì)切換狀態(tài)這個(gè)場景,新增類會(huì)引起狀態(tài)切換類的修改,最不能忍受的是這個(gè)方式會(huì)把整個(gè)狀態(tài)機(jī)的邏輯隱藏在零散的代碼里。
舊版的交易系統(tǒng)就使用的是解釋遷移表來實(shí)現(xiàn)的,簡化版本是這樣的:
# 完結(jié)訂單 add_transition(trigger=ARRIVED,src=ACCEPT,dest=SETTLED,on_start=_set_order_settled_at,set_state=_set_state_with_record, // 變更狀態(tài)on_end=_push_to_transcore) ... # 引擎 def event_fire(event, current_state):for transition in transitions:if transition.on_start == current_state && transition.trigger == event:transition.on_start()current_state = transition.desttransition.on_end()這個(gè)版本非常容易理解,狀態(tài)邏輯集中在一起,也沒有和動(dòng)作耦合起來,擴(kuò)展性也比較強(qiáng),唯一缺點(diǎn)的話是遍歷的時(shí)間,但也可以通過字典表來優(yōu)化,但它總體帶來的好處更加明顯。
不過隨著業(yè)務(wù)發(fā)展,交易系統(tǒng)需要同時(shí)支持多套狀態(tài)機(jī),意味著會(huì)出現(xiàn)多個(gè)遷移表,而且還有根據(jù)業(yè)務(wù)做擴(kuò)展定制的需求,這套解決方案會(huì)導(dǎo)致代碼編寫變得復(fù)雜起來,我們?cè)谥貥?gòu)時(shí)采用了二級(jí)編排+流程引擎的方式來優(yōu)化了這個(gè)問題,只是不在我們討論的范圍內(nèi),這里只想強(qiáng)調(diào)第二個(gè)決策:代碼上要靈活通過設(shè)計(jì)原則分析問題,再通過合適的設(shè)計(jì)模式解決問題,不能設(shè)計(jì)模式驅(qū)動(dòng)編程,比如有時(shí)候一個(gè)全局變量就可以替代所謂的單例模式。
豐富的領(lǐng)域含義
?一旦你想解說美,而不提擁有這種特質(zhì)的東西,那么就完全無法解釋清楚了。
用個(gè)不那么貼切的說法,如果前面說的是針對(duì)靜態(tài)問題的策略,現(xiàn)在我們需要討論面對(duì)動(dòng)態(tài)問題的解決辦法:即使沒有風(fēng),人們也不會(huì)覺得一片樹葉是穩(wěn)定的,所以人們定義穩(wěn)定的時(shí)候和變更的頻繁度無關(guān),而是和變更需要的成本有關(guān),因?yàn)榇狄豢跉?#xff0c;樹葉就會(huì)隨之搖擺了。我們除了要寫好當(dāng)前代碼,讓其足夠清晰合理,還要能寫好應(yīng)對(duì)需求變化的“樹葉”代碼。
面向業(yè)務(wù)變化的設(shè)計(jì)首先就是要理解業(yè)務(wù)的核心問題,進(jìn)而進(jìn)行拆解劃分為各個(gè)子領(lǐng)域,DDD--也就是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),已經(jīng)被證明是一個(gè)很好的切入點(diǎn)。這里不是把它當(dāng)作技術(shù)來學(xué)習(xí),而是作為指導(dǎo)開發(fā)的方法論,成為第三個(gè)決策,并且我個(gè)人仍處在初級(jí)階段,所以只說一些理解深刻的點(diǎn)。
通用語言
設(shè)計(jì)良好的架構(gòu)在行為上對(duì)系統(tǒng)還有一個(gè)最重要的作用:就是明確的顯式的反映系統(tǒng)設(shè)計(jì)的意圖,簡單來說,在你拉下某些服務(wù)的代碼的時(shí)候,大概掃一眼就可以覺得:嗯,這個(gè)“看起來” 就像一個(gè)交易系統(tǒng)的應(yīng)用。我們不能嘴上在談?wù)摌I(yè)務(wù)邏輯,手上卻敲出另一份模樣的代碼,簡單來說,不能見人說人話,見鬼說鬼話。可以對(duì)比一下這兩類分包的方式,哪一個(gè)更容易理解:
發(fā)現(xiàn)領(lǐng)域通用語言的目的之一是可以通過抓住領(lǐng)域內(nèi)涵來應(yīng)該需求變更,這個(gè)需要很多客觀條件,比如團(tuán)隊(duì)里有一個(gè)領(lǐng)域?qū)<摇5珱]有的時(shí)候,我們也可以向內(nèi)求解,我有次看到一位在丁香園工作的程序員朋友,購買了一大批醫(yī)學(xué)的書籍,不用去問,我就猜他一定是成了 DDD 的教徒。
針對(duì)這個(gè)點(diǎn),我們這次重構(gòu)時(shí)還做了些讓“源代碼即設(shè)計(jì)”的工作:領(lǐng)域元素可視化,當(dāng)系統(tǒng)領(lǐng)域內(nèi)的一些概念已經(jīng)和產(chǎn)品達(dá)成一致之后,便增加約定好的注解,代碼編譯時(shí)便可以掃描并收集起來發(fā)送給前端,用于畫圖。
回到前面提到的評(píng)價(jià)域模型,后來在和產(chǎn)品多次溝通后意識(shí)到,產(chǎn)品沒有希望評(píng)價(jià)這么多種類,對(duì)它來說商品也好、騎手也好,都屬于被評(píng)價(jià)的對(duì)象,從領(lǐng)域模型來看,之前的設(shè)計(jì)更多是面對(duì)場景,而不是面對(duì)行為,所以合理的域模型應(yīng)該是:
限界上下文
這個(gè)在我們平時(shí)開發(fā)過程中會(huì)很常見。拿用戶系統(tǒng)舉例:一個(gè) User 的 Object ,如果是從用戶自身的視角來看,就可以登陸、登出,修改昵稱;如果是從其他普通用戶來看,就只能看看昵稱之類的;如果從后臺(tái)管理員來看,就可以注銷或者踢出登陸。這時(shí)就需要界定一個(gè) Scope ,來說明現(xiàn)在的 User 到底是哪個(gè) Scope ,這其實(shí)就是 DDD 中限界上下文的理念。
限界上下文可以很好的隔離相同事物的不同內(nèi)涵,通過嚴(yán)格規(guī)范可以進(jìn)入上下文的對(duì)象模型,從而保護(hù)業(yè)務(wù)抽象行為的一致性,回到交易領(lǐng)域,餓了么是最開始支持超級(jí)會(huì)員玩法的,為了支持對(duì)應(yīng)的結(jié)算訴求,需要接入交易系統(tǒng)來完成這個(gè)業(yè)務(wù),我們通過分解問題域來降低復(fù)雜度,這個(gè)時(shí)候就對(duì)應(yīng)切割為會(huì)員域和交易域,為了保護(hù)超會(huì)卡在進(jìn)入交易領(lǐng)域的時(shí)候,不擾亂交易內(nèi)部的業(yè)務(wù)邏輯,我們做了一次映射:
切分
當(dāng)所有代碼完成之后,隨著程序增長,會(huì)有越來越多的人參與進(jìn)來,為了方便協(xié)作,就必須把這些代碼劃分成一些方便個(gè)人或者團(tuán)隊(duì)維護(hù)的組。根據(jù)軟件變更速度不同,可以把上文提到的代碼化為幾個(gè)組件:
-
Extension :擴(kuò)展包,這里存放著前面提到的業(yè)務(wù)定制包,面向?qū)ο蟮乃枷?#xff0c;最核心的貢獻(xiàn)在于通過多態(tài),允許插件化的切換一段程序的邏輯,其實(shí)軟件開發(fā)技術(shù)發(fā)展的歷史就是一個(gè)想法設(shè)法方便的增加插件,從而創(chuàng)建一個(gè)可擴(kuò)展,可維護(hù)的系統(tǒng)架構(gòu)的過程。
-
Domain : 領(lǐng)域包,存放著具備領(lǐng)域通用語言的核心業(yè)務(wù)包,它最為穩(wěn)定。
-
Business :業(yè)務(wù)包,存放著具體的業(yè)務(wù)邏輯,它和 Domain 包的區(qū)別在于,可能 Domain 包會(huì)提供一個(gè) people.run() 的方法,他會(huì)用這個(gè)方法去跑著送外賣,或者去健身。
-
Infra : 基礎(chǔ)設(shè)置包,存放這對(duì)數(shù)據(jù)庫及各種中間件的依賴,他們都屬于業(yè)務(wù)邏輯之外的細(xì)節(jié)。
然后是分層依賴,Martin Flower 已經(jīng)提供了一套經(jīng)典的分層封裝的模式,拿簡化的訂單模塊舉例:
然而如果有的同學(xué)避免做各種類型的轉(zhuǎn)換,不想嚴(yán)格遵守分層依賴,覺得一些查詢(這里指 Query,Query != Read )可以直接繞過領(lǐng)域?qū)?#xff0c;這樣就變成了 CQRS 模式:
但是最理想的還是下面這種方式,領(lǐng)域?qū)幼鳛楹诵臉I(yè)務(wù)邏輯,不應(yīng)該依賴基礎(chǔ)設(shè)施的細(xì)節(jié),通過這種方式,代碼的可測性也會(huì)提升上去。
單體程序的組件拆分完畢后,再向上一層,我們開始關(guān)注四個(gè)核心服務(wù):Booking被分拆為 Cart、Buy、Calculate,Eos 被分拆為 Procee、Query、Timeout,Blink 一部分和商戶訂單相關(guān)的功能被分拆到 Process、Query,和物流交付的部分單獨(dú)成一塊 Delivery ,最后交易的核心服務(wù)拆解成下圖:
到目前,算上這個(gè)切分的方式,加起來一共就四個(gè)決策,其實(shí)也沒必要分序列,它們核心都是圍繞著軟件靈活性這個(gè)目標(biāo),從程序范式到組件編寫,最后再到分層,我們主動(dòng)選擇或避開的一些教條限制,所以業(yè)務(wù)架構(gòu)從某種意義上來講,也是在某種領(lǐng)域中限制程序員的一些行為,讓他往我們所希望的規(guī)范方向編碼。從而達(dá)到整個(gè)系統(tǒng)的靈活可靠。
"No Silver Bullet"
“個(gè)體和交互勝過過程和工具”,敏捷宣言第一條。
目前系統(tǒng)架構(gòu)是什么樣子并不重要,因?yàn)樗赡軙?huì)隨著時(shí)間還會(huì)拆解成其他模樣,重要的是,我們要認(rèn)識(shí)到對(duì)于如何建造一個(gè)靈活的交易系統(tǒng)——沒有銀彈。
如果仔細(xì)觀察的話,會(huì)發(fā)現(xiàn)當(dāng)前系統(tǒng)里仍有很多問題等著被解決。比如一些橫跨型變更:系統(tǒng)鏈路里會(huì)因?yàn)槟硞€(gè)服務(wù)的接口增加了字段,而導(dǎo)致上下游跟著一起改。更為尷尬的是,本來我們拆分服務(wù)就是為了解耦合,但有時(shí)還會(huì)出現(xiàn)服務(wù)發(fā)布依賴的現(xiàn)象。系統(tǒng)演進(jìn)是一場持久的戰(zhàn)爭,“個(gè)體和交互勝過過程和工具”,人才是勝利的核心因素。
過去的兩年里,我們沒有停止過思考和實(shí)踐,經(jīng)常可以看到交易團(tuán)隊(duì)內(nèi)部成員的爭執(zhí),小到一個(gè)接口字段變更,大到領(lǐng)域之間的邊界,大家為拿到一個(gè)合理的技術(shù)方案做了很多討論,這讓我想起《禪與摩托車維修藝術(shù)》里所提到的良質(zhì),有人點(diǎn)評(píng)說:關(guān)于良質(zhì),程序員可能有這樣的經(jīng)歷——寫出了一段絕妙的代碼,你會(huì)覺得“不是你寫出了代碼,這段代碼一直存在,而你,發(fā)現(xiàn)了它”。
本文作者:
盛赫,花名白茶,就職于阿里本地生活中臺(tái)研發(fā)部,多年交易系統(tǒng)建設(shè)開發(fā)經(jīng)驗(yàn),目前轉(zhuǎn)入營銷領(lǐng)域繼續(xù)探索。
參考書籍
《軟件設(shè)計(jì)的哲學(xué)》--John Ousterhout
《禪與摩托維修藝術(shù)》--Robert M.Pirsig
《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》--Eric Evans
《敏捷軟件開發(fā)》--Uncle Bob
《架構(gòu)整潔之道》--Uncle Bob
《極客與團(tuán)隊(duì)》--Brian W.FItzapatrick
總結(jié)
以上是生活随笔為你收集整理的重构:改善饿了么交易系统的设计思路的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Kafka创建查看topic,生产消费指
- 下一篇: Kafka Manager 编译 + 部