DDD(领域驱动设计)

1,906 阅读18分钟

1、什么是DDD?

wiki百科:

DDD(Domain-Driven Design)是一种软件设计方法,专注于根据领域专家的输入对软件进行建模以匹配领域。

领域驱动设计主要基于以下目标:

  • 将项目的主要重点放在核心领域和领域逻辑上;
  • 基于领域模型的复杂设计;
  • 发起技术专家和领域专家之间的创造性合作,以迭代改进解决特定领域问题的概念模型;

个人理解:

DDD(Domain-Driven Design)是一种解决复杂软件问题的方法论,通过划分好业务边界,设计业务模型,将复杂软件问题不断的拆解为具有高度业务内聚的一个个的小问题,然后针对每一个小问题聚焦于其业务逻辑的实现,保证代码模型和业务模型的一致性,从而实现整个复杂问题的高内聚、低耦合的可演进式架构。

目的:

  • 使业务边界清晰;
  • 减少重构风险;
  • 系统高内聚、低耦合;

2、DDD(领域驱动设计)

image-20220926213134640

DDD从具体实现操作上来说分为两部分的架构设计:

  • 战略设计

    所谓战略设计,是一种高阶的设计方法,直接面向业务,其主要目标是清晰区分系统中不同的业务关注点。在实施过程中,战略设计需要考虑各个业务场景下不同业务操作所在的业务边界,从而实现对领域的合理划分。从架构设计上讲,战略设计偏向于业务架构的规划和梳理,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,使用限界上下文作为微服务设计的参考边界。

  • 战术设计

    相较战略设计,战术设计偏向于底层技术实现。针对战略设计中已经形成的业务领域和边界,战术设计的主要目的是采用合理的、高效的技术手段来实现各个业务操作。显然,战术设计关注技术架构的设计和实现,包括聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计与实现。

1、DDD核心概念

image-20220926221320405

1、领域

领域就是一个业务问题域,可大可小,比如商城领域。

一个大的业务问题可以拆分为一个个小的业务问题,那么每个小的业务问题就可以称为子域,比如商品子域、订单子域等等。

根据重要性的不同以及业务场景的不同会对领域内的子域有着不同的标识(优先级):

  • 核心域:该业务领域的核心子域,它是业务成功的主要要素,是该领域内的核心竞争力。
  • 支撑域:解决该业务领域问题的必要支撑点,具有很强的业务相关性,不具备通用性。
  • 通用域:没有太多个性化的需求,同时能够被多个不同领域使用的通用功能子域。

标识的目的:通过领域划分,区域不同子域的不同功能属性和重要性,从而可以对不同子域才去不同的资源投入和建设策略,其关注度也会不一样。

image-20220926224757639

2、限界上下文

限界上下文的解释是:业务边界或者微服务的真实物理边界中的通用语言(实体、值对象、领域事件等),也就说同一个通用语言在不同的业务边界中可能会具有不同的含义。

每个限界上下文一般都可以作为真实的物理边界划分为一个微服务,但是可能在实现过程中会考虑粒度问题,把多个限界上下文放在一个微服务中,此时的限界上下文可以理解为逻辑边界(聚合),比如把商品和订单两个限界上下文同时放在同一个商城微服务内。

image-20220926224833470

3、实体和值对象

实体和值对象是组成领域模型的基本单元。

在不同的业务场景或领域中,实体和值对象的角色是可以互换的。

实体:具有唯一标识符,在复杂的业务逻辑或流程中其标识符不会发生变化,它的重点在于延续性和标识,而不是属性。

  • 充血模型:将大多数业务逻辑放在领域实体中实现,实体本身包含了属性和它的业务行为,它在领域模型中就是一个具有业务行为和逻辑的基本单元。
  • 实体往往采用充血模型进行实现,每个实体就是一个实体类,包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。
  • 实体以DO的形式存在,是领域模型对象,不是数据模型对象,一个实体可能对应0或多个数据库持久化对象。例如订单实体就是由订单持久化对象和商品持久化对象构成。

值对象:实体中多个具有相同业务概念的属性的集合(其实本质上单一属性就是一个值对象),被实体所引用,只能整体初始化和替换,不支持局部变更,基本没有什么业务操作。

  • 贫血模型:只有全参构造方法以及get方法,没有任何业务逻辑。

  • 起到一个描述的作用,把多个碎片化的有相关业务概念的属性进行整合为一个Class,不具备唯一标识符,然后由实体引用,依赖于实体而存在。

  • 值对象嵌入到实体的方式一般有2种:

    • 属性嵌入,一般和实体是1:1关系,映射到数据模型中:

      image-20220926232026962

    • 序列化大对象,一般和实体是N:1关系,映射到数据模型中:

      image-20220927083749677

DDD引入值对象本质上是希望由以前的数据建模优先转变为领域建模优先,在领域建模时,将部分对象设计为值对象,即保留了业务对象的含义,同时又减少了实体的数量。在数据建模时,我们可以直接将值对象嵌入到实体中,减少数据表的数量,简化数据库设计,极端点说:只把数据库当做一个存储仓库,无需符合数据库设计的三大范式或其它规则。

代码设计:

@Data
public class Order {
    private Long id;
    private String orderId;
    private Address address;
    private List<Item> itemList;
}

@Getter
public class Address {
   private String province;
   private String city;
   private String country;
   private String street;
   public Address(String province,String city,String country,String street) {
       this.province = province;
       this.city = city;
       this.country = country;
       this.street = street;
   }
}


@Getter
public class Item {
  private String skuId;
  private String name;
  public Item(String skuId, String name) {
      this.skuId = skuId;
      this.name = name;
  }
}

什么时候应该设计为实体?什么时候应该设计为值对象?

我的理解:在当前领域内有业务行为的就设计为实体,没有业务行为并且只做状态描述的就可以设计为值对象。

4、聚合和聚合根

聚合:由业务逻辑紧密关联的实体和值对象组合而成,是数据修改和持久化的基本单元,每个聚合对应一个仓储,实现数据的持久化,同一个聚合内的数据需要保持强一致性,外部调用只能通过聚合的仓储进行访问,不能直接访问聚合内的领域对象。

聚合根:聚合的领导者,也就是对外提供的唯一的访问入口,要想访问聚合内的实体,必须先访问聚合根,由聚合根导航到内部实体。使用聚合根保证聚合内数据模型的数据一致性。 例如订单实体就是订单聚合的聚合根。

  • 聚合根本身就是一个实体,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 在聚合之间,是聚合对外的接口人,以聚合根ID关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。

image-20220927222508140

5、领域事件

领域事件:用来表示领域中发生的事件,一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关系后续订阅方事件处理是否成功,这样可以实现领域模型之间的解耦,维护领域模型的独立性和数据一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不要求强一致性,而是基于事件的最终一致性。

领域事件存在的两种形式:

  • 微服务内的领域事件

    所谓微服务内的领域事件,就是在同一个限界上下文内存在多个聚合,聚合之间存在发布-订阅的关系,发布方将领域事件发布到事件总线(EventBus)中,订阅方接受事件处理后续业务操作。

    由于本质上是在同一个微服务当中,因此不建议通过事件总线的方式去实现,增加了系统复杂度;如果确实存在跨聚合的业务处理,可以在应用层中以服务调用的方式完成跨聚合的访问。

  • 微服务之间的领域事件

    跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,其主要目的是实现微服务解耦,减轻微服务之间的实时服务访问的压力。

    跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件、甚至分布式事务等问题。

image-20220731110319839

识别领域事件的方式:

  • 如果发生……,则……
  • 当做完……的时候,请通知……
  • 发生……时,则……

2、DDD常见架构模式

image-20220927231535958

1、用户接口层

用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等。例如:Controller、Consumer,Subscriber、Job等调用入口都应该放在该层。

2、应用层

应用层是很薄的一层,理论上不应该有业务规则和逻辑,只负责服务组合编排相关的工作,用来暴露系统的全部功能,满足user case,具体的实现也就是应用服务。此外,应用层也是微服务之间交互的地方,它可以调用其他微服务的应用服务,完成微服务之间的服务组合和编排。例如:领域服务调用、RPC调用、分布式事务、非业务性的校验等都应该放在该层。

3、领域层

领域层的作用是实现核心业务逻辑,主要体现领域模型的业务能力,用来表达业务概念、业务状态和业务规则。 业务逻辑主要是由实体和领域服务来实现,实体采用充血模型提供该实体内的业务功能,领域服务实现跨实体之间的复杂业务逻辑。

领域服务:当一个业务逻辑需要跨多个领域模型对象(同一个聚合内)时,负责串联领域模型对象、资源库等一系列领域内的对象的行为,为应用服务提供一个原子性的业务逻辑处理。

其实很多时候在一个聚合内,领域服务的逻辑是可以直接写在聚合根里的,因为聚合根已经包含了该聚合内的所有实体,所以理想的情况是不应该存在领域服务的。

4、基础设施层

基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、MQ、gateway、缓存、数据库等。

在基础设施层中有一个重要的服务:仓储(Repository),一个聚合对应一个仓储

在MVC中使用的DAO本质上就是数据模型,内部没有任何业务逻辑,仅仅只是一个底层的数据结构,而DDD中领域层的领域模型就是业务逻辑的体现,基于该模型的原子性业务逻辑均是内聚在模型内部的。那么仓储的作用就是隔离以及衔接数据模型和领域模型,所谓隔离就是降低数据模型的具体实现对领域模型的影响,让开发者更加聚焦于业务,衔接指的是,将领域模型转换为对应的1个或数据模型进行存储。

仓储不是DAO,在实际应用中,由领域层提供抽象(Repository),而基础层实现抽象(RepositoryImpl),在基础层实现中才会真正的使用到DAO。

5、MVC和DDD对比

image-20221129102340736

3、DDD业务流图和数据流图

DDD数据流图: image-20221129102221711

DDD业务流图: image-20221129102144572

4、战略设计

1、事件风暴

事件风暴是DDD战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模。

事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令(动作),再为每一个事件标注出命令发起方的角色。命令可以是用户发起,也可以是第三方系统调用或定时触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。

事件风暴的准备工作:

  1. 事件风暴的参与者

    事件风暴采用工作坊的方式,将项目团队和领域专家聚集在一起,通过可视化、高互动的方式一步一步将领域模型设计出来。除了领域专家,事件风暴的其它参与者可以是DDD专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。

  2. 事件风暴要准备的材料

    事件风暴参与者会将自己的想法和意见写在便利贴上,并将贴纸贴在墙上的合适位置,所以便利贴和水笔是必备材料,另外还可以准备一些胶带或者磁扣,以便贴纸随时能更换位置。

    事件风暴的元素表示:

    image-20220928173256604

    image-20220928193040915

  3. 事件风暴的场地

    有足够大的墙和足够大的空间就可以了。

  4. 事件风暴的关注点

    在领域建模的过程中,我们需要重点关注业务的语言和行为。比如某些业务动作或行为是否会触发下一个业务动作,这个动作的输入和输出是什么?是谁发出的什么动作?触发的这个动作导致了什么事情的发生?根据这种分析思路,分析出领域模型中的事件、动作和实体等领域对象。

2、基于事件风暴构建领域模型

  1. 确定业务目标

    事件风暴的第一步就是弄清楚通过本次事件风暴,最终要满足什么样的业务目标。

    达成目标的共识非常重要,如果参与者对于目标没有什么共识,会在后续的讨论中产生各种各样的分歧,相反,如果目标有了共识,就有了聚焦点,那么在如何实现目标的业务策略上,就有可能会出现很多预想不到的创意。

    这里以订单流程为例,我们的目标就是构建一个用户从下单到支付,最后订单完成的完整流程。

  2. 事件风暴

    1. 探索和发现事件

      画一条时间线,约定左边是时间线的起点,右边是时间线的终点。

      在探索和发现事件时,最重要的一步是放置第一个事件,第一个事件最好是一个真正的“业务结果”,这里我认为的第一个真正的“业务结果”是订单已完成,然后以结果为导向,直到回溯到整个业务流程的源头,就可以得到整个事件流:

    2. 寻找阻碍业务流程发展的因素

      在第1步得到的事件流是最核心的业务流程,但是往往实际情况不会这么一帆风顺,会有很多异常的情况发生,在这里就需要参与者共同发现那些阻碍业务流程发展的因素。

      寻找的核心思路:针对每个事件,思考其上一个事件中可能会发生什么情况导致本事件不能够完成。

      image-20220929214603238

    3. 补充执行者和动作

      image-20220929220510224

    4. 对业务流程进行走查

      最后在结束事件风暴之前,需要对业务流程从前向后进行一次走查。沿着时间线,按照“执行者”-“动作”-“事件”的顺序,把整个业务流程走一遍,查漏补缺,建立参与者关于业务流程的整体认识。

  3. 领域建模

    根据事件风暴过程中产生的领域对象,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划分限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。

    1. 找出实体

      根据事件风暴提取产生这些行为的实体。这里只提取了3个实体作为演示,订单实体、物流单实体、支付单实体。

      image-20220929232025654

    2. 找出聚合根,划分聚合

      根据聚合根的管理性质,从3个实体中找出聚合根,根据业务依赖和业务内聚原则,将聚合根以及它关联的实体和值对象组合为聚合。

      将订单、物流单、支付单分别划分为3个聚合,订单、物流单、支付单分别作为3个聚合的聚合根。

      image-20220929232140431

    3. 划分限界上下文

      根据上下文语义对聚合进行归类,根据订单支付的语境,订单和支付两个聚合共同订单支付域,分别负责下单和支付的领域业务。而物流单则构成物流域,满足物流的领域业务。

      image-20220929232628931
    4. 微服务拆分与设计

      原则上一个领域模型就可以作为一个微服务,但由于领域建模时只考虑了业务因素,没有考虑微服务落地时的技术、团队以及运行环境等非业务因素,因此在微服务拆分与设计时,我们不能简单地将领域模型作为拆分微服务的唯一标准,它只能作为微服务拆分的一个重要依据

      微服务的设计还需要考虑服务的粒度、分层、边界划分、依赖关系和集成关系。除了考虑业务职责单一外,我们还需要考虑将敏态与稳态业务的分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等非业务因素。

5、战术设计

通过战略设计基本上梳理出了微服务的边界以及领域模型,战术设计在战略设计的基础上进行更加详细的设计。

  • 分析微服务领域对象

    • 领域层的领域对象:

      • 设计实体
      • 设计聚合根
      • 设计值对象
      • 设计领域事件
      • 设计领域服务
      • 设计仓储

      应用层的领域对象:

      • 实体方法的封装
      • 领域服务的组合和封装
      • 应用服务的组合和编排
  • 设计微服务代码结构

  • 详细设计

    • 实体属性、数据库表和字段、实体与数据库表映射、服务参数规约及功能实现等
    • 代码开发和测试

3、参考文章

DDD实战课

深入迁出DDDDDD案例实战课

领域驱动设计在互联网业务开发中的实践

领域驱动设计:从理论到实践,一文带你掌握DDD!

基于事件风暴的需求分析

4、参考案例

github.com/ouchuangxin…

github.com/tianminzhen…

github.com/louyanfeng2…

github.com/alibaba/COL…

github.com/AxonFramewo…