Spiga

领域驱动开发分享

2018-09-14 18:39:56

前言

微服务已经成为目前开发的主流设计,我们也要与时俱进不能一直还停留在三层架构的思想层面。年前也更大家简单介绍过什么是微服务,以及实现微服务中需要用到的一些相关技术。之前一直停留在工具和代码阶段,那具体应该如何设计如何落地呢?微服务设计过程中往往会面临边界如何划定的问题,微服务到底应该拆多小,不同的人会根据自己对微服务的理解而拆分出不同的微服务,如何我们还是不经过设计,只是靠拍脑袋硬完成开发的话,上线后运维的压力将会远大于单体程序。那是否有合适的理论或设计方法来指导微服务设计呢?答案就是DDD。微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。换句话说,确定了业务边界和应用边界,这个困境也就迎刃而解了.

2004 年埃里克·埃文斯(Eric Evans)发表了《领域驱动设计》(Domain-Driven Design –Tackling Complexity in the Heart of Software)这本书,从此领域驱动设计(Domain Driven Design,简称 DDD)诞生。DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。但 DDD 提出后在软件开发领域一直都是“雷声大,雨点小”!直到 Martin Fowler 提出微服务架构,DDD 才真正迎来了自己的时代。利用 DDD 设计方法来建立领域模型,划分领域边界,再根据这些领域边界从业务视角来划分微服务边界。而按照 DDD 方法设计出的微服务的业务和应用边界都非常合理,可以很好地实现微服务内部和外部的“高内聚、低耦合”。于是越来越多的人开始把 DDD 作为微服务设计的指导思想。现在,很多大型互联网企业已经将 DDD 设计方法作为微服务的主流设计方法了。DDD 也从过去“雷声大,雨点小”,开始真正火爆起来。

DDD知识体系

领域

领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。

核心域:系统最核心并有复杂业务逻辑的业务界限上下文,比如OA系统的物资管理。
支撑域:系统支撑其他界限上下文的基础,比如OA系统的员工基础资料。
通用域:需要使用的基础框架或第三方成熟解决方案,比如OA系统工作流引擎。
工作流引擎是个比较大的领域,目前我们的工作流引擎把所有的功能都放在OA系统里面,这就是一个单体系统。假设想引入分布式微服务架构来替换这个单体系统。那分布式微服务架构就需要划分业务领域边界,建立领域模型。我将工作流领域细分为:流程设计和流程执行两个子域。

限界上下文

领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。子域还可根据需要进一步拆分为子子域,比如流程设计子域可继续拆分为流程图设计和表单设计子域。拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。拿流程图设计举例:又包括了界面设计和规则设计等限界上下文。理论上限界上下文就是微服务的边界。将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。

实体和值对象

在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。

通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。

聚合

DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作。领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。
工作流引擎里面的聚合包括:

流程聚合:WorkFlow(实体)、流程分类(值对象)

节点聚合:WokTask(实体)、Operator(实体)

线路聚合:WorkLink(实体)、TaskVar(实体)

聚合根

传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

领域事件

在DDD设计时我们发现除了命令和操作等业务行为以外,还有一种非常重要的事件,这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。
我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。
扩展概念:事件总线、消息中间体、事件溯源。

DDD分层结构

1.用户接口层

用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。

2. 应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排。
不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。
另外,应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。

3. 领域层

领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
这里特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。

4. 基础层

基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。

程序框架



分层及依赖关系

  • 领域模型层: 领域模型专注业务的设计,不依赖仓储等基础设施层。
  • 基础设施层: 基础设施的仓储层仅负责领域模型的取出和存储。
  • 应用层: 使用CQRS 模式设计应用层。
  • UI层:Web API 是面向前端的交互的接口,避免依赖领域模型。
  • 共享层:将共享代码设计为共享包供其他项目使用。

代码注意事项:

  • 区分领域模型的内在逻辑和外在行为
    • 将领域模型字段的修改设置为私有
    • 使用构造函数表示对象的创建
    • 使用具有业务含义的动作来操作模型字段
    • 领域模型负责对自己数据的处理
    • 领域服务或命令处理者负责调用领域模型业务动作
  • 使用工作单元模式(UnitOfWork)管理实务,使用EF Core实现仓储层
  • 使用领域事件:提升业务内聚、实现模块解耦
    • 由领域模型内部创建事件
    • 由专有的领域事件处理类处理领域事件
    • 根据实际情况来决定是否在同一事务中处理(如一致性、性能等因素)
  • API层只负责用户的输入输出定义、身份认证和授权。与领域服务职责区分开,不承担业务逻辑。
  • 使用集成事件解决跨微服务的最终一致性,使用RabbitMQ来实现EventBus。
  • 使用MediatR:实现命令查询职责分离模式(CQRS)