DDD as Code:如何用代码诠释领域驱动设计?

简介: 相较于常规的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

DDD DSL

用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有专门的介绍,以下:安全

  • 战术设计(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的表达。app

在介绍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/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);
      }
    }

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/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中进行介绍。

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),也会帮助你很快学习,掌握这些方法论,回头你再使用图书和文章进行巩固一下,也是很是好的。

做者:茶什!
原文连接本文为阿里云原创内容,未经容许不得转载

相关文章
相关标签/搜索