领域建模
领域建模是针对问题空间的战术求解的过程:观察真实世界的业务需求,对业务知识进行提炼和转换,排除技术因素对建模产生的影响,一切围绕着业务需求而来。同时满足未来的需求变更与产品维护
快速建模法:名次动词法
建模过程
- 名词建模
- 识别业务流程(用例)中的名词
- 动词建模
- 识别动词,判断对应的行为是否产生了过程数据(补充手段,看是否产生了影响管理、法律或财务的过程数据,如果缺少它的记录,就会影响到商业的运营管理、造成经济损失或引起法律纠纷)
- 归纳抽象
- 确定关系
- 确定领域概念之间的关系
名词建模
只要名词属于领域概念,符合统一语言的要求,就快速将他提取出来,放到领域分析模型中
动词建模
讲识别出来的动词当作一个领域行为,然后看他是否产生了影响管理、法律或财务的过程数据。
- 驱动出隐藏的关键概念:针对动词代表的领域行为,是否需要记录过程数据
- 以验证挖掘出来的业务概念是否真的属于领域分析建模的核心概念:如果缺少了过程数据,是否影响运营管理、引起法律纠纷或造成经济损失
归纳抽象
为了提高模型的质量,可对已有领域概念进行归纳抽象,主要是针对由那些定语修饰的领域概念。如配送地址、家庭地址、已付款金额、冻结资金等。需要分辨他们是类型的差异还是值的差异,如果是值的差异,类型相同,应归为一个领域概念。
- 比如收获地址和家庭地址表达了不同的值,但实际上都是地址Address类型
- 订单状态和商品状态修饰的都是状态,但实际上代表完全不同的值(类型),两个概念不能合并
注意:在分析阶段,如果分不清楚一个模型应该保留还是删除时,应优先考虑保留,待到领域姜末设计时在进行判断
确定关系
如果某个类型拥有多种相似的关联,可以为这些关联对象定义一个新的类型。也就是说如果发现用一个领域概念来描述关系更为合理,就可以将该关系建模为一个领域概念。比如:读者和作平之间存在关联关系,表达了一种收藏的概念,故可以提炼出收藏的概念
建模书籍:《分析建模:可复用的对象模型》,《彩色UML建模》
领域模型设计要素
- 实体:谓语描述的主体
- 值对象:为主体对象的属性
- 领域事件:封装了主体的状态
- 领域服务
1、实体
巴门尼德认为实体是不同变化状态的主体:主体的状态在相当长一段时间内会持续的变化,因此需要一个身份标识来标记。一个实体应具备3个要素:
- 身份标识
- 属性
- 领域行为
身份标识
身份标识是实体对象的必要标识,在领域驱动设计中,没有身份标识的领域对象就不是实体。身份标识的主要目的是管理实体的生命周期
属性
实体的属性用来说明主体的静态特征,可分为:
- 原子属性:不可再分。【基本类型】
- 组合属性
如何定义属性是原子属性和组合属性?
- 划分标准:该属性是否存在约束规则、组合因子或属于自己的领域行为
- 约束规则:即为业务规则
- 组合因子:是否不可再分。比如重量、体积,需要与计数单位共同组合,如果只有值而无单位,就会因为单位不同导致计算错误
- 领域行为:每个抽象层只专注于做自己的事情,各司其职,这样实体类就能分配职责
领域行为
领域行为:可以更好的说明其作为主体的动态特征
- 变更状态的领域行为:应该让变更状态的方法名满足业务含义
- 自给自足的领域行为:意味着实体对象只操作了自己的属性
- 互为协作的领域行为:通过方法参数传入
实体拥有的变更状态的领域行为,修改的只是对象的内存状态,与持久化无关
2、值对象
值对象是不可变的,不需要分配标识。实体与值对象的本质区别在于是否拥有唯一的身份标识
值对象和实体的区别
- 业务对它相等的判断是:依据值还是依据身份标识
- 确定属性值是否会发生变化:如果变化了,是产生一个完全不同的对象,还是维持相同的身份标识。前者是值对象,后者是实体
- 生命周期的管理:值对象没有生命周期的管理
不变性
领域驱动设计建议尽量将值对象设计为不变类,因为一个不变的类是线程安全的。如果既要保证对象的不变性,又要满足更新状态的需求,就需要用一个保存了新状态的实例来替换原有的不可变对象
值对象的优势
- 内建类型无法展示领域概念,比如String 和 Name
- 内建类型无法封装领域逻辑
- 内建类型缺乏验证能力
3、聚合
类的关系
- 泛化: 子类继承父类
- 关联:代表整体的对象包含了代表部分的对象,即为组合关系
- 合成:体现了强烈的所有权的特征,即组合关系的两个对象属于同一个生命周期。比如学校和教室
- 聚合:没有所有权特征,不会约束它们的生命周期。比如教室和学生
- 依赖:一个类使用了另一个类的信息或服务
引入边界:聚合
当规模越来越大时,类之间的关系变得错综复杂,对象的层次变得越来越深,类之间的关系难以梳理和控制。因此需要引入边界来降低和限制领域类之间的关系,每个边界都有一个主对象作为外交发言人,总体负责与外部的协作。
- 就好像公司员工多了之后,会分部门,部门下会分团队,便于管理
引入这种关系后,就可以只保留主对象之间的关系。这种层次的边界称为聚合,边界内的主对象称为聚合根。聚合在限界上下文与类的粒度之间形成了中间粒度的封装层次
聚合的特征
- 聚合是包含了实体和值对象的一个边界
- 聚合内包含的实体和值对象形成一棵树,只有实体才能作为这颗树的根:因为聚合需要通过资源库管理生命周期,要管理生命周期,就需要通过身份标识对其进行跟踪
- 外部对象只允许持有聚合根的引用
- 聚合内部需要保证事务的一致性
- 由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间的协作
聚合的设计原则
**当聚合边界存在模糊时,独立性对聚合边界的影响要高于完整性。**完整性将聚合视为一个高内聚的整体,独立性影响了聚合的粒度,不变量时对动态关系的业务约束,一致性体现了聚合数据操作的不可分割
- 完整性
- 对内、对外有一致的生命周期
- 独立性
- 需要看待合并实体是否会被调用者单独使用,比如汽车和发动机。汽车没有发动机不完整,但是发动机可以单独使用
- 当聚合边界存在模糊时,独立性对聚合边界的影响要高于完整性
- 不变量
- 聚合内部的恒定关系,可以理解为固定规则
- 一致性:事务的一致性
- 原子性:聚合不可再分的领域概念
- 一致性:聚合边界内最重要的不变量就是一致性约束
- 隔离性:通过唯一的身份标识进行聚合关联
- 持久性:一个聚合只有一个资源库,由资源库保证聚合整体的持久化
最高原则
只有聚合根才是访问聚合边界的唯一入口,聚合外部的对象不能引用除根实体之外的任何内部对象。聚合之间通过身份标识进行引用
4、聚合生命周期的管理
生命周期经历的各种状态取决于存储介质,内存与硬盘,分别对应对象的实例化与数据的持久化。如果不是因为计算机无法做到永不宕机且内存资源便宜,那么是可以不进行持久到外部存储设备中。
在领域模型的设计要素中,由聚合根实体的构造函数或者工厂负责聚合的创建,若要修改聚合的状态,需要在内存中先进行状态的变更,然后通过持久化确保聚合对象与数据记录的一致
工厂
**工厂封装了聚合对象的创建逻辑。**许多面向对象与药支持类通过构造函数创建自己,对象自己创建自己,就好像扯着自己的头发离开地球表面,不合情理。
- 由被依赖聚合担任工厂
- 引入专门的聚合工厂
- 聚合自身担任工厂
- 消息契约模型或装配器担任工厂
- 使用构建者组装聚合
资源库
资源库是对数据访问的一种业务抽象,分离了聚合的领域行为和持久化行为,保证了领域模型对象的业务纯粹性。
一个聚合一个资源库,如果要访问聚合内的非根实体,需要通过聚合访问。在资源库获得整个聚合后,将根实体作为入口,在内存中访问封装在聚合边界内的非根实体对象
- 资源库和数据访问对象(DAO)的区别?
- 数据访问对象在访问数据时,无聚合的概念,可以针对领域层的任何模型对象
- 如何设计资源库接口?
- 一派认为以通用性换取接口的可扩展,但却牺牲了接口方法的可读性
- 一派以封装获得接口的可读性,却因为方法过于具体导致接口膨胀与不稳定
可以一边为资源库定义常见的个性化查询方法,一边保留对查询条件的支持。因为一旦资源库提供了通用的查询接口,就会将组装查询条件的代码混入应用层,违背了保持应用层轻薄的原则
5、领域服务
聚合的已知数据不一定满足完整的领域需求,为了保证聚合的自治性,需要将不足的部分作为方法的参数传入。但两个聚合之间的协作应该由谁负责发起呢?——由领域服务负责
运用场景
- 当针对领域行为建模时,优先考虑使用值对象和实体来封装领域行为。如果领域行为的变化方向没有拥有数据的类保持一致,就应分离变与不变,将这一变化的领域行为从所属的聚合中剥离出来,形成领域服务
- 当有两个聚合需要进行协作时,该由领域服务负责
- 当业务需要调用其他服务时,也应该由领域服务负责,不应该在聚合内部引入对南向网关端口的依赖
归纳为:
- 与状态无关的领域行为
- 变化方向与聚合不一致的领域行为
- 聚合之间协作的领域行为
- 聚合和端口之间协作的领域行为
6、领域事件
特征
- 领域事件代表了领域概念
- 领域事件是已经发生的事实
- 领域事件是不可变的领域对象
- 领域事件会基于某个条件而触发
定义
- 事件ID:通过ID唯一标识事件,进行管理
- 事件发生时间
领域设计建模
实体、值对象与领域事件共同构成了描述真实世界业务问题的基本要素;聚合从设计角度为实体与值对象圈定了概念边界,并映入了工厂和资源库的设计模式,用于管理聚合的生命周期;领域服务作为聚合的补充,专注于领域行为的表达,负责协调聚合之间以及聚合与端口之间的协作
- 远程服务:若为当前限界上下文的远程服务,负责响应角色的服务请求
- 应用服务:提供具有服务价值的服务接口,完成消息契约对象与领域模型对象的转换,调用货编排领域服务
- 领域服务:提供聚合无法完成的业务功能,协调多个聚合以及聚合与端口之间的协作。封装领域逻辑,以避免其泄漏到应用层
- 聚合:作为信息的持有者,履行自给自足的领域行为
- 工厂:封装复杂货可能变化的创建聚合的逻辑
- 端口:作为访问外部资源的抽象
- 适配器:端口的实现,提供访问外部资源的具体技术实现,并通过依赖注入设置到领域服务或应用服务中
设计聚合
- 理顺对象图:辨别实体还是值对象
- 分解关系薄弱处:以关系强弱为界,以聚合边界为刀,逐一分解
- 调整聚合边界
分解关系
泛化关系的处理
- 整体视角:调用者不关心特化的子类之间的差异
- 独立视角:调用者只关注具体的特化子类,此时应以特化的子类作为独立的聚合根
如果一个继承体系的子类存在不同于父类和其他子类的特定属性,说明该子类具有了领域概念的独立性。
如果是合成关系,也属于一个聚合
服务驱动设计
- 分解任务:根据职责的层次对业务服务进行任务分解,直到分解为原子任务
- 同一层次的任务必须位于同一个抽象层次
- 分配职责:为角色构造分配不同层次的职责
分解任务
- 把基本流程以动词短语形式列出,作为基础任务
- 以归纳法将具有相同目标的基础任务由上而下归纳为组合任务
- 比如验证订单有效性和验证库存有效性具有共同的目标,就是验证订单可以归纳为一个组合任务
- 再以分解法判断基础任务是否是原子任务,如果不是,就自上而下进行拆分
分配职责
- 远程服务:匹配业务服务
- 应用服务:匹配业务服务。自身并不包含任务领域逻辑,仅负责协调领域模型对象,通过它们的领域能力组合完成一个完整的应用目标,完成对领域服务和聚合的协调
- 领域服务:匹配组合任务,领域服务的主要目的就是控制多个聚合与端口之间的协作
- 聚合:匹配原子任务
- 端口:匹配原子任务,抽象对外部资源的访问