電商微服務體系中分層設計和領域的劃分

 來源:https://www.cnblogs.com/jurendage/p/13795640.html

 前言   

比起 “高併發、多線程”、“分佈式 CAP、一致性、Paxos”、“高可用 SLA” 等具體的乾貨技術點,軟件體系知識顯得很“溼”,似乎人人都有自己的認識,但又很少有人能說完整。

有一點可以確定的是,如果你未來需要獨立設計一個複雜的系統中臺,並使之未來能快速應對各種需求變化的話,科學合理的領域劃分和邊界界定需要我們 “處女座級” 的堅持下去,這對防止人力失控、減少項目爛尾很有幫助。合理的界定了邊界後,即便某個微服務很糟糕,也可以就輸入輸出以很少的人力投入進行重構,相反的就是牽一髮而動全身,加上業務需求頻繁而來,很容易爛尾或是達不到如期的效果。

其實很多技術大神都是某一個技術點的好手,但可能在整體軟件體系上思考並不多,每個人都有自己的設計方法,大部分容易想到的設計方法處理一般的系統已經夠了,後面發生問題慢慢打補丁就行了,當我們面對各種需求變化陷入開發困境的時候我們就該想想了,咱們系統的體系設計上是否出了問題?本文不打算涉及領域建模和設計模式等代碼級別的詳述,而是探討如何將一個複雜的大系統進行分層和拆分,這是設計一個優美系統的第一步,相信對各 BU 同事們快速搭建系統中臺也是很有參考意義的。

文中的一些例子大家也可能遇到過,大家如果在開發中遇到困境,可以多來圈子交流和發表問題,大家一起學習進步。大概知道內容背景的可以直接跳到第 3 部分。想了解一個大項目如何進行科學人員安排的可以直接看 5.4 部分。如果你的組裏還有人把數據庫模型當接口契約用,可以建議他看下 5.1 部分。假如你在開發過程中遇到一些別人的開發設計習慣,你覺得不是很好,但是又不知道如何說服他,都可以到評論區聊聊,大家一起討論討論。

摘要

本文闡述了一種將分層設計和 DDD 領域設計思想應用於微服務體系架構的方案實踐,也是個人的最佳實踐。對於大部分互聯網公司來說,我們主張將其 Web 服務架構分爲五層:基礎設施層、領域服務層、應用服務層、網關層和用戶界面層(表示層)。

領域服務層和應用服務層均可以採用微服務設計進行拆分,其中領域服務層將按照 DDD 領域設計進行領域劃分,設計爲一個個領域模塊微服務,每個微服務高度內聚,僅關注自己的業務,領域服務間通過接口調用進行松耦合。這種設計方案可以大大簡化大系統,並且在後期的維護中優勢會日漸凸顯,然而把大系統分而治之拆成微服務同時也對架構師和開發人員提出了更高的要求。第 2 部分介紹了相關背景,接着第 3 部分探討了分層設計以及每一層的功能,第 4 部分結合微服務和 DDD 對領域服務層進行服務模塊劃分和設計。第 5 部分則就分層設計和 DDD 領域設計中常見的問題進行了整理。

背景介紹 

想寫這樣一篇文章很久了,雖然本科學的是軟件工程,但礙於自己能力有限,從 08 年寫代碼以來一直斷斷續續的思考,始終對項目模塊設計和分層結構設計沒有一個可以讓自己覺得滿意且無糾結點的答案,假設了某個設計,很快在實踐中又會發現其存在着一些問題。

直到 2014 年畢業工作了解了 DDD 領域驅動設計後,纔有了相對清晰的方向。實際上早在 2004 年,Eric Envas 的《領域驅動設計:軟件核心複雜性應對之道》就已出版,畢竟軟件開發自計算機普及以來已經存在很長一段時間了,早期國外程序員對軟件開發理論的研究也十分興盛,如今成熟後反而研究的相對少了,基本上依葫蘆畫瓢即可。

DDD 領域驅動設計對軟件設計各個環節的人員都有較高的要求, 用《領域驅動設計》一書的話來說它需要一個 “領域驅動團隊”[1],它要求從分析階段,產品經理、項目經理、架構師以及開發工程師就使用統一的模型語言(Ubiquitous Language)來進行溝通,並且他們都懂一些代碼、產品和建模相關的知識,事實上這在國內很難實施,國內的產品經理約等於需求整理工,對其計算機基礎的要求是少之又少,在我所從事的公司裏,也曾發生過產品經理直接指導開發,以至於後面雙方理解的同一個詞有着不同含義的情況。

所以本文不打算去闡述 DDD 領域內部建模代碼級別的實踐,甚至本文並不認爲貧血模型是不好的,本文主要探討領域之間的劃分和分層設計,正如引言說提到的,這是設計優美系統的第一步。另外提一句:其實合理設計的微服務體系中的服務本身就是功能單一邊界清晰的小應用,屆時貧血也好、DDD 領域建模也好,其實都可以勝任。

近年來,隨着分佈式的發展,傳統中小型機集中式服務器已經不在流行,所以微服務體系也成爲了各大互聯網公司主流的選擇。直觀的感受下微服務和 DDD 兩者,似乎一個是微系統,另一個則是大系統的設計方法,似乎兩者天生互斥,微服務化的小系統也用不着 DDD,其實並不是,DDD 是針對整個複雜的軟件解決方案的一種科學設計方法,微服務化也是把複雜的大系統拆分爲小系統,方便維護和管理,所以兩者都有一個特點——爲複雜的大系統服務。

下面咱們就來探討下,如何把 DDD 的領域設計和其主張的分層設計應用到微服務體系架構中。需要說明的是本文主要是個人多年來的一點總結,未必適合所有場景,有更好通用性更爲廣泛的方案請不吝賜教。

分層設計

準確的說分層設計(Layered Architecture)跟 DDD 沒有必然的聯繫,我最早接觸分層設計是在攜程網,當時內部使用的應該只是簡單的業務層(Biz)和表示層,數據庫訪問之類的也是放在各自的業務包下的。

後來接觸和學習了《領域驅動設計:軟件核心複雜性應對之道》,書的第 4 章 “分離領域” 中說到了四層分層設計,即:基礎設施層、領域層、應用層和用戶界面層(表示層)。

DDD 產生的年代微服務還未流行,當時甚至基於瀏覽器的 Web 應用都比較少,更多的是 PC 軟件和 EJB 等網絡應用,所以作者更多的是想表達對複雜系統的邏輯分層,並不在意每個領域是單獨的系統還是一個軟件系統內不同的模塊。所以爲了跟其做區分,我們建議的四層爲在其基礎上引入 “服務” 兩個字,即:基礎設施層、領域服務層、應用服務層和用戶界面層。這樣做的意圖是讓開發人員立刻可以瞭解到——每個領域模塊即一個微服務(一個領域可以對應一個或者多個模塊 Module)。

摘要中提到我們主張的分層體系中還有一個層,即網關層,這又是什麼鬼呢。剛剛提到的 DDD 的時代背景,PC 軟件系統或者企業內部使用的網絡應用系統是根本沒有網關層(有也是網絡網關設備)這一說的,而現如今互聯網公司產品的輸出形式無外乎 Web 應用(網站、或者網絡服務),並且爲了更好的適配 PC 站和 App,一般會採用前後端分離的應用設計方案,這時候會產生一個需求——內部網絡應用系統如何把自己的服務輸出到互聯網上,供外部系統或者瀏覽器網頁訪問。最直接的方式就是把應用層直接暴露在公網上,但我們不建議這麼做,應用層服務更多的是關注業務應用,對網絡級的系統安全性(防 DDOS、釣魚、跨域等)、請求監控等缺乏考慮,這些工作交給網關層統一管理會輕鬆很多(比如淘寶的 TOP 平臺)。

這時候我們在 Web 應用系統中引入網關層用於銜接表示層和應用層 ,因爲這樣可以更好的劃分各層的職能。

網關層也可以看作是應用服務層的對外包裝層。如果一定要把網關層做到應用服務層裏理論上也是可行的,比如針對於 Spring Cloud 這種框架下的微服務體系,可以考慮直接暴露應用層,只需輔助一些運維手段進行統一的安全驗證和監控即可。假設我們選擇引入網關層,那麼我們就得到了以下網絡應用系統分層體系:

其中,各層的職能和作用爲 [2]:

各層除了實現自己的功能外,還需要遵守以下原則:

  1. 每一層設計保持內聚,並且只依賴於它的下方的層。

  2. 下層向上層發起的通信只能通過中間件等間接方式進行。

  3. 上層和下層只能有鬆散耦合(各自爲獨立個體,通過簡單引用關聯)。在某些微服務框架比如 Dubbo 中,可以把 api 包提供給上層引用即可。這也符合依賴倒置原則。

這裏重點說明應用服務層和領域服務層之間的關係。舉一個我經常跟部門其他開發舉的一個例子:有一家上市企業 A 公司,靠賣水果發家,其首席架構師科學合理的按照 DDD 搭建了一套基於微服務體系的賣水果應用,其架構圖如下:

今年水果行情一般,而房地產十分火熱,A 公司高層發現房地產帶動的五金行業也十分火熱,於是下達任務給技術部,要求其立即着手搭建五金銷售系統,貨源已經談好。

得益於首席架構師之前優秀的架構設計,他發現只需要做一個賣五金的網站以及另外對微服務進行微量的調整即可滿足老闆的需求——因爲賣五金和賣水果並無本質區別,他們涉及的環節幾乎一致。加入五金售賣的系統架構圖如下:

可見應用服務層代表是某一個業務應用,它代表的更多的是從需求出發的應用定義,而領域服務層則是業務領域按照自身的邊界進行設計的一個高內聚的服務體。應用層通過協調和組合各個領域服務即可形成一個新的應用服務。

《領域驅動設計》中明確指出,在設計領域服務時無需考慮表示層和持久層服務的東西。我在現實開發中總是遇到大量工程師按照產品的設計稿一溜煙的從上至下設計應用層服務和領域層服務,完全沒有考慮業務領域的概念,導致後面微服務數量膨脹,功能重複度高。這種開發習慣代表的是《領域驅動設計》作者極力吐槽的一種模式——SMART UI “反模式”[5]。

  領域劃分和微服務化   

根據 DDD 理論,領域建模主要發生在領域服務層,各領域模塊都應該是高內聚低耦合的,具有清晰的業務邊界。

本文不打算討論具體的 DDD 建模(服務,工廠,倉庫,實體,值對象,聚合等),這需要對 DDD 有較深入的研究,就目前所從事過的公司來看,似乎沒有一家真正嚴格按照 DDD 進行項目代碼設計的,就像摘要中說的,這對整個軟件工程鏈路上的人員都有較高的要求。有機會可以單獨寫一篇關於自己對 DDD 建模的思考和建議,本文更多的是討論高視角下的領域服務拆分,從而搭建一個低耦合高內聚的微服務體系。如果一定要將微服務和 DDD 聯繫起來的話,領域層的微服務就對應了 DDD 中的領域模塊 Module,每個 Module 由多個 Service 模式對象以及對應的模型對象(實體, 值對象以及它們的聚合)組成。

從《領域驅動設計:軟件核心複雜性應對之道。》中我學到的主要有兩塊:領域設計思想和領域建模模式。本文更多的是對前者的運用,後者的對立模式是貧血模型,大家日常用到的也都是貧血模型,我也覺得貧血模型有存在的必要性,所以本文我們主要從其中借鑑一下領域設計思想。本文所描述的設計理念,並不影響具體的模型設計方法,我們仍然可以在每個微服務中使用 DDD 領域建模。

如何切分領域模塊並沒有一個明確的規則,不同的場景下可能相同的業務塊邊界也不盡相同。這裏提幾點領域劃分的個人心得:

  Q&A  

能不能在所有層使用數據持久層模型,簡單快捷?

大家一定聽說過不同層的數據模型的叫法不同的概念,比如數據持久層的模型對象叫 DBO(database object)或者 DPO(data persistence object), 領域層的模型對象叫 DMO(Domain Model Object)或者就叫 Model,數據傳輸層的模型對象叫 DTO(Data Transfer Object)。那爲啥要這麼多模型呢,直接使用 Mybatis 等 ORM 框架生成 DBO,然後一路吐給前端不是更爽(還真有同事嘗試立項寫 Mybatis 插件來實現這種所謂的代碼自動化)。

我個人建議如果您真的是要搭建一個複雜的大系統,大平臺,一定不要偷這種懶,最好的就是做到” 一層一模型”(網關層使用應用層模型即可)。各層之間採用手動的數據賦值(getter,setter)來完成,或者使用一些轉換框架來簡化轉換代碼,個人在用 getter/setter 時感覺並不會耽誤什麼,在一個個 set 的時候,恰好可以對模型的字段細節進一步確認,並且拒絕使用 BeanUtils.copyProperties() 這種工具類,因爲這樣的工具類會讓” 一層一模型” 形同虛設,開發會熱衷於把 DPO 拷貝到領域中換個名字以保證可以用拷貝工具。下面我們來細談下不能在每一層都是用數據持久層模型的具體原因:

剛開始推廣” 一層一模型” 的時候,會有耍小聰明的開發去把下一層的模型 POJO 直接拷貝過來改個名字,然後用 BeanUtils.copyProperties() 完成賦值,這樣跟直接使用數據持久層模型就沒有區別了,所以要杜絕這種情況的發生。

爲啥需要應用層,領域層微服務直接通過網關暴露不就行了嗎?

對於習慣了單體應用開發者來說,一個微服務很可能就直觀對應成了一個個垂直的應用服務,每個服務間的關係是這樣的: 

 其實這樣的體系本質上仍然不能解決軟件的複雜性,這只是把系統簡單粗暴的拆分了,耦合問題仍然很嚴重,甚至這很有可能比原來的單體應用更復雜(多對多依賴),如果使用微服務體系來處理複雜系統,其服務體系應當是這樣的: 

這兩幅圖的區別在於,其實第一幅圖中的每個服務都包含了完整的 2~3 層,所以不再需要單獨的應用層。而第二幅圖各個領域模塊互相協作,對外提供服務時,則需要有一層直面用戶需求的應用層。

達成了微服務體系是解決複雜系統的出路之一這個共識後,我們再來看” 應用層服務存在的必要性” 有哪些理由:

爲了加深對應用層的理解,我們舉個代碼的例子,假如我們寫一個很簡單的首頁應用:

Response getHomeData(Request request){
    String nickName = userService.getNickName(request.getUserId()); 
    OrderInfo orderInfo = orderService.queryLatestOrder(request.getUserId());
    CostTrend costTrend = payService.queryCostTrend(request.getUserId());
    Integer points = pointService.queryAvailablePoints(request.getUserId());
    return new Response(nickName, orderInfo, costTrend, points);
}

這裏的 4 個服務類實例 userService,orderService、payService 和 pointService 如果都是本地的方法,那麼這就是一個單體應用,而微服務化後這 4 個可能都是微服務了,但是應用層應用的結構還是可以不用變化(現在很多的 RPC 框架都做到了與調用本地方法無差別)。這就是應用層的位置所在。

什麼是反模式?

這裏的反模式是指《領域驅動設計:軟件核心複雜性應對之道》這本書裏提到的與 DDD 相違背的模式,也是 Eric 極其反對的一種模式,即 SmartUI 模式(注意反模式不等於 SmartUI,只是在本書中作爲一個反模式的例子而已),這是一種什麼樣的模式呢,其實我很早之前做 C++ Builder(和 Delphi 很像)的時候還不知道,C++ Builder 就是一種 SmartUI 模式。

但其實 SmartUI 並沒有錯,對於小規模的 PC 本地應用開發來說也是有很多好處的。舉個例子,C++ Builder 中在窗體上添加一個按鈕,然後雙擊按鈕添加事件,這樣就跟實際操作的時候有機的結合了起來。

換句話說就是使用界面驅動業務開發。在大型系統的開發上,這種模式是害人精,我很理解 Eric 爲啥這麼討厭它。曾有一次我帶領着一個團隊做封閉式開發,在過完產品需求後,家裏出了點事我請了幾天假,回來後發現產品經理竟然指揮讓開發按照 UI 原型來設計數據庫,我審覈的時候發現這些開發設計的表有極其多的冗餘,而有一些重要的過程變量值卻沒有考慮到。比如他們會爲每個頁面建幾個表,這顯然是行不通的,科學的方法是拆分領域,每個領域自己建立自己的表。UI 只是應用層整合了各領域服務的數據並且處理後輸出的一種展示。

分層設計的開發步驟是怎樣的?

假設我們以一個標準的 SaaS 項目爲主,也就是表示層是前端頁面(可以是 APP,H5,M 站,小程序,PC 站等),那麼高效的一種開發步驟可以是這樣的:

  1. 業務、產品、開發 PM 進行需求評審(可行性等)

  2. 產品準備好原型

  3. 產品、開發(前後端)、架構師(或有架構師能力的資深開發)開會過 PRD,瞭解要做什麼

  4. 架構師開始設計領域(資深架構師一下午就能搞定),前端開始切圖,應用層開發開始按照 UI 和 PRD 設計前端每個頁面使用的 Restful 接口(比如直接 Springfox 代碼生成 Swagger)

  5. 架構師設計完領域後分工給領域層開發,進行領域邊界明確,然後領域層開發開始設計數據庫表等。

  6. 這樣前後端開發就同時開工了。

  7. 開發初步完成後,自測加連調。

  8. 後續就是測試發佈了。

具體階段和時間線可以參考下圖:

總結

做 Java 生鮮電商平臺的互聯網應用,無論是生鮮小程序還是 APP,微服務系統是非常重要的,本文只是起一個拋磚引玉的作用,希望用生鮮小程序的微服務拆分的實戰經驗告訴大家一些實際的項目經驗,希望對大家有用。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uNSH6LOERSGen4v0kC5hZw