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]。