领域驱动设计

243 阅读13分钟

什么是DDD

Domain-Driven Design

目的

解耦,实现高内聚低耦合,方便扩展。

规范

根据业务边界的上线进行架构设计。

文档与图形的载体

概念设计

  1. 概念关联图
  2. 概念类
  3. 领域类图
  4. 填充属性

逻辑设计

  1. 业务逻辑
  2. 业务模型

物理设计

  1. 所有开发人员
  2. 重新定义
  3. 架构风格

微服务与领域驱动

2004年 领域驱动定义领域模型,帮助我们确定业务边界以及技术边界

2015年 微服务

领域驱动可以作为微服务划分的标准
领域驱动可以作为微服务设计的指导思想

领域驱动用什么方式进行边界定义

围绕业务边界,从业务角度出发,建立业务的领域模型,来进行战略设计
围绕技术边界,从技术角度出发,对业务领域模型进行技术实现,来进行战术设计

战略设计

从业务角度出发,建立业务模型,划分业务边界。

领域

子域

核心子域

通用子域

支撑子域

通用语言

限界上下文

根据业务模型进行技术实现,完成软件的开发以及落地。

领域模型

领域模型是对领域内的概念类或现实世界中对象的可视化表示。

业务对象模型:业务对象之间的引用关系。

业务对象:

  1. 业务角色(表示的一个角色以及他所承担的一系列责任)

    收银员:计算商品价格、收银、找零...

  2. 业务实体(与业务角色交互,可交付的工件,资源,事件)

    商品、发票

  3. 业务用例(业务角色与业务实体之间如何执行工作流程)

    业务链路

实际上,业务对象模型就是将实体的概念以及行为的概念结合起来了。也就是说,整理起来 ,就是你的 业务逻辑流转以及中途所需要的角色。

4种模型

常用的是贫血和胀血模型

失血模型

对象中只有get、set方法,业务逻辑层包含了业务逻辑与持久层的交互

  • 优点:

    1. 领域对象结构简单。
  • 缺点:

    1. 肿胀的业务代码逻辑,难以维护;
    2. 无法应对平凡更改的需求。

贫血模型

在失血模型的基础上加上了一个持久层

  • 优点:

    1. 层次结构清楚,各层之间单向依赖;
    2. 对于只有少量业务逻辑的应用来说,使用起来非常自然;
    3. 开发迅速,易于理解。
  • 缺点:

    1. 无法良好的应对非常复杂逻辑和场景。

充血模型

  • 优点:

    1. 更加符合OO的原则;
    2. Business Logic层很薄,符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重, 只充当事务管理以及整合的角色,不和DAO打交道。
  • 缺点:

    1. 职责不好进行划分,我们的业务逻辑到底是划分到我们的哪一个层级呢?是领域对象还是业务属性呢?哪些划分到领域对象呢?而且持久化的内容都放在我们的领域对象里,所以我们在写业务的时候会深入到领域对象去,这对于开发者的水平要求很高,变相的增加了企业的成本;
    2. 由于充血模型包含了太多的操作,你实例化的时候也会有很大的麻烦,拿到了很多你不需要的关联模型。

胀血模型

胀血模型取消了Service层,只剩下domain object和DAO两层

  • 优点:

    1. 简化了代码分层结构;
    2. 也算符合面向对象设计。
  • 缺点:

    1. 取消了Business Logic层(业务逻辑层),在Domain Object的Domain Logic上面封装事务,授权等很多 本不应该属于领域对象的逻辑,使业务逻辑再次进行到混论的状态,引起了Domain Object模型的不稳定;
    2. 代码理解和维护性差。

战术设计

Model

实体

实体的核心是用唯一的标识符来定义,而不是通过属性来定义。即使两个对象的属性完全相同也可能是不同的对象。同时实体本身是有状态的,实体有演进的生命周期,实体本身会体现出相关的业务行为,业务行为会对实体属性或状态造成影响和改变。

值对象

它用于描述领域的某个方面本身没有概念标识的对象,值对象被实例化后只是提供值或叫设计元素,我们只关心这些设计元素是什么?而不关心这些设计元素是谁。这种对象无状态,本身不产生行为,不存在生命周期演进。

值对象是无状态的,并且是不可变的。可以把值对象看成是一个实体的依附属性集或者叫做多个属性聚合的结果集。值对象一般不会进行单独的持久化,或者说不会单独的去进行数据库表设计(注意是不会单独的持久化,意思就是不会单独的去设计一张表,一般值对象的值和实体放在同一张表中)。这跟实体就有很大的区别,大部分实体都会单独的进行数据库表设计或者映射。

值对象的“不可变性”、“属性判断”、“无负作用”和“替换性”:
参考文章 戏说领域驱动设计(二十)——值对象 - SKevin - 博客园 (cnblogs.com)

  1. 在设计值对象的时候不应该有“setter”方法来支持部分属性值的修改,只能通过构造函数进行全属性赋值;
  2. 判等的时候,应该是每个属性值都相等才能算两个值对象是相等的,你可以把值对象想象成由几个原始类型属性组成的,判等肯定要比较每一个属性;
  3. 值对象可以包含丰富的业务方法包括命令型的,但业务方法不应该修改值对象的某个属性值;
  4. 修改属性时只能通过整体替换。

以上特性的作用:

比如联系人信息,姓名“张三”+电话“123321”在我构建这个值对象时已经验证过是合法的。如果允许修改单个的属性,您把电话变成了“ABC”,造成了人是张三但电话是李四的,小心人家投诉你打骚扰。

值对象与实体的关联以及区别:

实体跟值对象都是在领域模型设计的时候,必不可少的基础单元。在逻辑领域模型映射到物理模型的时候,一个实体可能会对应一个或多个数据库的表结构,即持久化对象。但也有特殊情况,比如电商项目中的折扣实体,可能最终被存储为订单的价格,不会单独的去进行持久化。而值对象呢?值对象在代码中会有两个形态,第一个形态是单一属性,第二个形态是属性集。比如User实体类中会有姓名属性(单一属性String类型,基本数据类型);也会有属性集Address(引用类型)。

聚合和聚合根

概念:

将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并仅允许外部对象持有对聚合根的引用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。

聚合是由业务和逻辑紧密关联的实体和值对象组合而成,聚合是数据修改和持久化的基本单元,一个聚合对应一个数据的持久化;也就是说,一个数据对象如果需要持久化,那么它在持久化的过程中将实体与值对象总和在一起的特征,便是聚合。

聚合的边界划分设计原则:

  1. 生命周期一致性原则

    生命周期一致性是指聚合内部的对象,应该和聚合根具有相同的生命周期,聚合根消失,则聚合内 部的所有对象都应该一起消失。比如只有下单与支付的系统,支付没有单独存在的意义,那么支付比下单操作生命周期长又有什么意义呢?如果这个支付还能支持其他的模块,比如电商还有会员售卖,那么你就应该把支付进行设计,而不是属于这个聚合。

  2. 问题域一致性原则

    生命周期一致性只是指导原则之一,有时如果只考虑生命周期一致性原则可能会引起问题。考虑一个这样的场景:比如电商网站上,订单与支付存在聚合,而订单显然就是我们的聚合根,那么订单被删除,支付也要消失,那么支付一定属于订单聚合吗?实际上不属于同一个问题域的对象,不应该出现在同 一个聚合中。

  3. 场景一致性原则

    通过上面两个原则,基本能够划分清楚一个聚合的边界,但是仍然会存在一些复杂的情况。这时可以根据第三个原则来判断:场景一致性原则。场景一致性就是场景操作频率的一致性。在很多业务场景中,我们会对领域对象进行查看、修改等操作。经常被同时操作的对象,应该属于同一个聚合,而那些极少被同时关注的对象,即使上面两个原则都满足也不应该划为一个聚合。

  4. 聚合应尽可能地小

    在划分聚合时,除了应该满足上面三个指导原则外,还应该让我们的聚合尽可能地小。通常,较小 的聚合会让一个系统变得更快并且更加可靠,因为会传输较小的数据引发并发冲突的概率较小。

    而设计一个大的聚合会带来各种问题:

    • 大聚合会降低性能

      聚合中的每个成员会增加数据的量,当这些数据需要从数据库中进行加载的时候,大聚合会增加额外的查询,导致性能降低;

    • 大聚合更容易受到并发冲突的影响

      大聚合可能包含了很多职责,这意味着它要参与多个业务用例。随之而来的就是,有很大可能出现多个用户对单个聚合进行变更的情况,从而导致了严重的并发冲突,影响了程序的可用性和用户体验;

    • 大聚合扩展性差

      大聚合意味着与更多的模型产生依赖关系,这会导致重构和扩展的难度增加。

领域服务

概念

参考文章 (43条消息) DDD领域驱动设计实战(六)-领域服务_JavaEdge.的博客-CSDN博客_领域服务

领域中的服务表示一个无状态的操作,它用于实现某个特定领域的任务。 当某个操作不适合放在聚合和值对象中时,最好的方式便是使用领域服务。有时会使用聚合根上的静态方法来实现这些这些操作,但是在DDD中,这是一种坏味道。

这次的概念还是比较清晰好懂的,简单的来说,在领域层,实体干不了的(或者说不属于实体的)操作,就交给领域服务。

领域服务的一些特点

参考文章 如何运用DDD(三):领域服务 - DockOne.io

  1. 领域服务处理的是领域中的对象,比如实体、值对象等;

  2. 领域服务是负责对领域中一系列对象的编排处理;

  3. 当我们发现一个操作无法赋予一个实体或者值对象,且该操作又对业务流程很重要时,我们往往需要使用领域服务;

  4. 领域服务中的操作,从领域的角度来看,它是一个整体。

如果你在进行下面的操作时,可能证明你需要一个领域服务

  1. 通过A和B,得到一个C;

  2. A需要一个繁琐的内部策略才能得到一个结果B。

领域事件

优缺点

  1. 优点:解耦,事件驱动的优点就是解耦,如果是异步事件的话还有削峰和异步(快)的优点,但最重要的还是解耦这一点。

  2. 缺点:可读性差,代码复杂度增加,相当于是增加了一个事件中间层。

举例

先看一段伪代码1,逻辑很清晰。

下单() { 
    修改订单状态(); 
    通知商家(); 
    通知并修改库存(); 
    通知买家(); 
}

再看一段同样的伪代码2,然后对这两段伪代码进行比较。

下单{ 
    // 修改订单状态,并发布下单成功的事件 
    下单(); 
}

// 其他地方监听这些事件 
@listen 
pub1ic void 监听事件() {
}

总结:伪代码2使用监听的方式,监听下单操作,然后进行后续的通知商家等等操作,在伪代码1中,修改订单状态后有3个通知操作,在伪代码2中应该用3个相应的监听事件来完成,这样的好处是,如果下单操作以后要是又多了某些步骤,可以直接新开一个监听事件,来处理新步骤的逻辑,无需修改原来的代码。

仓储与工厂

概念

  1. 工厂:将根据入参创建实体的行为进行封装;

  2. 仓储:将根据入参把聚合根/实体从数据库里面拉出来,把聚合根/实体给存回数据库里面的行为进行封装。

仓储其实就是将三层架构的dao层多加了一层封装,这样在进行crud的时候就是通过仓储去操作数据库,而不是直接通过dao层去操作数据库,好处就是,如果ORM框架需要更换,只需修改仓储层的代码,在聚合根中通过仓储crud的代码无需变动。

仓储的规范

  1. 原则上,只有聚合根会需要一个仓储,不要给每个实体的创建一个仓储。因为聚合根是业务的边界;

  2. 原则上,仓储的方法最好只有一个byld()和save()。仓储只是封装聚合根/实体和持久层交互的逻辑。和dao不一样,有更细的查询/保存的需求,请交给dao去干。这一条和概念最后阐述的有些矛盾,待研究。

工厂没啥规范,实现功能即可。