全文约3600字,预计阅读时间10分钟

导读

上篇DDD系列文章第7篇:可落地的DDD分层架构介绍了分层架构的框架结构和编码规范,本篇通过一个例子进一步强化从需求到代码的实战体验。

01

示例需求

以电商订单业务为示例,假设有下列具体功能需求:

  1. 可以通过API调用创建订单、或者通过订阅消息创建订单。

  2. 订单行不能超过100行。

  3. 订单行小计金额和总金额保留2位小数点。

  4. 订单行小计金额超过200元,给订单打9.5折。

  5. 订单运费为10元,如果订单总金额超过68元则免运费。

  6. 订单创建后数据要保存在数据库中。

  7. 订单创建后需要触发库存系统扣减库存。

  8. 订单创建后需要给用户发送短信告知结果。

  9. 订单创建后需要触发支付流程。

  10. 支付成功后修改订单状态。

  11. 如果历史累计订单金额超过8888元,升级该用户为VIP用户。

02

需求分析

上面是一个常见的业务功能需求描述,如果是系统第一次建设订单能力,还会涉及到一些细粒度的逻辑规则比如商品上下架状态判断、订单编号生成规则、期望送货时间和下单时间的关系,支付方式选择等等。也可能会涉及一些粗粒度的模块间需求,比如订单模块和商品模块、账户模块、支付模块等其他模块的职责边界和协作关系。

这里假设系统不是第一次引入订单模块,已经完成了前几篇讲的DDD战略设计的领域识别和BC识别(限界上下文)阶段。本次需求分析主要工作是拆解好业务流程会涉及到哪几个模块以及哪些功能点。用时序图来表达完整业务流程是一个不错的方法,大面上理清楚主要交互步骤。

图片

03

领域建模

接下来聚焦订单限界上下文(订单BC),对业务逻辑进行领域模型。业务逻辑本质就是由行为(数据获取、条件判断、系统调用等)和数据(现成的入参数据、从外部获取的数据、加工形成的数据等)组成,因此建模也可以分为两大步骤:给业务行为找归属和给业务数据找归属。这两个步骤没有绝对的先后顺序,可能涉及互相交叉,并行设计。

3.1 业务行为建模

对业务行为的建模步骤:

图片

  • 第1步:识别接口。对应用户接口层的入口端口设计,需求中#1的API或消息订阅,#10的支付消息订阅都是创建或修改订单的形式,分别对应三个入口接口。

  • 第2步:识别应用服务。一个用户用例就是应用服务,用例是个组合行为,本身没有具体的原子业务逻辑,而是通过编排聚合、资源库、适配器和领域服务来组装出有意义的业务能力。订单的创建是一个用例,需求#10对应的订单状态修改也是一个用例。

  • 第3步:识别聚合行为。所有的业务行为都应该优先考虑内聚到聚合身上。从是否访问聚合外的资源来判断能否归属到聚合。比如需求#2、#3、#4、#5的判断逻辑依赖的数据均来自订单和订单行自身,因此属于订单聚合的行为。这也是富血对象设计的原则,对象里既有数据也有相应的操作数据的行为。

  • 第4步:识别资源库行为。如果需要访问聚合外的资源,再问是否需要访问存储数据(包括但不限于数据库,可能还有缓存、文件等存储介质)。如果答案为是,则应该属于资源库行为,比如需求#6。

  • 第5步:识别适配行为。如果仍然不需要访问存储数据,那么一定需要访问外部系统,这涉及到需要整合BC之外的业务能力来完成本BC的业务。这里的每个行为都应该识别为一个适配接口,比如需求#7、#8、#9的领域事件发送,商品状态校验、调用库存系统扣减库存。

  • 第6步:识别领域服务。领域服务和应用服务一样也是组合行为,需要靠编排上面的聚合行为、资源库行为和适配器行为三种原子行为。设想一下,如果没有领域服务,应用服务也可以编排它们。但为了让业务逻辑更内聚,往往会根据某些业务行为的亲和性进一步提炼成领域服务,达到内聚和复用的用途。比如订单创建时调用分布式ID生成服务来生成订单号(依赖订单特性数据和分布式Id生成服务)可以设计为领域服务,视作订单BC的一种业务逻辑。

  • 第7步:基础设施层实现。最后在基础设施层分别完成以上三种原子行为的具体实现。这里也是技术框架集中使用的地方。

3.2 业务数据建模

业务数据主要由聚合、实体、值对象和领域事件来承载。

  • 第1步:识别实体。先假设业务对象都是实体,然后再看某些重要属性字段是否有强烈的理由设计为值对象。上述需求里有订单和订单行,一个订单包含多行订单行,每行代表具体的商品及其数量和价格。

  • 订单有订单号这个唯一标识,在全生命周期各个阶段内都有明确的状态和业务操作,因此订单显然是个实体(OrderEntity)。

  • 订单行也设计为实体比较直观(OrderLine)。虽然严格说订单行号这个标识一般不具有业务含义,设计为值对象也不是不可以。

  • 商品该是个实体吗?如果订单BC里没有单独冗余保存商品信息的话,商品在订单BC里就是订单行上的一个字段productId,通过productId可以访问到商品BC里的商品聚合信息。其他业务对象类似道理,订单BC往往只需要知道其Id就足够了。

  • 第二步:识别值对象。值对象是实体的多个字段的集合,因为业务亲和性而组合成值对象。比如共同描述一个特征,或者共同完成一些验证或者计算逻辑。比如收发货地址往往由多个字段描述并且具有独立的验证地址格式的内在逻辑。

  • 第三步:识别聚合。尽可能设计小聚合,默认先假设每个实体都是聚合,再综合考虑事物完整性和独立性做一定程度的聚合。订单行实体没有独立维护的必要性,从完整性考虑应该跟订单实体同时保存和修改,因此和订单实体一起组成订单聚合。聚合是个虚拟概念,聚合的根实体就是订单实体。

  • 第四步:识别领域事件。这个步骤相对容易,如果之前有经历过事件风暴的话这步可以忽略。示例需求中订单已创建OrderCreatedEvent是个领域事件。

  • 值得一提的是消息内容需要包含全部必要信息还是只包含ID等少量信息让消息订阅者再次回查事件内容?我建议优先考虑事件本身携带全部必要的信息,因为BC间耦合更松散。当然并不意味着事件就是整个聚合,仍然需要对聚合内容做一定裁剪。

由于这个需求示例比较简单,领域建模的过程看起来可能比较容易,但一旦业务变复杂后建模就会比较有挑战。注意不要被传统的优先考虑数据库表的建模思维干扰,领域模型和数据库表没有直接对应关系。一个聚合可以对应多张表,一个值对象也可以单独保存为一张表。

04

示例代码

用Java语言为例。

4.1 项目结构

一个父项目,四层分别是四个子项目,项目间的依赖严格遵守分层架构的规范。

图片

4.2 各层主要代码

图片

4.3 编码注意事项

  1. 接口层:接口代码跟传统的Java Web项目里定义controller接口一样,非常薄的一层,主要调用应用服务xxAppService的用例方法。

  2. 应用服务:涉及数据库事务的应用服务记得开启和提交事务。

  3. 领域层:领域模型除了类名有特定的后缀命名外,最好还应该有一些代码级别的约束。比如:

  4. 聚合这个虚拟概念如何在代码里体现?可以让根实体实现一个特定接口,或者打上特定注解。

  5. 每个实体类的唯一标识ID,其实是个值对象,有特定的生成逻辑、格式校验逻辑、字段类型转换逻辑等等,如何规范统一?可以定义必要的接口让每个实体类的ID都要实现它。

  6. 实体类往往需要保存到数据库,有一些表字段是通用的比如创建日期、创建人等等,如何规范统一?可以用注解或者基类统一定义。

  7. 资源库:资源库的实现就是传统ORM框架做的事,最好对所有聚合对应的资源库的CRUD标准方法做一个默认实现。

  8. 数据对象互转:DTO与Entity、PO与Entity,这些转换逻辑大部分是样板代码,可以考虑用更高效的方式。比如mapstruct就是一个不错的框架,会让代码简洁度提高很多。

类似上面说的事项还有很多,总之为了让某些规范更有控制力,让一些样板代码得到最大程度的复用,可以酌情定义相应的二方库供每层按需引用。如果是Java项目甚至可以考虑引入ArchUnit这类针对项目依赖,包引用等项目架构基本的规范检查工具,结合CI流水线达到在线检查的强约束。

05

结语

不知道你有没有体会到业务语义化、结构化地组织代码带来的愉悦,其实团队规模越大愉悦度越大。有些复杂业务团队甚至把并行开发引入到BC内部,部分人编写领域层逻辑代码、部分人编写基础设施层的适配器代码。这都得仰仗规范的层间依赖和积木式的领域模型代码。本篇和上篇讲的分层架构都是讲BC(限界上下文)内部的代码框架,但就像上面需求分析章节里画的图一样,实际项目里的一个需求往往会涉及多个BC,那么BC之外还有哪些需要关注的架构吗?必须有。比如BC之间通过领域事件进行协作有哪些技术细节呢?与事件驱动架构有什么关系?面向多端展示的业务逻辑到底算不算领域逻辑呢?后续将继续介绍DDD有关的架构知识。

感谢阅读,往期文章节选:

DDD系列文章第7篇:可落地的DDD分层架构

DDD系列文章第6篇:领域建模那些事

DDD系列文章第5篇:承上启下的限界上下文很重要

DDD系列文章第4篇:DDD如何做战略设计,做个懂业务的技术

DDD系列文章第3篇:DDD实施地图

DDD系列文章第2篇:建立DDD的知识体系

DDD系列文章第1篇:初识DDD