DDD as Code:如何用代碼詮釋領域驅動設計?

網上有非常多關於DDD的文章,這當然是好事情,大家都想掌握好的設計方法來解決軟件開發中的問題。但是這其中也存在一些問題,如果你隨便打開網上的幾篇DDD文章,雖然每一位作者都說自己是按照DDD思路進行架構設計的,但是細心的同學會發現每一個作者DDD文章中的結構描述、畫的架構圖都千差萬別,你會非常奇怪,這些都是DDD設計嗎?這裏其實有一個問題,就是通過文字和圖示描述一些抽象的概念時,本來就會有很大的差別。大家不要用盲人摸象的概念進行類比,這個不太合適,即便兩個同學,對DDD都非常瞭解,而且都實踐了好幾年多個項目,他們寫出來的東西還是不一樣。我Java入行稍微早點,當然你說我年紀大保守也可以,記得當初沒有那麼多中間件,就是基於Struts 1.x這個MVC框架進行開發,不同的同學寫出的設計文檔也是千差萬別。這麼簡單的MVC架構都能有不同的架構設計文檔,而DDD相對更抽象、更難以理解,所以架構設計文檔長的不太一樣,這個也是可以理解的。

那麼我們是不是要接受這個事實,“各個作者對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這個概念,讓我們的設計更統一些,更能方便表達設計思想,更容易被人理解。

DDD DSL

用DSL也就是用代碼方式來表達DDD,這個很早就有了,但是更偏向DDD的戰術設計(Tactic Design)和代碼層面,如 Sculptor[1]和http://fuin.org DDD DSL[2] ,大家普遍都認爲,就是基於Xtext的DDD代碼生成器。要費勁學習那麼多,只爲生成一些代碼,而且只是Java代碼,所以普遍關注度並沒有多高。

我們能否將DDD DSL除了代碼生成這一部分,更偏向於戰略設計(Strategic Design),突出設計的思想,那麼DDD as Code就全面多了。接下來我們就介紹ContextMapper這個框架。

名詞解釋一下:有不少同學對於DDD的戰略設計(Strategic Design) 和戰術(Tactic Design)之間區分有些疑惑,DDD有專門的介紹,如下:

  • 戰術設計(Tactic DDD):Entity, Value Object; Aggregate, Root Entity, Service, Domain Event; Factory, Repository。
  • 戰略設計(Strategic DDD):Bounded Context, Context Map; Published Language, Shared Kernel, Open Host Service, Customer-Supplier, Conformist, Anti Corruption Layer (context relationship types)。

其實也比較簡單,戰略設計更大一些,偏宏觀,你可以理解爲公司高層在討論的業務和技術方向,各個團隊或者產品的分工和配合;而戰術設計則相對小很多,主要集中在一個BoundedContext內部,比如如何設計DDD那些Entity,Service,Repository等,外加可能的應用開發的技術選型,可以說更關注技術層面。

ContextMapper框架介紹

ContextMapper是一個開源項目[3] ,主要是爲DDD設計提供DSL支持,如DDD的戰略(Strategic)設計,Context間映射(Context Mapping),BoundedContext建模以及服務解耦(Service Decomposition),那麼我們就看一下如何基於ContextMapper來完成一個項目基於DDD DSL的表達。

在介紹ContextMapper時,我們先交代一下項目背景。如花是一名架構師,對DDD也非常熟悉,而且有過幾個項目的DDD實踐,最近他加入會員線,負責完成對會員系統的改造,更好地配合公司的微服務化的設計思路。會員線之前就是三個應用:會員中心對外提供的大量的REST API服務;會員註冊和登錄應用;會員中心,處理會員登錄後如修改個人密碼、基本信息、SNS第三方綁定和支付方式綁定等。

如花加入會員團隊後,和大家溝通了基於DDD + MicroServices的架構思想,大家都表示同意,但是如何落實到具體的架構設計和文檔上,大家就犯難啦。讓我們看一下最典型的DDD設計圖:

其中的概念,如SubDomain、BoundedContext、Entity、ValueObject、Service、 Repository、Domain Event,以及Context映射關係(Context Mapping),這些都沒有問題,但是如何向他人表達這個思想?總不能每次都把DDD設計圖和分層圖都貼上去,然後說我就是按照DDD設計的。

從SubDomain開始

如花開始DDD的第一步,也就是Subdomain的劃分。當然DDD中包括三種類型的SubDomain,分別爲通用(Generic)、支撐(Supporting)和核心(Core)三種類型,這裏稍微說明一下這幾者的區別:

  • 通用(Generic) Domain: 通用Domain通常被認爲已經被行業解決的問題,如架構設計中的可觀測性的Logging、Metrics和Tracing,各種雲服務(Cloud Service)等,這些都已經有比較好的實現方案,對接就可以。當然業務上也有,如成熟的行業解決方案,如ERP、CRM、成熟硬件系統等,你購買就可以啦。
  • 支撐(Supporting) Domain:和通用Domain類似,但是系統更來自內部或者還需要在通用的基礎上進行一些定製開發。如一個電商系統,會員、商品、訂單、物流等業務系統,當然還有一些內部開發的技術類型支撐系統。
  • 核心(Core) Domain: 也就是我們常說的業務核心,當然如果是技術產品,就是技術核心,這個也就是你最要關注的。

這三者整體關係如下:Core是最與衆不同且花費精力比較多的,在複雜性Y維度,我們要避免高複雜度的通用和支撐Domain,這樣會分散你的注意力,同時還要投入非常大的精力,如果確實需要,購買服務的方式可能最佳。

圖源:https://github.com/ddd-crew/ddd-starter-modelling-process

如花首先將會員先劃分爲幾個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);
      }
    }

Context Map

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/context-mapping

如果你自行畫圖來表達這些關係,一定有非常多的工作量,細緻到箭頭類型,備註等,不然會引發誤解。這裏我們就直接上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中進行介紹。

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的名稱,這個不用說啦,這個和ContextMap中名稱一致。
  • implements AccountDomain:表示要實現哪一個SubDomain,我們都知道一個Subdomain可能會包含多個BoundedContext,這些BoundedContext配合起來完成Subdomain的業務需求。ContextMap還提供refines,來表示BoundedContext要實現一些user case,官方文檔有對應的說明。
  • BoundedContext的屬性字段:type表示類型,如APPLICATION、SYSTEM等。domainVisionStatement描述一下BoundedContext的職責。implementationTechnology表示具體的技術,前面我們說到BoundedContext已經涉及具體的應用和系統等,所以要說明對應的技術方案實現,核心的部分描述一下就可以。responsibilities 表示BoundedContext的職責列表,這裏只需要關鍵字就可以,如Account要負責安全驗證等。
  • AccountFacadeAggregate: 表示提供給外部調用的聚合,這裏DTO的對象定義、服務接口的定義等。
  • Aggregate Accounts:這個表示BoundedContext內部的聚合,如entity、value object、service等。這裏說明一下,DDD中的那個Aggregate是entity,value object的聚合對象,而ContextMapper中的Aggregate表示爲一些資源的集合,如Service集合等。

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-4-java: Base classes for DDD with Java[6]
  • jDDD:Libraries to help developers express DDD building blocks in Java code[7]
  • ddd-base: DDD base package for java[8]

這三者其實出發點都是一致的,就是在代碼層面來描述DDD,核心是一些annotation、interface,base class,當然也包括推薦的package結構。

ContextMapper的其他特性

講到這裏,其實DDD整體上來說,我們已經闡述清楚:Domain劃分、整體Domain的BoundedContext拓撲圖和關聯關係、BoundedContext具體定義和架構設計文檔規範。但是ContextMapper還提供了UserStory和UseCase對應的DSL,讓我們來看一下。

UserStory

好多同學都問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."
}

UseCase

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
}

ContextMapper帶來的收益

按照你的說法,我們用DSL代碼方式來描述DDD,這個有什麼收益?

架構設計標準化

這種代碼方式,一目瞭然且非常規範。如果你代碼寫錯會有什麼問題,當然是編譯不通過,IDE都會幫你糾正。所以DDD DSL也是這樣,完全無歧義。目前ContextMapper DSL包括Eclipse和VS Code插件,在IntelliJ IDEA可以通過自定義File Types和Live template方式來輔助你編寫cml文件。

生成器(Generators)支持

前面我們聊到DDD DSL支持代碼生成器,可以輔助你生成代碼,相信這個大家都能明白,因爲DDD DSL代碼是標準的,基於這個Code Model生成其他形式的代碼,這個當然可以。

另外ContextMapper還支持其他模型生成,如ContextMap圖形化展現、PlantUML的結構圖,對應的代碼在這裏[9]。我這裏給大家一些截圖:

當然ContextMapper還提供通用的生成器,也就是基於DDD DSL模型,加上Freemarker模板,然後就可以生成你想要的各種輸出,如生成JHipster Domain Language (JDL)用於快速創建文件腳手架也不奇怪。相信很多Java程序員對此都不陌生,我們開發Web應用時就是使用Freemarker生成HTML的。更多細節訪問這裏[10]。

現實中的DDD設計流程

我們有了DDD DSL來描述我們的架構設計,是不是就全面了,完全夠用,開發不愁了呢。還不是,我們知道在軟件架構設計和編寫代碼前,都有需求調研、客戶走訪、領域專家溝通、需求分析、研討等等,這個在現實生活中還是少不掉的,其目的就是爲了後續的架構設計提供素材並做鋪墊。那麼如何將DDD和這些前期操作整合起來?其實DDD有涉及這方面的內容,如EventStorming卡片:

Bounded Context Canvas卡片:

如果你在需求分析階段注意這些DDD卡片的使用,那麼後續的DDD設計就會有更好的素材,當然還有UserStory和Use Case等。

個人建議:如果你有時間的話,強烈建議關注一下ddd-crew[11] ,有非常全面的DDD相關的最新並實用的知識和實踐。

DDD和MicroServices的關係

和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),也會幫助你很快學習,掌握這些方法論,回頭你再使用圖書和文章進行鞏固一下,也是非常好的。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章