一、用領域驅動來把握真正的業務需求
如果把我今天的分享比作一個故事的話,那麼故事的主線是:領域驅動設計幫助我解決了工作的難題。這個難題表現在兩個方面,首先無路可退 :入職第一個任務,做不成就意味著回家。其次是左右為難 :實現技術重構的目標,滿足不了業務需求!不去實現,又不知道該做什麼?
在進入到主題之前,我們先來從商家視角瞭解一些電商業務的背景知識。一個商家想要賣自己的商品,不管是線上下實體店面,還是線上電商,至少要有賣貨,收錢,發貨三個環節,如果我們從網上找一個開源的商城系統,也會包括這三部分的功能。然而,對於商家來說,只有這三個環節是不夠的,還需要有一個非常重要的環節就是算賬。我分享內容,就是和收錢,發貨,算賬相關的。
我們再來看一下得到APP電商業務涉及的組織和系統。我所在的聽書屬於業務後端,主要負責賣貨和發貨環節,而收錢環節需要由交易平臺組和基礎平臺組提供的服務來完成,算賬相關的由財務平臺組負責。大家先對紅框的三個系統有個印象,後面會頻繁的提到。
得到app目前如何確認收入呢?財務部門在算賬交稅前,首先要確認收入,賣了多少商品,收了多少錢,實收賬款和應收賬款能不能對上。使用者在APP內下單後生成訂單記錄,收到錢後生成支付記錄,發貨生成權益記錄。虛擬商品的發貨不需要快遞,就是我們在表裡面寫一條資料。財務部門需要把三張記錄表中的每條資料都用訂單號關聯起來,並且狀態含義嚴格匹配,才能確認為是收入,否則就會成為壞賬或者呆賬,需要人工處理。
然而,開始的時候,並不是這樣的。使用者在APP中購買虛擬商品,我們沒有生成訂單記錄,只有支付記錄和權益記錄。當出現數據不一致時,就給財務確收帶來了問題。主要分為兩類,只有支付記錄沒有權益記錄資料,是有支付無交付,我們收了錢沒發貨;而只有權益找不到對應的支付記錄的資料,是有交付無支付,發貨了沒收到錢。每個財務結算週期,兩個開發組的人,幾乎都要去排查問題資料。你也許會好奇,一個電商平臺居然沒有訂單?我相信“存在即合理”,當時這麼做肯定有當時的原因和背景,說白了一切都是為了快速上線,快速驗證得到app的商業模式,活下去對於創業公司來說,比設計實現一個完美的系統優先順序更高。
我們看下沒有訂單的情況下,系統之間的呼叫關係。為了說明核心問題,我挑選了流程最簡單的節操幣購買的方式,節操幣是得到APP內的虛擬貨幣,節操幣系統是由我們的基礎平臺組負責的。以購買聽書的內容為例,APP去請求聽書系統的購買介面,然後由聽書系統呼叫節操幣系統完成扣款,扣款成功後寫入已購。
這張圖是沒有訂單時,聽書業務實現全部售賣方式的呼叫關係,從圖中我們可以看到,聽書系統需要直接與許多外部系統互動,系統間的依賴和耦合是比較高的。
我們再回到財務結算的場景。由於沒有訂單,給財務核算工作帶來很多問題,財務就要求必須記錄訂單及交易狀態。於是各業務系統就花時間去改造,使用者購買時先呼叫訂單系統的CreateOrder來生成訂單記錄,支付完成後要呼叫PayOrder來標記訂單的支付狀態,發貨後呼叫SignOrder來把訂單簽收,我們內部把增加訂單的這幾個動作叫做“訂單化”。實現了訂單化以後,財務就可以按照訂單記錄、支付記錄、權益記錄的嚴格匹配來進行核算與確認收入了。
我們看一下實現了“訂單化”後的呼叫關係,聽書系統需要增加3個請求去呼叫訂單系統,而原來的每個介面都要傳遞訂單ID。例如建立完訂單以後,在扣除節操幣的環節,要告訴節操幣系統是為具體的哪筆訂單扣除餘額的。
實現了訂單化後,業務系統對外部系統的依賴和耦合有些加劇,增加了對訂單系統的三次呼叫,同時要給別的系統呼叫請求中傳遞訂單號。
上線後沒多久,原系統架構帶來的痛點很快就出現了。財務要求在訂單加個“簽收時間”欄位,據說20多天才上線!為什麼需要20多天,因為所有的業務系統都要修改相關的程式碼,而測試人員要把所有業務的購買都要回歸測試一遍,在需求開發時間非常緊張的情況下,投入與
財務又提了需求,要求儘快把所有交付的內容實現“訂單化”,因為我們除了有資金往來的購買,還有很多使用者免費領取的方式,這些也是要核算成本和交稅的。如果再來一次前面那樣的修改,可要命了。於是團隊思考該怎麼辦?結論是內部實現個系統,代理全部訂單相關的功能,這樣再有修改,只改這個代理服務就行了!實現隔離變化!
這就是我接到的重構任務:訂單代理(訂單化)系統。實現 一個代理服務,對接 交易平臺組的訂單系統和基礎平臺組的支付系統,推動若干個業務系統改造,改成呼叫新的代理服務。
我們看下訂單代理系統是如何隔離了變化的。從圖中我們看到,業務系統不在直接依賴外部系統的了,訂單相關的引數由代理系統去組織,如果再有訂單相關的修改,只改這個代理服務就行了。還以增加“簽收時間”的欄位為例,業務系統就不需要關注這個欄位了。
表面看,我們的設計方案, “同時滿足”了業務需求和技術目標。業務需求:所有的商品都實現“訂單化”:技術這邊不光都實現“訂單化”,還實現個“訂單化的代理系統”,應對外部系統的變化。
這張圖很多人一定都看到過,他描述的是對使用者需求理解偏差造成的軟體專案的失敗,在我這個場景,方案確定了!但這是業務的目標嗎?
我讀了些程式碼並做了系統分析後,帶著掌握的內容,滿懷信心的去和合作部門交流,卻感覺大家關注的點甚至方向都常常不一致!!!在需求理解上,我們和業務方存在認知的偏差。開發最關心的是,完成全部商品的訂單化,實現訂單代理系統,降低業務系統與外部系統的耦合。而業務關心的是,一定要正確的交付(面向現在),能夠高效準確的算賬(面向未來),把過去的賬給解釋清楚(面向過去),我給總結為三個面向。訂單代理系統的目標在財務那裡只是個過程!他們甚至絲毫不關心我們是不是要實現一個訂單化的代理系統。真正的目標需求是什麼???如果一個系統還沒有開始做,你就知道即使做完也達不到業務的目標,那種心情是很糾結的。
這就是前面說的,我面臨的挑戰。首先是無路可退,入職的第一個任務。第二是左右為難:實現“訂單代理系統”,滿足不了業務需求!如果不去實現“訂單代理系統”,那該做什麼?
思考了沒有把握到真正需求的原因。技術人員理解到的所謂“需求”是一種內部視角,是有侷限性的。而業務方是外部視角,看到的要比我們全面。領域驅動設計,它能讓技術人員和業務都能從外部視角——也就是領域來看問題。
DDD思想指導的開發過程,是一個全程強調領域的過程,開發人員和領域專家,從業務需求中提煉出【統一語言】,基於統一語言建立【領域模型】,用領域模型指導設計及編碼實現。
從財務的訴求中,我們把握到了需求的問題域是:電商的發貨與算賬,而業務的期望是精確交付。
為了讓自己的理解和產品經理想要表達的不產生太大的偏差,當天結合這個草圖,趕緊畫了一個自己理解的圖,第二天又去給產品經理講了一遍。反述的過程,自己明白訂單化在全域性的位置,雖然貌似不起眼但是卻擔負著得到所有虛擬商品的交付。
經過繼續深入調研後,把“訂單化”要完成的內容,劃分成了支付和交付兩部分,而所在的得到後端,應該關注得到商品的交付部分。
和業務的充分溝通與協作,漸提煉出來了一些“統一語言”,成為了我們的交易領域內的業務語言。例如,訂單完整的生命週期:下單,支付,交付,簽收。而確收的內容指的是:已購權益和時間權益。
在領域驅動設計思想的指導下,我們找到了真正的業務需求,並不只是開始的,實現一個訂單代理系統那麼簡單,而是要實現“財務核算級別的精確交付”。
二、領域驅動設計指導架構設計與建模
在第一部分,我們找到了真正的業務需求,第二部分,來介紹一下“領域驅動設計指導架構設計與建模”。在這部分,業務的真正需求將會落地。
我們再來看一下電商業務的基本模型。我們在電商平臺所完成的的購買行為,其實就是買賣雙方,圍繞著交易物,以雙方認可的價格簽訂合同後,展開的履約行為。簽約生成的合同,就是訂單,買方付錢後合同開始生效,賣方收錢發貨,買方收貨簽收後,合同結束。賣方還要具備賣貨,算賬等能力。
我們想一下,當一個人或者一個組織具備了電商業務的全部功能後,他是不是就可以成為“個體戶”或者“小商販”了呢。“個體戶”和“小商販”用自己勞動換來回報,值得我們尊敬。但是在一個電商平臺中,每個業務系統都把自己做成“小商販”系統,並不是件好事。
我們再來看下訂單代理系統架構的弊端。首先,每個業務都是個“小商販”,相同功能的程式碼依然會重複。即使業務系統把原來直接呼叫外部系統的方式改成呼叫訂單代理系統,交付資料的準確性依然達不到財務要求,因為無論是業務系統的開發還是產品經理,對於交付領域和交付資料都缺乏足夠的敏感度,畢竟術業有專攻。
小商販模式能夠不那麼優雅的解決技術的問題,但卻不能滿足業務“財務核算級別的精確交付”的需求,因為交易領域中缺少一個專注交付的子領域。於是,我們重新理解和確定了領域問題。在前面的概念模型中,我們得到了幾個概念:交易是基於合同的行為,訂單是合同,交付是履約。得到後端的核心子領域問題:是“履約”是交付,到此,一個訂單交付系統就呼之欲出了。
在我們這個場景中,業務系統實現了交易的全部功能,就是個“小商販”。每個小商販都賣貨收錢發貨,而和現實中小商販不同的是,每個商販都不對賣貨產生的賬務資料負責,而賬務卻是由一撥人算的。我們透過DDD的限界上下文劃分來分析,就會發現,應該把每個業務上下文中和交易相關的功能獨立出來,把自己變成一個商品的供貨商,入駐超市賣場,讓專業的人去做專業的事,業務系統自己變為“超市賣家”。
由訂單交付系統接管業務的交易行為,原來聽書商品的購買,變成了這樣的呼叫關係。我們的訂單交付系統前置,直接和app對接,建立訂單和交付統一由交付系統負責完成,原業務系統只需要提供一個返回商品價格、上下架狀態的介面,就可以完成售賣。我們用這樣的方式,讓訂單交付系統從所有的業務方接管了售賣交付行為,而業務方几乎不需要做什麼開發,甚至感覺不到訂單的存在。
最終,訂單交付系統滿足了業務和技術的目標。業務方不再是“小商販”,入駐“超市”成為“賣家”,交付的資料達到財務精準核算的要求。綠框部分,就像一個超市,業務系統將自己的商品上架後,就不需要關注收錢、交付和算賬了,可以把更多的精力投入到自己業務產品的研發中,
三、使用限界上下文來保護領域
首先,我們來認識一下,強調上下文的重要性。我抽象了一個乘客到飛機場登機的例子。
機場,為了維護自己的秩序,是要採取許多措施來保護這個領域的。而我們識別了領域後,也要保護領域。保護是手段,目標:邊界內的“完美世界”不可侵犯,完美世界是什麼,我們想一下架構優良,職責單一,程式碼整潔等等讓人感覺到美好的詞語。保護的依據:就是識別了限界上下文後劃分的邊界,以及限界上下文內的規則。
在訂單交付系統開發和推進的過程中,採取了一些保護這個領域的措施,我按照上下文之間和上下文內部總結為如下的分類。上下文之間,第一、規範可以進入上下文內的物件模型,第二、用領域事件解耦合與其它上下文的關係,第三、把握領域的職責,第四、隔離上下文間的業務內涵。上下文內部,保護業務抽象行為的一致性。後面我們結合例項,來看一下具體的行動措施。
1、保護領域的行動:規範可以進入上下文內的物件模型。
《領域驅動設計》一書中解釋限界上下文時,Eric Evans 用細胞來形容限界上下文,認為“細胞之所以能夠存在,是因為細胞膜限定了什麼在細胞內,什麼在細胞外,並且確定了什麼物質可以透過細胞膜。”當進入機場上下文的時,“人物”要變為“乘客”;當進入訂單交付上下文的時,“業務物件”要變為“商品”。
為什麼要這麼做,因為在之前,進入各業務上下文的模型是由業務自己決定的,客戶端結算臺是根據許多的if……else來判斷該請求哪個業務系統的介面。而由於缺乏領域規範,這些引數形態各異,業務端的實現也是各不相同,導致出現問題排查排查困難。
所以,在新的訂單交付系統推廣的第一步,就是透過統一引數來規範進入交付領域的物件。App開發新的功能和我們對接時,我們要求不管原來是什麼型別的業務物件,進入訂單交付領域,必須轉換為商品,必須傳遞ProductID + ProductType。而對於老版本的業務,我們透過直接在閘道器轉發時,把介面轉到我們的防腐層,轉換為商品再去呼叫交付邏輯。
2、保護領域:用領域事件解耦與其它上下文的關係。
由於購買資料有很多系統會關注,所以在之前,每記錄一條使用者權益,就會至少給四個相關上下文推送格式大同小異的訊息供相關方消費,浪費資源不說,維護成本也高,還被外部的上下文業務規則所綁架。磐石,關心商品sku和價格,勳章關係價格和數量,已購只關心數量,大資料方面關心商品和價格。4個需求方,不同時間段提的需求,由於沒有之前沒有領域,就都是定製開發。在重構新的訂單交付系統時,我們認為交付結果推送的訊息是一個領域對外提供的服務,應該統一由領域來提供標準外部去適配,而不是外部提要求我們來定製開發。於是,就在記錄使用者權益後,釋出統一的領域事件,由外部系統的上下文來訂閱和適配。
3、保護領域:把握領域的職責,領域之外的事少管。
在之前,使用者的充值工作完全是由得到後端來全權負責的,要反覆和訂單系統和支付系統互動,前後至少要經過9次排程和響應,極易出現問題,所以在財務對賬中,充值也是被財務吐槽比較多的一個地方。前面分析過,交付的節操幣這個商品其實不屬於得到後端的商品,我們做了那麼多,是出力不討好。
交易中心是我們中臺化的一個產物,這個系統接管了包括訂單和支付的所交易行為,所以在和交易中心對接時,我認為既然節操幣不屬於得到後端,就不應該由我們來負責交付,於是我們就把節操幣充值的交付,“讓”給交易中心。充值行為變成了這樣的呼叫關係,我們的訂單交付系統比原來少做了很多事情,全域性的充值結果卻更有保障了,因為減少了不必要的呼叫。紅線部分,是完成同樣的充值行為,我們訂單交付系統需要做的事情,連訂單的支付和簽收也不需要我們關心了。
4、保護領域:使用上下文隔離相同事物的不同內涵。
之前,使用者在得到商城內購買一個課程,要先產生一個商城訂單,然後再推送給得到後端,會再生成一個得到的訂單並交付簽收。在財務審計和對賬中,這個邏輯也帶來了很多的問題資料,因為兩筆訂單的對應關係是很不穩定的。一次購買,產生兩筆訂單,並不符合我們這個場景。
一次購買,產生兩筆訂單的適用用場景,適用於兩個商家彼此之間是完全獨立核算的場景。我之前有個同事,開了個網店,但是她一點貨都不存。她是怎麼玩的呢?從網上把別的商家的圖片商品資訊抓過來,在自己的網店上加10~100塊錢不等就上架,當用戶在她的網店下單後,她再去別的網店下個同樣商品的訂單,收貨人填買家。由真正的賣家發貨。這個場景下,雖然看著是同一個東西,但內涵上其實是兩個不同的事物,如果嚴格點,肯定是兩個不同的sku。
而我們這個場景,只收了一份錢,內部也沒有再次產生資金流水行為,商城上下文和我們訂單交付上下文應該是合作關係,一起接力完成一筆訂單的交付。雖然在不同的上下文中,表現為訂單和交付單兩個不同的東西,但內涵上是同一個事物的不同狀態。
於是,在對接這部分業務時,我們打破了原有規則,說服大家接受商城購買是一種下單途徑,流轉到得到這邊,是訂單履約的方式,就是一個訂單。這樣做以後,商城同步售賣的訂單核算,可以和其它支付型別購買的商品採用一樣方式核算了,技術人員再也不用給財務導兩筆訂單的對應關係了。
5、保護領域:保護業務抽象行為的一致性。
我們先看一下訂單交付系統實現層面的設計。使用了橋模式,一邊抽象了支付方式,另一邊抽象了商品的交付。中間透過統一的交付邏輯來控制。
在重構對接老業務的時候,遇到了一個歷史產品,會破壞“業務抽象行為一致性”,會破壞我們的完美世界,使我們的程式碼和架構壞腐。使用者只要購買過這個產品,那麼在有效期內可零元購買“得到app”的所有內容,為了實現這個產品功能,程式碼中幾乎所有購買場景,都要增加if……else……,即破壞設計,汙染程式碼。這個產品一張一年,而有的使用者,一下子把這個產品的有效期買到了2037年。
記得一次開會,我們的老闆說過這個產品未來可以下線,我就記下了。所以當這個老的產品要再來破壞我們領域內的完美世界時,我就決定有法可依的去推進這個產品的下線。當選擇不寫程式碼去解決業務問題,必然就要去做很多溝通協調方面的工作。這雖然是一個極端的例子,但是可以說明我們技術人員要有保護領域的決心。
我分享的內容即將結束,最後來說一下,DDD指導的設計建模帶來的幾個長尾收益:可以快速支援商城直接售賣各種商品,可以快速接入書單等多商品打包的購買,可以快速接入各種產品的贈送功能,客戶端可以封裝統一的結算臺元件,其中最我認為重要的是我們可以逐漸從每月核查財務資料的工作中解放了……
最後的結語是:不要把DDD只當做一門技術來學習,ta可以是指導開發流程的方法論。