写点什么

【实践篇】DDD 脚手架及编码规范 | 京东云技术团队

  • 2023-08-24
    北京
  • 本文字数:3235 字

    阅读完需:约 11 分钟

【实践篇】DDD脚手架及编码规范 | 京东云技术团队

一、背景介绍

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


(1)可以对新建的项目进行统一的规范;


(2)对于指导老项目进行 DDD 的改造提供指导。


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

二、脚手架的理论基础

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


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


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


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


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


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


三、脚手架介绍及使用

使用命令如下:


mvn archetype:generate     -DarchetypeGroupId=com.jd.jr.cf     -DarchetypeArtifactId=ddd-archetype     -DarchetypeCatalog=local     -DarchetypeVersion=0.0.1-SNAPSHOT     -DinteractiveMode=false     -DgroupId=com.jd.demo.test        //从这一行开始需要根据项目名称修改 -DartifactId=demo-test     -Dversion=1.0.0     -Dpackage=com.jd.demo.test     -DappName=demo-test  -s D:/git/settings.xml      // 本地 git配置文件
复制代码


生成完的项目结构如下:


|--- adapter                     -- 适配器层 应用与外部应用交互适配|      |--- controller           -- 控制器层,API中的接口的实现|      |       |--- assembler    -- 装配器,DTO和领域模型的转换|      |       |--- impl         -- 协议层中接口的实现|      |--- repository           -- 仓储层|      |       |--- assembler    -- 装配器,PO和领域模型的转换|      |       |--- impl         -- 领域层中仓储接口的实现|      |--- rpc                  -- RPC层,Domain层中port中依赖的外部的接口实现,调用远程RPC接口|      |--- task                 -- 任务,主要是调度任务的适配器|--- api                         -- 应用协议层 应用对外暴露的api接口|--- boot                        -- 启动层 应用框架、驱动等|      |--- aop                  -- 切面|      |--- config               -- 配置|      |--- Application          -- 启动类|--- app                         -- 应用层|      |--- cases                -- 应用服务|--- domain                      -- 领域层|      |--- model                -- 领域对象|      |       |--- aggregate    -- 聚合|      |       |--- entities     -- 实休|      |       |--- vo           -- 值对象|      |--- service              -- 域服务|      |--- factory              -- 工厂,针对一些复杂的Object可以通过工厂来构建|      |--- port                 -- 端口,即接口|      |--- event                -- 领域事件|      |--- exception            -- 异常封装|      |--- ability              -- 领域能力|      |--- extension            -- 扩展点|      |       |--- impl        -- 扩展点实现|--- query                       -- 查询层,封装读服务|      |--- model                -- 查询模型|      |--- service              -- 查询服务
复制代码

整体的分层架构图如下:

四、脚手架编码规范

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


作者:京东科技 史纪军

来源:京东云开发者社区 转载请注明来源

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
【实践篇】DDD脚手架及编码规范 | 京东云技术团队_DDD_京东科技开发者_InfoQ写作社区