1.各个层命名约定

image.png

适配器层

web服务、定时任务、消费者分别为一个model :shebao-dispatch-ws、shebao-dispatch-task、shebao-dispatch-consumer , 拥有自己的启动类

文件命名

web服务:xxxController
定时任务:xxxJobHandler
消费者:xxxConsumer
**

入参与出参

定时任务和消费者的入参与返回都按照公司框架内要求即可
web服务:
入参:xxxDTO、xxxCmd
返回:xxxDTO、xxxVO

应用层(app和client)

文件命名

client-model中
api包 对外暴露的接口 xxxAppService
dto包 定义的外部参数,xxxAppServiceImpl汇总命令, 命令动作是 xxxCmd, 查询动作时 xxxQry
data包 普通数据传输对象 xxxDTO
event包 领域事件数据传输对象 xxxEvent

app-model中
consumer包 外部消息 xxxAppConsumer
scheduler包 定时任务 xxxAppobHandler
executor包 处理request xxxCmdExe
query 处理查询请求 xxxQryExe

入参与出参

xxxAppService 和 xxxCmdExe
入参: xxxCmd 【client中定义】
出参数: xxxDTO 【client中定义】

xxxQryExe
入参: xxxQry 【client中定义】
出参数: xxxDTO 【client中定义】

领域层

文件命名

domain-model中
domainservice包 放领域服务定义 xxxService
domainservice.impl 放领域服务实现 xxxServiceImpl
gateway包 定义网关接口 xxxGateway
model包 实体、值对象、聚合相关 【贴紧业务 好理解的命名】
aggregates包 聚合 (实体)
valueobjects 值对象

入参与出参

xxxService领域服务
入参:聚合、实体、值对象【domain.model包中定义】
返回:聚合、实体、值对象【domain.model包中定义】

xxxGateway 网关接口
入参:聚合、实体、值对象【domain.model包中定义】
返回:聚合、实体、值对象 【domain.model包中定义】

聚合、实体、值对象分门别类放到对应包即可,不带后缀,直接起一个贴切的名字

分别放到对应的包即可: aggregates、entity、valueobjects

基础设施层

文件命名

infrastructure-model中
gatewayimpl包: 网关的实现 xxxGatewayImpl
database包 数据库相关 xxxmapper.java 和 xxxmapper.xml
do包 用到的数据库实体对象 xxxDO , 与表结构一一对应
rpc包 远程调用相关 , xxxRPC
dto和vo包 用到的dto与vo
config包: 配置相关的
utils包: 工具类相关

入参与出参

xxxGatewayImpl
入参:聚合、实体、值对象【domain.model包中定义】
返回:聚合、实体、值对象 【domain.model包中定义】

mapper
入参:xxxDO
出参:xxxDO

xxxRPC
入参:xxxDTO
出参:xxxVO

2.通用职责约定

2.1 校验

应用层必须做一次基本校验

  • 必传参数
  • 参数的基本属性校验,如:邮箱账号

领域层中:

  • 实体内自己做校验,自己实体内数据正确【通过get、set,在领域对象内包含自检逻辑确保入参合法】
  • 领域服务的能力去做业务规则上的校验 【跨多个实体的情况】

2.2 异常处理

一般原则

基础设施层和领域层 只抛异常,不处理异常
所有异常都在应用层统一处理

提供两种异常类型:BizException、SysException

异常名称 用法 备注
BizException 业务异常,我们不需要知道也不需要处理 Biz为“商户的缩写” 与 business 含义基本一致
SysException 系统异常,我们需要知道需要处理

异常的响应码统一放到ReturnCode枚举中, 且按照分类分别放到 :ReturnCode10Enum、ReturnCode11Enum等

/*
返回码分类枚举都在此报下定义
异常类型: 错误码是int类型以1开头
10.其他
11.参数校验问题
12.业务逻辑问题
13.二、三方系统异常
14.数据问题
15.底层组件依赖异常
/

代码见下图
image.png

2.3 表结构设计

要注意区分领域里的实体与数据库实体DO
应该转变思想为 领域驱动 ,而不是表驱动
领域实体与数据库实体通过gateway来解耦

2.4 公共代码: 枚举、常量类、bean工厂、工具类等存放约定

枚举、bean工厂、常量类放到 领域层中

  • 根据所属不同的聚合进行分包 , 如: businesspeople下有自己的 枚举、bean工厂、常量 , contract下也有自己的 枚举、bean工厂、常量
  • 如果有多个领域用到的 如 是否删除的枚举 ,此种可以单独在domain建立enums统一放置

工具类放到 基础实施层

  • 属于工具类的枚举、常量等与工具放同一个地方

业务的归领域层、 基础工具的归基础设施层
如果所有层都要用,则放到 xxx-components 中

2.5 全局能力:切面日志、异常处理存放约定

处理哪层的就放到哪层
比如:
接口切面日志就放到用户接口层
应用层切面日志放到用户接口层

全局的异常处理同样也是

2.6 如果需要事务,则将事务控制加在应用层

对于数据库事务: 建议是手动加事务注解 ,避免全局的事务切面(防止事务过大)
如果涉及到多个外部系统,采取最终一致性来保障数据一致性

3.原则与注意点【重点】

3.1 应用层的 xxxAppServiceImpl 是汇总用例的文件,里面的一个方法只能调用一个 CmdExe

这样做将不同用例的调用代码按照命令合理拆分到各个CmdExe中,可以避免service的膨胀,避免出现上帝类
正例:
反例:

3.2【推荐】应用层的 xxxCmdExe 是一个命令所有相关代码写的地方,它负责调用网关gateway获取数据,调用domain或实体的能力按照业务规则处理数据, 调用网关gateway持久化数据 【若是有特别复杂的业务逻辑,可以沉淀到domainService,然后domainService顺便调用gateway进行持久化】

应用层的职责是负责 组合网关与领域的业务规则 ,是一个流程具体的控制者 , 数据操作找网关,业务规则找领域服务
这样分离了业务规则和流程控制(查和插入通过网关)【分离关注点
正例:

反例:

3.3~~ ~~领域层的domainService与实体里只能有业务规则的代码,不允许使用gateway操作数据, 操作数据应当由应用层做 【删除本条】【允许】

领域层是居于最核心的层,他不依赖任何层,只有最纯粹的业务规则,这些规则被其他层所依赖
正例:

反例:

3.4 实体、值对象、聚合他们自己内部应该通过get与set自校验,多个实体的业务规则(这个业务规则的行为放到哪个实体都不自然)放到domainService

一般来说, 实体、值对象、聚合都有自己的属性和行为(规则), 但是某个规则涉及多个实体放哪个都不合适,就可以把它们统一放到domainService

注意:如果业务规则较为简单,仅仅实体的规则就够用了, domainService可以不写 ,

正例:

反例:

3.5: 先把App做厚,再把App做薄

我们先可以把业务逻辑都写到App里面,在写的过程中,我们会发现有一些业务逻辑,不仅仅是过程式的代码,它也是领域知识(Domain knowledge),应该被更加清晰、更加内聚的表达出来,那么我们就可以把这段代码沉淀为领域能力。

重构的过程里我们可能会发现,很大一部分逻辑都在应用层的各个CmdExe里, domain层的逻辑少的可怜,没关系,先这样写,没问题,别怀疑,如果你不了解该放到哪里,就按照以前的思路,把CmdExe当成以前的service即可 ,大家在CR 的时候一块看
:哪些是领域的规则,哪些是底层的技术实现细节,分门别类的移动过去 , 渐渐的你就会发现代码越来越清晰了
例子:

3.6: CmdExe 只可以引用 domain 的能力(gateway 、实体、领域服务) , 不可以直接用mapper或RPC

原因1:
COLA作者张建飞建议将Domain设计为开放的,可以应用层可以直接用mapper、rpc,但这样会导致退化, 我们在实践的时候要避免

我更愿意把Domain层设计成开放的,这种开放性不仅体现在CQRS的时候,App可以绕过Domain层直达Infrastructure;也体现在当你的团队实在hold不住DDD的时候,可以选择退化到老的三层架构。
虽然可以退化,但不应该成为你轻易放弃Domain层的理由。据我观察,很多同学不喜欢DDD,其根本原因还不在于对象之间的转换成本(实际上,这个转换成本也没那么大),而在于他不清楚Domain的职责,不知道哪些东西应该放到Domain里面。一种典型的错误做法是把所有的业务逻辑都放到了Domain层,包括我们上面说的CRUD统统放到了领域层,这样的DDD当然没人喜欢

原因2:直接调用mapper、rpc等,势必要求应用层了解具体的细节逻辑,当依赖变更会影响到应用层 , 而通过gateway调用则相当于建立了防腐层,只需要改动gateway即可 【防腐层】

原因3: 层次严格的调用,依赖比较好管理

3.7:假如一个同一个实体被多个限界上下文用的划分方式:分别在两个限界上下文中各定义一次

原因:虽然是同一个东西,但在不同限界上下文下可能含义也是稍稍有所不同,为了避免 限界上下文1下 的实体里有本上下文中用不到的, 我们彻底给分开 【业务含义清晰】

3.8 注意转换(converter)与 构建(build)之间的区别: 转换是值拷贝与字段映射, 构建是带有业务逻辑的设置值

3.9 区分领域对象与数据库实体字段的区别:哪些字段是数据库实体的,但是领域对象上不应该有的

我们的表结构设计规范要求必须有: create_time , create_id, update_time,update_id,is_delete 字段

但是到了领域对象中,我们还需要这些吗?

4.推荐的最佳实践

4.1 重视统一语言,建立项目的核心词汇对照表

中文是我们日常交流和文档中经常要体现的,所以需要统一,这样我们在交流的时候才能高效,没有歧义;英文和英文缩写主要体现在我们的设计和代码上,也就是说我们的“统一语言”不仅仅是停留在交流和文档中,还要和代码保持一致,这样才能做到知行合一,提升代码的可读性和系统的可理解性。
image.png
注意的是,词汇表中英文对中文的翻译不一定非常“准确”,不过没关系,语言就是一个符号,共识即正确,只要大家容易理解达成一致即可

4.2 推荐的编码良好习惯

  • 【日志规范】能用debug就不要用info,能用warn就不要用error。滥用的error与狼来了无疑;
    - 【方法参数要少】参数越少,越容易理解,也便于测试,各个参数的组合就如笛卡尔积;
    - 【空行规范】方法、逻辑分段,要加空行,提高代码可读性。车轮毂与车轴之间有空隙,车才能跑;书法绘画有留白;
    - 【防止破窗】首先我们要有一套规范,并尽量遵守规范,不要做“打破第一扇窗”的人;其次,发现“破窗”要及时修复,不要让问题进一步恶化;
    - 【三次原则】第一次用到某功能时,写一个特定的解决方法;第二次又用到时,复制上一次的代码;第三次出现时,就要着手写通用解决方案了;
    - 【最小惊奇原则】写代码不是写侦探小说,要的是简单易懂,而不是时不时搞点烧脑的骚操作;
    - 【请求读写分离】增删改,会改变对象的状态,只需返回成功失败即可;查询是不会改变对象状态的,对系统没副作用。

4.3 优雅的异常使用写法【推荐 ,非强制】

4.3.1 使用异常工厂方法获取异常

提供了异常工厂 ExceptionFactory ,里面封装了异常对象
用法如下:

// 不使用异常工厂 if(xxx){ throw new BixException(code, msg); } //使用异常工厂 if(xxx){ throw ExceptionFactory.BixException(code, msg); }

源代码参见 cola 源码中的组件模块

4.3.2 使用断言来进行优雅校验

提供了断言类 Assert
用法如下:

//定义listA

//不使用断言
if(CollectionUtils.isEmpty(listA)){
throw ExceptionFactory.BixException(code, msg);
}

//使用断言
Assert.notEmpty(listA,code,msg);



//断言内部对if进行了一次封装
public static void notEmpty(Collection<?> collection, Integer errorCode, String errMessage) {
if (collection == null || collection.isEmpty()) {
throw new BizException(errorCode, errMessage);
}
}

源代码参见 cola 源码中的组件模块

4.3.3 使用 CatchAndLog 注解打印方法和类的执行信息(包括异常)

使用切面包了所有加 CatchAndLog 注解的类和方法, 将它们执行前入参执行后返回值都打印出来

源代码参见 cola 源码中的组件模块

4.4 删除“规范: 消费者和定时任务不能直接连库,必须要通过接口操作数据”

我们之前有一条规范: 消费者和定时任务不能直接连库,必须要通过接口操作数据

目的是: 收住逻辑的入口避免一个逻辑到处改, 迁移的时候也方便

但是改为COLA架构后, 我们的核心业务规则都在领域层,且定义了抽象的网关接口 , 由基础设施层来实现,应用层编排时依赖的都是:纯业务规则或抽象网关定义

此时:业务逻辑集中,底层设施变动时只需要改网关的实现,不会对上层的代码产生影响

“消费者和定时任务不能直接连库,必须要通过接口操作数据” 带来的好处基本就意义不大了, 反而因为只能走接口还得写额外的接口调用麻烦了很多

4.5 领域层的实体和领域服务是核心的业务逻辑聚集地,不应该依赖外部资源 (不通过gateway调用外部资源)【推荐,非强制】

领域层只有核心的业务逻辑 ,尽量不依赖第三方资源 , 这时的单元测试就很好写
所以我们可以要求核心业务逻辑必须编写单元测试
实行起来复杂度就大大降低

image.png

参考:

跨越DDD从理论到工程落地的鸿沟:https://blog.csdn.net/significantfrank/article/details/123267395

cola架构的发展过程各个链接整理【推荐阅读:可以看到COLA架构的演变历史,理解为什么要这么分层这么约定
复杂度应对之道 - COLA应用架构 :https://blog.csdn.net/significantfrank/article/details/85785565
应用架构COLA 2.0:https://blog.csdn.net/significantfrank/article/details/100074716
应用架构COLA3.0 :让事情回归简单:https://blog.csdn.net/significantfrank/article/details/106976804
应用架构COLA 3.1:分类思维: https://blog.csdn.net/significantfrank/article/details/109529311
COLA 4.0:应用架构的最佳实践:https://blog.csdn.net/significantfrank/article/details/110934799

张建飞著. 代码精进之路:从码农到工匠[M].北京:人民邮电出版社,2020.1 【本书第三部分-12章作者详细讲了COLA架构的实践,值的一读