首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

【实践篇】DDD脚手架及编码规范

一、背景介绍

我们团队一直在持续推进业务系统的体系化治理工作,在这个过程中我们沉淀了自己的DDD脚手架项目。脚手架项目是体系化治理过程中比较重要的一环,它的作用有两点:

可以对新建的项目进行统一的规范

对于指导老项目进行DDD的改造提供指导

本文主要是梳理和总结了DDD脚手架使用中的编码规范以及遇到的问题。

二、脚手架的理论基础

DDD相关的应用架构有很多种,比如四层架构,洋葱架构,六边形架构,整洁架构等。这些应用架构都有各自的特点和不同。但是他们的总体思想都是相似的,主要是通过分层来实现功能和关注点的隔离。达到的目标是领域层不依赖任何其他外部实现,这样就能保证核心业务逻辑的干净和稳定。

左图是整洁架构的示意图,左图为分层,右图表示各个分层的变化频率和抽象层级。整洁架构主要分为4层:

Frameworks&Drivers层:这一层表示系统依赖的外部系统,比如数据库、缓存、前端页面等。这一层是变化频率最高的,也是需要和我们的核心业务逻辑做隔离的。

Interface Adapters层:这一层是一个适配层,主要负责外部系统和内部业务系统的适配,这一层的主要作用就是外部系统和内部系统的适配和协议转换。

Application Business Rules: 应用业务规则层,可以理解为用例层,这一层表示整个应用可以提供哪些用例级别的功能和服务。这一层也是对第4层中的核心业务规则的编排层。

Enterprise Business Rules: 这一层就是最为核心的业务逻辑层,这一层不包含任何和技术相关的内容,只包含业务逻辑。

三、脚手架介绍及应用

使用命令如下:

生成完的项目结构如下:

整体的分层架构图如下:

四、脚手架编码规范

1、Api模块编码规范:

Api模块是专门用于定义对外接口的模块,所以这个模块中只包含接口定义,出入参定义,尽量不依赖其他包。

Api中的接口定义类以xxxxResource(或者xxxxService)结尾。这条规范完全是为了和老的应用保持一致。

Api接口的入参尽量不要使用Java中的原子类型(Primitive Type), 需要将入参定义为单独的类。最好是继承现有的BaseRequest类。

Api接口的出参统一使用泛型类对真实的返回类型进行包装。

出入参类都以DTO结尾。

出入参中尽量不适用枚举值类型的成员变量。

2、Adapter/Controller模块编码规范:

这一层中需要将出入参的DTO和业务层的VO/DO对象进行转换。

这一层不要包含任何的业务逻辑,只包含参数转换和业务无关的校验逻辑。

接口返回值缓存类的逻辑,可以放在这个模块中实现,因为这个动作不包含业务逻辑。

3、App模块编码规范:

这个模块中的类统一以Case结尾。

这一层主要是对底层业务逻辑进行编排。可以直接调用Domain层的port定义。跨域的服务调用也可以放在这个模块中。

这一层可以直接调用Domain模块中定义的Repository服务。

事务处理:如果是跨多个聚合的业务逻辑需要放在一个事务中,需要在这一层开启和提交事务。

4、Domain层编码规范:

DomainService命名统一以Service为后缀。

Entity实体类的命名不用后缀。值对象类的定义统一以VO结尾。

DomainService逻辑中可以调用Repository和Port中定义的接口。

DomainService可以操作多个聚合,实体和值对象。

Entity实体类可以有构造函数,builder,getters。不要直接放开所有属性的setters,防止业务代码随意修改实体的属性。

编写业务逻辑需要遵守原则:优先将业务逻辑放在Entity和VO中,然后才是放在聚合中,最后才放在DomainService中。

依赖反转原则:Domain层依赖的外部接口都要定义在Domain模块的port包中。Domain层只面向接口编程,不依赖接口实现类。

5、Adapter/Repository和Rpc模块编码规范:

Repository实现类中需要将接口入参中的DO对象转换为PO对象后再调用数据库存储。

Repository和聚合的关系是一对一的关系。一个Repository有唯一的对应的聚合。

如果Repository中需要开始事务可以在Repository实现类中开启事务。

Rpc层最好是对外部接口的出参和入参定义一个防腐层对象,命名统一以DTO结尾。

五、常见问题及解决办法

Q1、Api模块对外提供的jar包中是否要引用其他应用的jar包?

A1:有一些场景,A应用的Api接口的入参需要引用其他应用的包中的类。比如A应用发出了一个事件,B应用提供了一个接口来处理这个事件,那B应用是否要引用A应用的包中的事件定义类呢?理想情况,最好是B应用定义一个自己的类,这样B应用就不会依赖A应用的包。

Q2、Api包中是否能包含枚举类的定义?

A2:最好不要在Api包中对外暴露内部的枚举值定义。因为枚举值是需要在Domain模块中定义和使用的,不适合通过jar包的形式暴露给外部。如果确实有需求要暴露给外部应用(比如为了让接口调用方方便的知道入参中的值有哪些),可以将枚举类的定义放在同一的common包中。这样Domain模块和对外提供的jar包都可以引用common包。

Q3、数据存储是否要使用统一版本号?

A3:对于新应用,最好是使用统一的版本号,这样在更新数据库的时候就可以统一使用版本号当做乐观锁。但是对于遗留系统而言,启用版本号的成本比较高,因为需要梳理所有对实体进行变更的点,要求所有的点都统一使用版本号。所以要根据情况来确定是否使用。

Q4、对于一些偏流程性的业务,频繁的调用外部rpc接口。如果每个rpc接口都添加一个防腐层对象的话,会降低开发效率。是否可以不定义防腐层对象?

A4:最好是定义防腐层对象,短期可能降低一些开发效率,但是从长期和代码标准话的角度看,还是值得的。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OEZxEbev3rsAboUiKOZdxO9g0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券