DDD系列:四、领域层设计规范
在一个DDD架构设计中,领域层的设计合理性,会直接影响整个架构的代码结构以及应用层、基础设施层的设计。但不同业务的领域层设计是需要不断思考和演进的,既要避免未来的扩展性差,又要确保不会过度设计导致复杂性。
传统OOP的缺陷:
- extends导致的强依赖父类对象行为,子类若重写父类方法时,违反OCP、里氏替换原则
- open for extension可以通过extend语法实现,但close for modified比较难实现,但可以考虑通过组合来实现扩展性(即将新增一个类,将两者的类组合起来)。
- 各个对象行为划分问题,某个行为可能影响多个对象,那么其业务逻辑实现应该在哪里?
- 多个对象有相同行为时,会有重复的代码,在两个对象中存在,如何维护
设计规范
Entity 实体类设计规范
Entity中,包含了一个领域的状态和对这个领域的操作。Entity的设计,应当保证实体的不变性,这里所说的不变性指的是Entity在初始化或接受任何操作后,内部的属性没有一致性或冲突问题。
1. 创建即一致
保证业务流程中,拿到的Entity属性是完整且有效的。为了达到这个效果,建议Entity的创建使用下面两种方式:
使用constructor创建Entity,对所有属性进行初始化
该方案中,可能导致业务使用时,对Entity construct中,传入过多的参数,如:
Weapon weapon=new Weapon(name,null,weaponTypeEnum,weaponHealth,buffer)
使用Factory模式来创建Entity,降低创建的复杂度
将初始化动作用Factory来完成,业务只需要传入特有的变化数据。如:
Weapon weapon=WeaponFactory.createWeaponFromEmptyId(name,WeaponPrototype weaponPrototype);
2. 使用Entity的行为对方法进行命名,替代public setter
传统的public setter,只对Entity中某个属性值进行变更,非常容易打破Entity中的一致性。如订单Entity中,如果只是用setter修改订单状态,那么内部关联的子订单就有可能出现不一致的情况。
public class Weapon{
int damageType;
/**
在DDD模式下,setter更名为changeDamageTo更合理,且方法中可以对相关联的属性进行设值
**/
public void setDamageType(int type){
this.damageType=damageType;
}
}
3. 不强依赖外部的领域服务或聚合根
原因:Entity依赖(指的是Entity属性中的依赖)外部的 DomainService 或聚合根时,当前Entity 无法保证依赖对象变更后,自己内部的数据是否还具有正确性和一致性。
正确的对外部依赖的方法有如下两个:
- Entity中依赖外部DomainServie的id,这个id建议是强类型的id,非Long型的id。因为强类型的id可以包含校验动作,可以在entity构建时,和其他基础类型区分开。
- 通过入参的方式,传入外部DI的Service服务,如equip(Weapon, EquipmentService)
Domain Service 领域服务设计规范
需要使用domain service的原则:
- 操作多个entity,确保多个实体的变更具有一致性
- 要使用托管的第三方服务来操作Entity
- 通用组件,多个Entity都具有的某个行为
思考
如何判断 Entity 设计是否合理?
- 是否符合业务概念:Entity应该反映出业务中的实体概念、重要信息,并且能够正确地表达实体之间的关系和属性。
- 是否具有可测试性:Entity 应该具有可测试性,能够通过自动化测试来验证其正确性。
- 内部一致性:创建的 Entity 应当保证内部数据的合理、有效,Entity 对外提供的操作,也应当保证该原则。
随笔
ECS(Entity-Component-system)架构的优点:
-
组件化:将数据的变化和行为,拆分为不同的组件,实现组件化,降低系统复杂度
-
行为抽离:即将通用的行为,做到公共的模块中
-
数据驱动:对象的行为由数据驱动,通过数据驱动+组件化,让对象的行为可根据数据实时变化
DDD下各个层级应当承载的内容
Interface层
Interface层通常返回统一的Result模型或其子模型,包装所有从Application、domian、infrastructure层抛出的异常。
接口层组成的功能:
完成鉴权、session校验、接口统一日志、限流、统一接口返回内容、前置缓存、异常统一管理等内容。
接口定义的数量和业务隔离问题:
在接口层面,当把多个类似的业务,做到同一个接口中时,容易出现参数膨胀,所以在设计接口时,考虑Single Responsibility Principle,若不同的接口有相同的一些操作,需要下沉到Application Service层中去。
接口层直接对接业务,可以快速的变化,但我们希望ApplicationService层的调整比较小,Domain层几乎没有变化。
Application层
核心内容:
- ApplicationService:业务服务编排层,理论上讲,不承接业务,但对领域模型的构建要求较高,所以,可以运行ApplicationService承接部分非核心的、非通用的业务逻辑
- 服务编排和业务逻辑的区分,需要看具体的代码逻辑来界定,没有一个非常明确的定义。
- DTO Assemble:将内部领域模型向DTO转换
- 入参由Command、Query、Event组成
- 出参为DTO
- 不负责处理异常,由上层统一完成处理
Command、Query、Event的应用
Command | Query | Event | 单一的入参,如Id | |
---|---|---|---|---|
语义 | 待执行的一个命令 | 查询动作 | 已经发生的一个事件 | 对这个id做某个动作 |
返回内容 | DTO、Boolean | DTO | void | void |
为什么需要CQE对象
- 由于业务的发展,方法的入参会越来越多,导致阅读困难、在使用时容易把同类型的入参位置填错
- CQE中,可以对对象中的字段进行校验,避免简单的校验逻辑,出现在application Service中。
- 如果在使用已存在的CQE时,发现CQE对象中的校验在新的业务场景不合适时,那么说明这个是两个业务,需要再建一个不同的CQE对象,其命名与老的区分开。
Infrastructure层
利用Anti-Corruption Layer,将外部的透出的模型,转为应用内部的数据模型,屏蔽外部接口变化对应用内业务代码的影响。