领域驱动开发相关规范
1. 包命名规范
- controller 包:用户界面层
- service 包:承接来自用户界面层的请求,属于应用服务层;服务类以
AppService
为后缀 - task 包:定时任务相关的包,属于应用服务层,即定时任务的业务逻辑依旧交割各领域完成。
- security 包:安全相关的包,属于应用服务层。
- aop 包:完成某个应用逻辑的 AOP,属于应用服务层。
- dto 包:按领域放,如果是涉及多个领域,则可直接放在根目录
- <领域名>
- domain 包:领域层
- <领域名>:
- service:领域服务类,大部分以
Service
为后缀,少部分可以更贴切其业务逻辑/职责来命名,如manager
、scheduler
、balancer
、validator
- impl:如果 sevice 只有一两个就不要分 impl 包了
- entity:实体包
- vo 包:valueObject 类
- common 包:领域的常量类、工具类;如果常量类、工具类仅在领域内使用,或者只受这个领域管理,则可以放在领域在。
- dto 包:用于 domain 层可接收的数据传输类,也可直接使用 controller 传输进来的 dto 包,这两种 dto 包均不能有冗余字段;
- repository 包:数据库接口类;
- service:领域服务类,大部分以
- <领域名>:
下面是基础设施层的各种包:
- dao 包:
- <领域名>
- mapper 类
- 其他的资源类
- <领域名>
- rpc 包:
- util 包:工具类
领域层不能直接使用 mapper
类的原因:
- mapper 类本质上是 ORM 框架 MyBatis 的技术实现,这些类需要依赖于 MyBatis 相关的类或注解;简而言之,直接使用无法与 MyBatis 解耦;而且有部分需求是使用
- 封装实体的更新动作与实际持久化操作的不一致:如实体删除动作实际上是逻辑更新、聚合根的持久化操作实际上可能是多个实体的持久化,而 Mapper 是半自动化 ORM,无法做到。
2. 接口方法规范:
- 除基础设施层外,其他层的包名、类名、方法名的命名都应具备领域知识含义(业务逻辑的概念);
- Service 层的接口命名规范:
- 如果 Service 层的方法不包含特别逻辑,仅仅是使用 Repository 进行操作,则应与 Repository 接口保持一致;否则,Service 层的方法应包含领域知识。
- 以
OrThrows
为后缀来指明方法会以抛出Exception
的方式来处理异常,如findByNameOrThrows(name)
就是为空时会抛出异常 - 检查方法以
check
开头,意味着会布尔类型的检查结果(true、false); - 校验方法以
validate
开头,意味着;
- Repository 的接口规范:
- 查找接口:
findBy...
、findBy...OR..OR..
、findBy...AND..AND..
- 插入、更新类接口:
save
、saveBatch
; - 删除类接口:
delete
、deleteBatch
;
- 查找接口:
- Service 层的接口命名规范:
- 基础设施层的命名应根据其技术来命名,以暴露技术实现细节:
- Mapper 层的接口命名规范:
- 查询类接口:
select...By...
、query...By...
、find...By...
selectPage
;需要标明查询字段 - 更新类接口:
update
、updatexx
、update...By..
、updateBatch
;默认是根据 ID 更新的,如果不是应标明字段名 - 插入类接口:
insert
、insertBatch
、save
、saveBatch
; - 删除类接口:
delete
、deleteBatch
- 查询类接口:
- Mapper 层的接口命名规范:
总的来说,本规范是希望通过统一的命名规范,让接口方法命名就能轻松表达出方法含义,避免维护大量的方法注释、代码注释,而且越是下层的方法命名、类命名更应该仔细认真,因为这块越下层的类、方法的粒度就越细,复用率很高,命名不好,很影响协作。
注意,上面只是一般性的接口命名规范,如果有些方法原本逻辑复杂,如查询接口的查询字段和查询条件复杂,无法通过方法名去完全表示,则应准备好充分的注释,包括输入字段
3. 应用服务层、领域服务层、实体/值对象的职责划分
3.1 应用服务层 VS 领域服务层的职责划分
- 每一层的 Service 类严禁不能相互调用,只能垂直向下调用(具体看文档)。
- 应用服务层应尽量简单,尽量薄,它最大的作用就是跨领域、跨 Service 调用,除此之外,就是与业务无关的横切逻辑。所以在绝大部分情况下,应用服务类会很简单。
>对外,应用服务为外部调用者提供了一个简单统一的接口,该接口为一个完整的用例场景提供了自给自足的功能,使得调用者无需求助于别的接口就能满足业务需求。对内,应用服务自身并不包含任何领域逻辑,仅负责协调领域模型对象,通过它们的领域能力来组合完成一个完整的应用目标。应用服务作为应用外观,仅仅是领域层的一个入口点,通过它可以降低客户程序与领域层实现之间的依赖。作为领域模型对象的包装,它自身不应该包含任何领域逻辑。由此可得到应用服务设计的第一条准则:不包含领域逻辑的业务服务应被定义为应用服务。
>“应用服务是协调者,它们只是负责提问,而不负责回答,回答是领域层的工作。”**注意,对所谓“提问”和“回答”的理解,要站在一个完整用例场景的高度来阐释。当客户端发来请求要执行一个完整的用例场景时,作为协调者的应用服务只负责安排任务,至于任务该怎么做,就是领域模型对象要完成的工作。这实际上是业务价值(Why)与业务功能(What)之间的关系。对于一个用例场景,需要为参与者提供业务价值,该价值由应用服务提供;要实现这一业务价值,需要若干业务功能按照某种顺序进行组合,组合的顺序就是编制,编制的业务功能就是回答问题的领域模型对象。
- 领域服务的作用同样也是如此,它的作用是协调各个领域对象;不过,实际上不少动作是不属于对象的,所以一般来说,领域服务无法做得很薄。
- 不要在领域层放置与业务逻辑无关的横切关注点:如 监控指标收集、错误处理、审计日志埋点收集、事务、认证与授权、定时任务等等,这些与业务逻辑无关的横切点应在应用层完成
3.2. 领域服务与实体/值对象的职责划分
实体是领域内的最小操作单元,而领域服务则是协调各个实体来完成应用服务层的请求。领域服务其实是面向过程编程的类,那么哪些操作应该是放在实体,哪些操作应该放在领域服务内,才不会让领域服务过度膨胀。
- 领域服务只允许放不适合由实体承担的一些动作,或者跨实体调用的逻辑(注意,实体不能调用实体,但聚合根实体可调用其内部领域对象的行为操作)。简单来说,可以优先将操作放在实体内考虑,在确认实体不适合承担该职责时才考虑放在领域服务内。
- 从粒度来看,实体是一个不可分的细粒度操作行为,复用度应该是很高的(供领域服务类复用);而领域服务则是提供一个中粒度的操作,相对定制化,可复用度相低(只在应用服务层复用);而应用服务则是基本不可复用,是直接针对应用功能定制化。
- 比如,返回结果封装成定制化的 dto 对象,很明显就是领域服务或应用服务的操作。
3.3 聚合根、实体、值对象、基本类型
- 实体有两个特征:1)唯一标识(可能是全局的、也可能是局部的)、2)有生命周期管理;
- 值对象与实体的区别仅在于是否有唯一标识:值对象是没有唯一标识;值对象有可能是有生命周期,也可能是没有生命周期(如仅作为类型);值对象也是可能是
- 聚合根只能是有全局唯一标识的实体
- 聚合内部有两种领域对象,一种是普通实体对象,一种是值对象;
- 实体对象和值对象也可能会有值对象
[图片]
3.4 小结
不管是应用服务层,还是领域服务层,都是面向过程编程的,都不应该过于复杂。下面一些经验结论可以帮助大家判断 Service 类的设计是否有问题:
- 根据代码行数判断:如果一个 service 类超过 100 行代码,可能是有问题的,如果是超过 200 行,那么很大概率是有问题的。
- Service 类不应包含属性,如果包含属性,那很可能需要使用面向对象建模去抽象一个对象出来。
- 实体/值对象不允许直接调用 Repository 接口:对象不应该包含持久化相关逻辑,这块应该交给领域服务完成。
应用服务层 和 领域服务层的优化的手段:
- 尝试把逻辑下放到下一层或使用;
- 以面向对象去抽象出对象,封装好对象的属性和行为。
4. 实体类规范 & 单元测试
- 实体需要生成
equals
、hashCode
方法:以实体的唯一标识去生成; - 实体类需要进行单元测试:要覆盖测试实体的行为;
- 领域服务类的单元测试应模拟实体类:实体是领域服务的下一层业务逻辑,不要耦合在一起测试,不然很难维护;
- 单元测试要善用一些工具类去快速生成输入参数:如
com.google.common.collect
下的集合工具类
5. 其他规范
- 不要污染实体类,让实体类有临时传输用的字段
- 使用 lombok 框架避免维护 get/set 方法:目前是面向对象编程,对象是有行为的,get/set 方法的维护会消耗我们的精力