DDD(领域驱动设计)分享(1/2)

Posted 蒋楠鑫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DDD(领域驱动设计)分享(1/2)相关的知识,希望对你有一定的参考价值。

这里写自定义目录标题

一、概述

领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。

领域驱动设计的前提是:

  • 把项目的主要重点放在核心领域(core domain)和域逻辑
  • 把复杂的设计放在有界域(bounded context)的模型上
  • 发起一个创造性的合作之间的技术和域界专家以迭代地完善的概念模式,解决特定领域的问题

相比于传统的三层架构来说,**DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。**因此微服务和DDD能够完美地契合,采用DDD架构能够使得微服务的项目拓展性更强,更加内聚,目前大部分的技术团队都会向DDD架构转型,而在进行领域驱动设计时,最重要的一步就是领域建模,建立清晰合理的领域模型可以大大降低服务间的耦合度,让系统设计更加规范。同时,划分合适的领域边界有助于开发人员专注于各自领域的开发,降低了开发成本。

至于DDD带来的好处,目前大家基本已经达成了共识,我总结了一下,包括但不限于以下几点:

  • 由于对业务进行了专业的领域划分,使得业务逻辑更加清晰,正确的业务归类有利于后续业务扩展。同时由于有领域的划分,复杂的系统往往会被拆分的很细致,这样就能将复杂的系统简单化,更加便于业务人员和研发人员的理解。
  • 领域对象面向对象编程,使得代码工程更加高内聚。将业务逻辑分散到各个领域对象中,使得对象外部代码更加精减。
  • 基于以上两个优点,衍生出另一个强大的优点:方便快速迭代,对复杂的系统进行拆分之后每个子系统(领域)都有专门的人负责维护,职责分明,每个人只用关系自己负责的子系统(领域)的逻辑,对于其他系统的调用只需要使用通用的网络协议请求即可,不用关系里面业务逻辑实现。

二、领域划分

DDD第一步要做的就是将复杂的业务模型进行拆分,将各种明确或者模糊的概念根据相关性进行组合,最终要确定出合适的范围,确保每个划分出来的集合能够独立的完成一个完成的事件,同时要确保所有的事件组合在一起能够达到我们最终的目的,那么每个这样的集合就是一个完整的领域。以电商系统为例,假设我们现在要设计一个电商系统,能够完成用户下单、物流配送、商品管理、库存管理、商品采购等功能,对功能进行拆分,划分出了如下的领域:

三、子域和限界上下文

确定了领域之后,需要对领域中的职责进行更加清晰的划分,一个大的领域其中可能还包含很多子领域,每个子领域也有其完整的业务逻辑和清晰的边界,因此对于一个复杂的领域,我们还要进行子域的划分。划分子域需要考虑的最重要的因素就是子域的边界,子域的内部逻辑完整,而同一个大的领域中的多个子域联系紧密,子域应当是每个领域中最小编排单元,子域不能在包含更小的子域。至于限界上下文则是宏观上的划分,一般和子域一一对应,是一个抽象的概念,用来描述每个子域的边界。以上面的采购领域为例,我们一般会划分如下的子域:

四、领域实体

DO(Domain Object)

**DO即实体,不仅拥有各种属性,还拥有一些常见的行为,除此之外还拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。**拿上述提到的供应商子域来说,实体至少应该包含供应商信息,作为实体除了包含如供应商名称、供应商代码、状态等形形色色的属性外,还必须有唯一标识如供应商ID,不管供应商信息的状态如何,属性如何变化,这个唯一标识一旦生成就不会改变。另外采用充血模型(后续会提到)建模时,每个实体都还会包含一定的行为,如新增实体、查询实体、更新实体、删除实体、名称重复校验等。领域外部需要获取和操作领域数据必须通过实体,而不能直接去操作底层。

VO(Value Object)

**VO即值对象,只拥有各种属性值,将多个相关的属性组合成一个对象。**值对象是领域中最常见的一类对象,与实体最大的区别就在于值对象不拥有任何行为只是属性的组合,同时,值对象一般不包含唯一标识,往往伴随实体一起出现。接着上面的供应商实体展开,对于供应商的信息可能还包括的属性,比如供应商的联系人信息、供应商的付款信息等,这些可能和供应商是多对一的关联关系,不能简单的作为实体的一个属性,但是如果拓展为单独实体又不合适,因为他们是和供应商信息紧密相关的,不会单独出现,而且也没有逻辑上的唯一标识,因此这些属性根据相关性组合在一起就产生了值对象:供应商联系人信息值对象、供应商付款信息值对象。

DTO(Data Transfer Object)

**DTO即数据传输对象,只拥有各种属性值,作为服务请求参数,隔离领域实体,不对外暴露核心逻辑。**简单来说就是主要作用在应用层,作为服务的请求参数,保护核心的实体的属性和行为不对调用方直接暴露,DTO和DO往往并不是单纯地一对一的关系,可能存在一对多或者多对多的情况。比如,我们要查询供应商数据,至少会有分页查询集合、查询详情两个功能,在设计接口时不应该将供应商实体直接作为入参暴露给调用方,因为实体中有很多核心的行为,直接暴露其实等同于把源码发给别人一样危险,因此我们需要创建DTO来接收调用方的参数,然后在工厂(防腐层)中转换成对应的实体。

PO(Persistent Object)

**PO即数据库实体模型,与数据库的表一一对应。**PO应该是我们最熟悉的对象了,只要使用了ORM映射框架数据库表就一定会有对应的model,每张表生成的model对象就是我们所说的PO。PO唯一的作用就是数据的持久化,PO和数据库表结构是完全对应的,只拥有数据库表对应的字段,不包含其他属性,更没有行为。

BO(Bussiness Object)

**BO即业务逻辑对象,是与具体的业务逻辑强关联的数据模型,同样也是只拥有各种属性值。**简单来说BO可以理解为接口的返回值,每个独立的功能都有自己的BO。对于我们一直提到的供应商信息子域来说,分页查询和查询详情就应该分别对应两个不同的BO对象,而对于搜索供应商的功能,不管有多少个接口,返回的BO都应该是同一个,因为业务逻辑相似,因此BO不能简单的根据接口来确定,主要还是和业务逻辑强绑定。

五、聚合与聚合根

在了解聚合与聚合根之前,先举个例子,社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作,朝着一个最大的目标前进,发挥出更大的力量。**领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。**换个角度去理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

了解完聚合之后,我们再来看看聚合根,把聚合比作社团等组织,那么聚合根就是社团的负责人,需要对整个社团的发展和管理负责。**聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。**传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。聚合根作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。其次聚合根本身也是一个实体拥有实体的属性和业务行为,实现自身的业务逻辑。

接着上面提到的采购子域说下去,假设一个完整的采购事件需要先创建备货计划,再生成采购需求,最后确认采购需求生成采购单,对于这样一个流程

设计聚合的一些原则

**1.聚合用来封装真正的不变性,而不是简单地将对象组合在一起。**聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。

**2.设计小聚合。**如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。

**3.通过唯一标识引用其它聚合。**聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。

**4.在边界之外使用最终一致性。**聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。

六、贫血模型和充血模型

1.贫血模型

**将数据与操作分离,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。**传统的MVC三层架构中,贫血模型四处可见,MVC架构中我们一般会将系统分为Controller 层(对外暴露的访问接口)、Service层(业务逻辑)、Repository 层(数据持久化),侧重于Service层,其中几乎所有的业务逻辑基本都在Service中,对于业务模型来说都是简单的属性组合,类似于上面所说的DDD中的值对象。而对于DDD架构来说贫血模式显然不符合我们的设计原则,遵循贫血模式设计出来的架构往往"低内聚,高耦合",这与DDD的初衷背道而驰。

2.充血模型

充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 各类业务逻辑实体。不过,Domain 既包含数据,也包含业务逻辑,包括数据的持久化,而Service类就比较单薄,主要负责领域实体的编排。

七、领域服务

上面所说的领域实体已经有足够的能力完成独立并且相对完整的功能,但是业务逻辑并不可能都这么简单,很多复杂的业务逻辑一个实体无法完成,需要多个实体共同参与才能完成,这种跨实体的业务流程就需要在领域服务中进行实现,简而言之:**实体方法完成单一实体自身的业务逻辑,是相对简单的原子业务逻辑,而领域服务则是多个实体组合出的相对复杂的业务逻辑。**老生常谈,还是拿上面采购子域来说,生成采购订单后,需要对采购订单进行下单操作,完整的流程包括更新采购单状态、更新采购单明细数量、生成结算付款单等一系列操作,可能还会涉及到库存的变动,对此单一的采购单实体已经不能逻辑自洽,需要多个领域实体共同完成这个流程,对于这种情况无论将业务逻辑封装在哪个实体中都不符合设计原则,因此我们需要一个能够操作多个实体的结构,这个结构就是我们说的领域服务。一般情况下一个子域只有一个领域服务,用来编排领域内的各个实体完成各种复杂的流程,同时对于跨领域的数据操作也需要通过领域服务之间的交互来完成。除此之外,领域服务一般还负责事务的处理和远程服务的调用等。

八、总结

综合一下上面所有的内容,对于一个采购领域,我们完整的DDD设计大致如下:

以上是关于DDD(领域驱动设计)分享(1/2)的主要内容,如果未能解决你的问题,请参考以下文章

DDD领域驱动设计:CQRS架构模式

DDD领域驱动设计:四层架构应用

DDD领域驱动设计:四层架构应用

领域驱动设计 领域事件DDD分层架构

DDD(领域驱动设计)系列之二-应用架构

DDD领域驱动开发