简介: 相较于惯例的MVC架构,DDD更形象、更难以了解,各个开发者对DDD的解释也不尽相同。那么哪种设计形式才更好?在学习时如何晓得哪种DDD更正统,没有被他人带歪?本文尝试应用“DDD as Code”的概念,即用DSL代码形式来形容DDD,对立DDD的设计思维,通过案例具体介绍如何基于ContextMapper来实现一个我的项目基于DDD DSL的表白,并分享事实中DDD的设计流程和微服务的关系。

网上有十分多对于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]和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/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),也会帮忙你很快学习,把握这些方法论,回头你再应用图书和文章进行坚固一下,也是十分好的。

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