簡介: 相較於常規的MVC架構,DDD更抽象、更難以理解,各個開發者對DDD的解釋也不盡相同。那麼哪一種設計方式才更好?在學習時如何知道哪一種DDD更正統,沒有被別人帶歪?本文嘗試使用「DDD as Code」的概念,即用DSL代碼方式來描述DDD,統一DDD的設計思想,經過案例詳細介紹如何基於ContextMapper來完成一個項目基於DDD DSL的表達,並分享現實中DDD的設計流程和微服務的關係。java
網上有很是多關於DDD的文章,這固然是好事情,你們都想掌握好的設計方法來解決軟件開發中的問題。可是這其中也存在一些問題,若是你隨便打開網上的幾篇DDD文章,雖然每一位做者都說本身是按照DDD思路進行架構設計的,可是細心的同窗會發現每個做者DDD文章中的結構描述、畫的架構圖都千差萬別,你會很是奇怪,這些都是DDD設計嗎?這裏其實有一個問題,就是經過文字和圖示描述一些抽象的概念時,原本就會有很大的差異。你們不要用盲人摸象的概念進行類比,這個不太合適,即使兩個同窗,對DDD都很是瞭解,並且都實踐了好幾年多個項目,他們寫出來的東西仍是不同。我Java入行稍微早點,固然你說我年紀大保守也能夠,記得當初沒有那麼多中間件,就是基於Struts 1.x這個MVC框架進行開發,不一樣的同窗寫出的設計文檔也是千差萬別。這麼簡單的MVC架構都能有不一樣的架構設計文檔,而DDD相對更抽象、更難以理解,因此架構設計文檔長的不太同樣,這個也是能夠理解的。git
那麼咱們是否是要接受這個事實,「各個做者對DDD的解釋能夠沒必要相同」,"DDD設計文檔能夠以不一樣種形式呈現"?若是是這樣,那麼想學習DDD的同窗就有很是大的負擔,哪一種設計表現方式纔是比較好的,纔是比較容易理解的,同時我怎麼知道我學的DDD是相對正統的,沒有被別人帶歪。我不是說發揮性思考不能夠,可是從傳道的角度來講,尊重理論事實仍是須要的。程序員
咱們都知道代碼在表達一些業務或者邏輯時,很是能反應真實狀況,即使是不一樣的開發者所編寫,考慮到遵循Design Pattern、命名規範、開發語言約束等,代碼大致上仍是相同的,仍是便於理解的,若是有單元測試和Code Review,那就更好啦。這也是在一些文檔不完善的時候,很是多同窗選擇閱讀代碼,更有同窗說,「直接看代碼,不要看他們PPT和文檔,會被誤導的,否則怎麼死的都不知道」。另外咱們都知道,如今一個很是好的實踐就是Everything as Code,典型的如Infrastructure as Code的Terraform,Platform as code的Kubernetes YAML, Diagram as Code的PlantUML等等, 那麼咱們可否使用DDD as Code這個概念,讓咱們的設計更統一些,更能方便表達設計思想,更容易被人理解。github
用DSL也就是用代碼方式來表達DDD,這個很早就有了,可是更偏向DDD的戰術設計(Tactic Design)和代碼層面,如 Sculptor[1]和fuin.org DDD DSL[2] ,你們廣泛都認爲,就是基於Xtext的DDD代碼生成器。要費勁學習那麼多,只爲生成一些代碼,並且只是Java代碼,因此廣泛關注度並無多高。express
咱們可否將DDD DSL除了代碼生成這一部分,更偏向於戰略設計(Strategic Design),突出設計的思想,那麼DDD as Code就全面多了。接下來咱們就介紹ContextMapper這個框架。編程
名詞解釋一下:有很多同窗對於DDD的戰略設計(Strategic Design) 和戰術(Tactic Design)之間區分有些疑惑,DDD有專門的介紹,以下:安全
其實也比較簡單,戰略設計更大一些,偏宏觀,你能夠理解爲公司高層在討論的業務和技術方向,各個團隊或者產品的分工和配合;而戰術設計則相對小不少,主要集中在一個BoundedContext內部,好比如何設計DDD那些Entity,Service,Repository等,外加可能的應用開發的技術選型,能夠說更關注技術層面。架構
ContextMapper是一個開源項目[3] ,主要是爲DDD設計提供DSL支持,如DDD的戰略(Strategic)設計,Context間映射(Context Mapping),BoundedContext建模以及服務解耦(Service Decomposition),那麼咱們就看一下如何基於ContextMapper來完成一個項目基於DDD DSL的表達。app
在介紹ContextMapper時,咱們先交代一下項目背景。如花是一名架構師,對DDD也很是熟悉,並且有過幾個項目的DDD實踐,最近他加入會員線,負責完成對會員系統的改造,更好地配合公司的微服務化的設計思路。會員線以前就是三個應用:會員中心對外提供的大量的REST API服務;會員註冊和登陸應用;會員中心,處理會員登陸後如修改我的密碼、基本信息、SNS第三方綁定和支付方式綁定等。框架
如花加入會員團隊後,和你們溝通了基於DDD + MicroServices的架構思想,你們都表示贊成,可是如何落實到具體的架構設計和文檔上,你們就犯難啦。讓咱們看一下最典型的DDD設計圖:
其中的概念,如SubDomain、BoundedContext、Entity、ValueObject、Service、 Repository、Domain Event,以及Context映射關係(Context Mapping),這些都沒有問題,可是如何向他人表達這個思想?總不能每次都把DDD設計圖和分層圖都貼上去,而後說我就是按照DDD設計的。
如花開始DDD的第一步,也就是Subdomain的劃分。固然DDD中包括三種類型的SubDomain,分別爲通用(Generic)、支撐(Supporting)和核心(Core)三種類型,這裏稍微說明一下這幾者的區別:
這三者總體關係以下:Core是最不同凡響且花費精力比較多的,在複雜性Y維度,咱們要避免高複雜度的通用和支撐Domain,這樣會分散你的注意力,同時還要投入很是大的精力,若是確實須要,購買服務的方式可能最佳。
圖源:
https://github.com/ddd-crew/d...
如花首先將會員先劃分爲幾個Sub Domain,如處理帳號相關的Account,處理會員打標的UserTag,處理支付方式的PaymentProfile,處理社交平臺集成的SnsProfile,還有一個其餘Profiles,這裏咱們不涉及Generic和Supporting Doman的規劃,主要從業務核心Domain出發。一個同窗用PPT闡述了劃分結構和出發點,以下:
可是也有同窗說是否是UML的Component圖更好一些,方便和後面的UML圖統一,以下:
固然還有其餘如Visio等很是多的圖示工具用於展示結構圖。DDD的第一步:SubDomain的劃分和展示,就有不一樣的理解方式,如何描述、如何圖形化展示,都有很多的分歧。
回到問題的出發點,咱們就想劃分一下SubDomain,那麼是否是下述的DSL代碼也能夠:
Domain User { domainVisionStatement = "User domain to manage account, tags, profiles and payment profile." Subdomain AccountDomain { type = CORE_DOMAIN domainVisionStatement = "Account domain to save sensitive data and authentication" } Subdomain UserTagDomain { type = GENERIC_SUBDOMAIN domainVisionStatement = "UserTag domain manage user's KV and Boolean tag" } Subdomain PaymentProfileDomain { type = CORE_DOMAIN domainVisionStatement = "User payment profile domain to manage credit/debit card, Alipay payment information" } Subdomain SnsProfileDomain { type = CORE_DOMAIN domainVisionStatement = "User Sns profile domain to manage user Sns profile for Weibo, Wechat, Facebook and Twitter." } Subdomain ProfilesDomain { type = CORE_DOMAIN domainVisionStatement = "User profiles domain to manage user basic profile, interest profile etc" } }
雖然目前咱們還不知道對應的DSL代碼語法,可是咱們已經知道Domain的名稱、domain類型以及domain的願景陳述(visionStatement),至於後期以何種方式展示系統Domain,如表格、圖形等,這個能夠考慮基於如今的數據進行展示。其中的UserTagDomain類型爲GENERIC_SUBDOMAIN,這個表示打標是通用性Domain,如咱們後期能夠和商品、圖片或者視頻團隊合做,你們能夠一塊兒共建打標系統。
注意:Subdomain不僅是簡單包括type和domainVisionStatement,同時你能夠添加Entity和Service,其目的主要是突出核心特性並方便你對Domain的理解,如Account中添加resetPassword和authBySmsCode,相信大多數人都知道這是什麼含義。可是注意不要將其餘對象添加到Subdomain,如VO, Repository, Domain Event等,這些都是輔助開發的,應該用在BoundedContext中。
Subdomain AccountDomain { type = CORE_DOMAIN domainVisionStatement = "Account domain to save sensitive data and authentication" Entity Account { long id String nick String mobile String ^email String name String salt String passwd int status Date createdAt Date updatedAt } Service AccountService { void updatePassword(long accountId, String oldPassword, String newPassword); void resetPassword(long acountId); boolean authByEmail(String email, String password); boolean authBySmsCode(String mobile, String code); } }
ContextMap主要是描述各個Domain中各個BoundedContext間的關聯關係,你能夠理解爲BoundedContext的拓撲地圖。這裏咱們先不詳細介紹BoundedContext,你如今只須要理解爲實現Domain的載體,如你編寫的HSF服務應用、一個處理客戶請求的Web應用或者手機App,也能夠是你租用的一個外部SaaS系統等。舉一個例子,你的系統中有一個blog的SubDomain,你能夠自行開發,也能夠架設一個WordPress,或者用Medium實現Blog。回到微服務的場景,如何劃分微服務應用?SubDomain對應的是業務或者虛擬的領域,而BoundedContext則是具體支持SubDomain的微服務應用,固然一個SubDomain可能對應多個微服務應用。
既然是描述各個BoundedContext關係,必然會涉及到關聯關係,如DDD推薦的Partnership([P]<->[P])、Shared Kernel([SK]<->[SK])、Customer/Supplier([C]<-[S])、Conformist(D,CF]<-[U,OHS,PL])、Open Host Service、Anticorruption Layer([D,ACL]<-[U,OHS,PL])、Published Language等,詳細的介紹你們能夠參考DDD圖書。這些對應關係都有對應的縮寫,就是括號內的表述方法。這裏給出關聯關係Cheat Sheet說明圖:
圖源:
https://github.com/ddd-crew/c...
若是你自行畫圖來表達這些關係,必定有很是多的工做量,細緻到箭頭類型,備註等,否則會引起誤解。這裏咱們就直接上ContextMapper DSL對ContextMap的描述方式,代碼以下:
ContextMap UserContextMap { type = SYSTEM_LANDSCAPE state = TO_BE contains AccountContext contains UserTagContext contains PaymentProfileContext contains SnsProfileContext contains ProfilesContext contains UserLoginContext contains UserRegistrationContext UserLoginContext [D]<-[U] AccountContext { implementationTechnology = "RSocket" exposedAggregates = AccountFacadeAggregate } ProfilesContext [D]<-[U] UserTagContext { implementationTechnology = "RSocket" exposedAggregates = UserTags } UserRegistrationContext [D,C]<-[U,S] UserTagContext { implementationTechnology = "RSocket" exposedAggregates = UserTags } UserRegistrationContext [D,C]<-[U,S] SnsProfileContext { implementationTechnology = "RSocket" } }
你們能夠看到Map圖中包含的各個BoundedContext名稱,而後描述了它們之間的關係。在關聯關係描述中,涉及到對應的描述。前面咱們說明BoundedContext爲Domain的具體系統和應用的承載,因此涉及到對應的技術實現。如HTTP REST API、RPC、Pub/Sub等,如blog系統爲Medium的話,那麼implementationTechnology = 」REST API"。還有exposedAggregates,表示暴露的聚合信息,如class對象和字段,服務接口等,方便通信雙方作對接,這個咱們會在BoundedContext中進行介紹。
在ContextMap中咱們描述了它們之間的關聯關係,接下來咱們要進行BoundedContext的詳細定義。BoundedContext包含的內容相信大多數同窗都知道,如Entity, ValueObject,Aggregate,Service,Repository、DomainEvent等,這個你們應該都比較熟悉。這裏咱們給出一個ContextMapper對BoundedContext的代碼,以下:
BoundedContext AccountContext implements AccountDomain { type = APPLICATION domainVisionStatement = "Managing account basic data" implementationTechnology = "Kotlin, Spring Boot, MySQL, Memcached" responsibilities = "Account", "Authentication" Aggregate AccountFacadeAggregate { ValueObject AccountDTO { long id String nick String name int status Date createdAt def toJson(); } /* AccountFacade as Application Service */ Service AccountFacade { @AccountDTO findById(Integer id); } } Aggregate Accounts { Entity Account { long id String nick String mobile String ^email String name String salt String passwd int status Date createdAt Date updatedAt } } }
這裏對BoundedContext再說明一下:
BoundedContext的更多信息,能夠參考sculptor的文檔[4],根據實際的狀況能夠添加對應的部分,如DomainEvent、Repository等。
我的以爲這裏BoundedContext尚未涉及到Ubiquitous Language,仍是須要對應的輔助設計文檔,須要交代相關的項目背景,技術決策等等。我的是推薦採用C4架構設計做者編寫的 《Visualise, document and explore your software architecture》[5],很是實用,做爲DDD架構設計文檔,徹底沒有問題。
文章的一開頭咱們說到以前的DDD DSL更多的是代碼生成器,若是是代碼生成器,那麼生成的代碼必定有對應的規範和結構等,如entity、value object,service,repository保存的目錄,生成的代碼可能還包括必定的Annotation或者interface,標準字段等等。固然這裏咱們不討論代碼生成器的問題,但咱們但願你們的DDD架構設計仍是要採用必定的規範目錄結構,這裏有幾個標準推薦給你們:
這三者其實出發點都是一致的,就是在代碼層面來描述DDD,核心是一些annotation、interface,base class,固然也包括推薦的package結構。
講到這裏,其實DDD總體上來講,咱們已經闡述清楚:Domain劃分、總體Domain的BoundedContext拓撲圖和關聯關係、BoundedContext具體定義和架構設計文檔規範。可是ContextMapper還提供了UserStory和UseCase對應的DSL,讓咱們來看一下。
好多同窗都問UserStory如何寫,有了這個DSL,同窗們不再用擔憂如何編寫UserStory啦。這個DSL比較明確的,主要是三元素:做爲 「aaa",我但願能"xxx",我但願能」yyyy",以便 "zzz", 也是符合UserStory的典型三要素:角色、活動和商業價值。
UserStory Customers { As a "Login User" I want to update a "Avatar" I want to update an "Address" so that "I can manage the personal data." }
Use Case是描述需求的一種方式,在UML圖就有對應的UseCase圖,核心就是actor,交互動做和商業價值,對應的DSL代碼以下:
UseCase UC1_Example { actor = "Insurance Employee" interactions = create a "Customer", update a "Customer", "offer" a "Contract" benefit = "I am able to manage the customers data and offer them insurance contracts." }
在Aggregate聚合中,你能夠設置useCases屬性來描述對應的UseCase, 以下:
Aggregate Contract { useCases = UC1_Example, UC2_Example }
按照你的說法,咱們用DSL代碼方式來描述DDD,這個有什麼收益?
這種代碼方式,一目瞭然且很是規範。若是你代碼寫錯會有什麼問題,固然是編譯不經過,IDE都會幫你糾正。因此DDD DSL也是這樣,徹底無歧義。目前ContextMapper DSL包括Eclipse和VS Code插件,在IntelliJ IDEA能夠經過自定義File Types和Live template方式來輔助你編寫cml文件。
前面咱們聊到DDD DSL支持代碼生成器,能夠輔助你生成代碼,相信這個你們都能明白,由於DDD DSL代碼是標準的,基於這個Code Model生成其餘形式的代碼,這個固然能夠。
另外ContextMapper還支持其餘模型生成,如ContextMap圖形化展示、PlantUML的結構圖,對應的代碼在這裏[9]。我這裏給你們一些截圖:
固然ContextMapper還提供通用的生成器,也就是基於DDD DSL模型,加上Freemarker模板,而後就能夠生成你想要的各類輸出,如生成JHipster Domain Language (JDL)用於快速建立文件腳手架也不奇怪。相信不少Java程序員對此都不陌生,咱們開發Web應用時就是使用Freemarker生成HTML的。更多細節訪問這裏[10]。
咱們有了DDD DSL來描述咱們的架構設計,是否是就全面了,徹底夠用,開發不愁了呢。還不是,咱們知道在軟件架構設計和編寫代碼前,都有需求調研、客戶走訪、領域專家溝通、需求分析、研討等等,這個在現實生活中仍是少不掉的,其目的就是爲了後續的架構設計提供素材並作鋪墊。那麼如何將DDD和這些前期操做整合起來?其實DDD有涉及這方面的內容,如EventStorming卡片:
Bounded Context Canvas卡片:
若是你在需求分析階段注意這些DDD卡片的使用,那麼後續的DDD設計就會有更好的素材,固然還有UserStory和Use Case等。
我的建議:若是你有時間的話,強烈建議關注一下ddd-crew[11] ,有很是全面的DDD相關的最新並實用的知識和實踐。
和DDD DSL無關,只是稍微說起一下。微服務架構設計在於如何將複雜的業務系統劃分爲密切合做的微服務應用,劃分的依據就顯得很是重要。SubDomain從業務的角度出發,進行業務邊界的劃分,而BoundedContext則是關注於業務領域對應的應用承載。而Generic類型BoundedContext能夠同時支撐多個SubDomain,可以作到不一樣業務系統的應用複用。若是在Cloud Native的場景中,咱們但願更多的使用System類型的BoundedContext,也就是重複利用雲上的系統,從而減小本身的開發和維護成本。回到Appplication類型的BoundedContext,這個就是你要具體開發的應用,你選擇哪些微服務框架,這個你能夠自行決定。整個過程,DDD都起到應用劃分的理論基礎做用。
但這裏還有一個問題,就是微服務之間的通信問題,你能夠反覆強調咱們須要構建強大的分佈式應用,可是推薦的技術棧是什麼?如何去作?並且還要作的更好,這個並無明確說明,因此你們選擇REST API、gRPC、RPC,Pub/Sub等等混合通信技術棧。
關於BoundedContext之間的關聯關係DDD已經給出了(partner ship, c/s, share kernel等),可是具體到通信和協做,並無給出很好的理論基礎, 可是這個在DDD社區也有一些共識,就是基於異步化的消息通信 + 事件驅動是比較好的方案,因此你看到DDD的首席佈道師Vaughn Vernon反覆講到DDD + Reactive,就是爲了解決ContextMapping的通信問題。
說到這裏,若是你看到ContextMapper支持MDSL (Micro-)Service Contracts Generator的輸出,那麼也就不奇怪了,也是理所固然的事情。
更多的關於MicroServices和DDD關係,你能夠參考《Microservices love Domain Driven Design, why and how?》[12]
ContextMapper提出的DSL概念仍是很是好的,至少讓你們在DDD的理解上歧義少啦,同時也規範啦,DDD初學者的門檻也下降,雖不能到架構設計的地步,至少閱讀理解起來無障礙。在我編寫這篇文章的時候,ContextMapper DSL 5.15.0版本已經發布,相關的特性都已經所有開發完畢啦,使用起來仍是很是順暢的。固然落實到實際開發,DDD as Code這種方式是否有效,還但願作DDD實踐的同窗給出寶貴的意見。
固然我一篇文章並不能將ContextMapper闡述的很是清楚,contextmapper[13]上有很是詳細的文檔和對應的相關論文, 固然你能夠不採用DSL這一套思路,可是這些思想和相關的資料對DDD設計仍是幫助很是大的。
另外我的更以爲,若是你是DDD的初學者,那麼ContextMapper可能更合適,DDD是方法論,那些圖書都枯燥的要死,看兩章節不犯困幾乎很是難的。相反若是你學習DDD DSL那就簡單多,這個DSL再複雜也不會比你學習的編程語言複雜吧?相反這個DSL是很是簡單的,經過簡單的DDD DSL學習,你會很快掌握其中的概念、思路和方法,不行就看一下其餘人的代碼(DDD DSL examples),也會幫助你很快學習,掌握這些方法論,回頭你再使用圖書和文章進行鞏固一下,也是很是好的。
做者:茶什!
原文連接本文爲阿里雲原創內容,未經容許不得轉載