Skip to content

DDD 之基础篇

概念综述

贫血/充血

  • 事务脚本/领域模型

    • Martin Fowler《企业应用架构模式》提出了两种开发方式:“事务脚本”和“领域模型”,分别对应 “贫血模型”和“充血模型”。
    • 事务脚本 的核心是,可认为大部分的业务处理都是一条条的 SQL,事务脚本把单个 SQL 组织成为一段业务逻辑,在逻辑执行的时候,使用事务来保证逻辑的 ACID。
    • 领域模型 是将数据和行为封装在一起,并与现实世界的业务对象相映射。各类具备明确的职责划分,使得逻辑分散到合适对象中。
  • 贫血模型

    • 数据驱动设计:倾向于依照表的设计,映射成携带数据的类或结构;即只有 状态 而没 行为 的对象,称为 “贫血对象”。
    • 数据和算法分离,即把领域行为和数据实体对象分开。
    • 数据实体对象只有属性和 getter/setter,没有任何的领域行为,成为哑对象。
    • 服务对象 (Service Object,通常在 Logic/Service/Manager 层) 承载业务逻辑,以一种 事务脚本的模式
    • 问题

      • 一种传统的 面向过程的方式;当业务变得复杂,这种模型逻辑耦合,缺乏变化,难以维护。
      • 马丁福勒定义的“贫血模型”是反模式。
      • 贫血领域模型的根本问题:引入了领域模型设计的所有成本,却没有带来任何好处。
      • 最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。
  • 充血模型

    • 面向对象设计的本质是:“一个对象是拥有状态和行为的”。
    • 充血模型是一种 领域模型模式
    • 领域驱动设计引入 限界上下文
  • 案例

    • 一种贫血设计

      • 类:User + UserManager
      • 保存用户调用:userManager.save(user)
    • 一种充血设计

      • 类:User
      • 保存用户调用:user.save()
      • User 有一个行为是:保存它自己,即自我履行。

定义

  • 领域驱动设计是一种处理高度复杂域的设计方法,试图分离技术实现的复杂性;
  • 围绕 业务概念 构建 领域模型 来控制业务复杂性,解决软件难以理解,难以演化等难题。
  • 团队应用它可以成功开发复杂业务软件系统, 使系统在增大时仍然保持敏捷

战略设计

  • 领域

    • 领域:业务所在的问题域,要解决的问题。
    • 核心子域:核心场景,关键业务行为
    • 通用子域:被多个不同子域复用,需关注标准和通用性。
    • 支撑子域:业务必须但非核心,不可复用。
  • 事件风暴

    • 领域建模常用方法,通过业务场景或用户流程分析,找出领域对象,构建聚合,划分限界上下文。
  • 限界上下文

    • 用来封装通用语言和领域对象,保证领域内但一些术语、领域对象没有语义二义性的业务边界。
  • 通用语言

    • 通过团队交流达成共识的,能够简单,清晰,准确描述业务含义和规则的语言。
  • 领域模型

    • 在限界上下文边界内,由若干业务实体、行为组合而成,完成某个单一职责业务能力的对象组合。
  • 失忆症

    • 业务的自我不明确。

战术设计

  • 聚合

    • 高度依赖的聚合根、实体和值对象集合,按照聚合业务规则保证数据一致性。
  • 聚合根

    • 根实体,有全局唯一标识。
  • 实体

    • 聚合内唯一标识,有生命周期,充血模型。
  • 值对象

    • 关注数值,无标识,数据整体替换,不可变更
  • 领域服务

    • Domain Services,聚合内多实体组合业务逻辑
  • 应用服务

    • 跨聚合服务组合、编排和协调
  • 设计模式

    • 工厂模式:数据初始化,工厂 (Factories)
    • 仓储模式:数据持久化,仓储 (Repositories)
  • 分层架构

    • DDD 经典四层
    • 洋葱架构
    • 六边形架构:端口-适配器架构
    • CQRS
  • 领域事件

    • Domain Events,触发进一步业务操作

领域驱动设计过程

一般实流程

  • 图示

x

  • 如何处理高复杂度的领域问题?
  • 定义:问题 > 能力
  • 思路:提升能力 or 拆分问题
  • DDD 是一种处理问题的 “分治” 思想。
  • 方法:问题空间 -> 分别求解(各子域名知识体系) -> 合并解

  • 领域分解&子域属性

    • 子域

      • 核心子域:核心产品或关键业务
      • 通用子域:单一职责的功能聚合,如支付
      • 支持子域:必须但非核心,不会被复用
    • 为什么区分子域?

      • 资源有限情况下,针对不同子域制定不同战略资源投入
      • 确保关键资源投入核心子域
  • 从领域到领域模型,从问题空间到解决方案空间

    • 业务宏观(战略)

      • 领域划分子域
      • 找出限界上下文
      • 构建领域模型
    • 技术微观(战术)

      • 拆分微服务
      • 建立微服务内对象依赖关系
      • 完成微服务内分层和解耦设计

1)领域分解(战略)

  • 输入

    • 问题空间,业务领域
  • 输出

    • 核心子域,通用子域,支撑子域
  • 方法或工具

    • 业务架构方法,流程和功能分析,业务场景分析
  • 参与者

    • 企业战略人员,领域专家,架构管理者,需求分析

2)领域建模(战略)

  • 输入

    • 核心子域,通用子域,支撑子域
  • 输出

    • 领域模型,限界上下文,上下文映射,聚合、实体、值对象
  • 方法或工具

    • 事件风暴工作坊,场景分析,用例分析
  • 参与者

    • 领域专家,架构师,项目团队成员

3)微服务设计(战术)

  • 输入

    • 领域模型,限界上下文,上下文映射,聚合、实体、值对象
  • 输出

    • 微服务及外部服务依赖
    • 微服务内聚合,实体,值对象,微服务分层及依赖
    • 微服务代码结构
    • 领域模型与代码模型映射
  • 方法或工具

    • 分层架构模型
    • CQRS
    • 领域事件驱动模型
    • 微服务拆分和设计原则
  • 参与者

    • 架构师,项目团队成员

4)详细设计和技术实现(战术)

  • 输入

    • 微服务代码结构
  • 输出

    • API设计和服务规约
    • 数据模型设计
    • 测试和集成
    • 部署和运维
    • 非功能约束
  • 方法或工具

    • 开发,设计原则,测试,运维等
  • 参与者

    • 架构师,项目团队成员

战略设计

关于问题域

  • 限界上下文(Bounded Context)

    • 限界上下是领域模型的知识语境。
    • 限界上下是业务能力的纵向切分;分而治之,开闭原则。
    • 限界上下文是架构层次的自治单元。

      • 自我履行
      • 最小完备
      • 稳定空间
      • 独立进化
    • 什么是业务的边界?边界内该干什么,不该干什么?

    • BC 是一个显式的边界,领域模型存在于边界之内。
    • 限界上下文确保领域(或子域)内的语义无歧义,且严格控制进入子领域的对象。
    • 作用

      • 领域逻辑层面:限界上下文确定了 领域模型的业务边界 ,维护了模型的完整性与一致性,从而降低系统的业务复杂度。
      • 团队合作层面:限界上下文确定了 开发团队的工作边界 ,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度。
      • 技术实现层面:限界上下文确定了 系统架构的应用边界 ,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低系统的技术复杂度。
  • 上下文映射图( Context Map)

    • BC 之间通过 Context Map 进行集成。
    • BC 之间交流需要进行翻译。
    • 比如 ACL(Anticorruption Layer,防腐层)。
  • 通用语言 UL(Ubiquitous Language)

    • 统一的领域术语;领域行为描述。
    • UL 需要让每个概念在各自的上下文中清晰无歧义。
    • 不管你在团队中的角色如何,只要你是团队的一员,你都将使用UL。
  • 领域 & 子域划分

领域建模

  • 领域行为分析(6W 模型)

    • 图示

    x - 用户故事

    - 描写场景的过程必须包含 What/Who/Why/Where/When/hoW 六要素,领域专家或业务分析师从领域中提炼出“场景”,然后讲出你的故事。
    - 从 UI 操作去表现业务行为
    - 描述技术实现而非业务需求
    
    • 在领域驱动设计中,主要分为战略建模和战术建模,其中战略到战术的思考本身可以看做是一种纵向到横向的信息归类,以便于我们结构化、有序化的理解和思考
    • 我们在编写用户故事时,应该按照行为驱动开发的要求,关注于做什么(what),而不是怎么做(how)。
    • 在业务建模阶段,业务才是重心,不能舍本逐末。
  • 领域建模过程

    • 图示

    x - 事件风暴工作坊

    x - 1)寻找领域事件

    x

    x - 2)寻找命令

    x

    x - 3)寻找聚合

    x

    x - 4)划分限界上下文

    x

    x

    战术设计

定义

  • 和领域专家沟通交流方式,建立业务与设计的 通用语言,抛开技术细节,建立 领域模型
  • 将领域模型 翻译 成相应的战术设计组件。

核心概念

  • 图示

x - 实体 (Entity)

- 实体就是领域中需要唯一标识的领域概念
- 实体有自己的生命周期,有 **属性(状态)** 和 **领域行为**。
- 属性(状态)可变。
  • 值对象 (Value Object)

    • 有自己的属性和行为;可以有唯一标识。
    • 值对象相等性

      • 如果两个对象所有的属性的值都相同,我们认为它们是同一个对象。
    • 可替换性

    • 无副作用行为
    • 所有属性都是只读的
  • 聚合/聚合根 (Aggregate/AggregateRoot)

    • 聚合,其实是对象的依赖关系。
    • 聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。
    • 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体
    • 最终一致性

      • 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念。
    • 聚合设计原则

      • 设计小聚合 ;大的聚合即便能保证事务的一致性,也依然可能限制系统的性能可伸缩性。
  • 领域服务 (Domain Service)

    • 对于领域中的一些概念不太适合建模为实体对象或值对象;DDD 认为服务是一个很自然的范式用来对应这种跨多个对象的操作。
    • 领域服务没有状态,只有行为;领域服务处理一系列无状态的逻辑过程,而状态应维护于实体中。
    • 实体(或聚合)和值对像才是 DDD最重要的建模对象, 若反而首先考虑领域服务,容易导致“贫血领域模型”。
  • 应用服务(Application Service)

  • 领域事件 (Domain Event)

    • 将领域中所发生的活动建模成一系列的离散事件。
    • 领域事件的技术实现实质上观察者模式的实现。
    • 使得微服务可以保持松耦合
    • 消息设施最终一致性
    • 事件存储

      • 1)将事件存储作为一个消息队列使用。
      • 2) 检查由模型命令方法的所产生的所有结果的记录。
      • 3) 使用事件存储中的数据进行业务预测和分析。
      • 4) 通过事件存储重一个聚合。
      • 5) 撤销对聚合的操作。
  • 工厂 (Factories)

    • DDD 中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。
    • 工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,工厂的作用是将创建对象的细节隐藏起来。
  • 仓储 (Repositories)

    • 仓储被设计的原因:领域模型中的对象自从被创建出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,然后当需要的时候我们会重建该对象;重建对象就是根据数据库中已存储的对象的状态重新创建对象的过程。
    • 仓储里面存放的对象一定是聚合,我们永远不会单独对某个聚合内的子对象进行单独查询或做更新操作。

分层架构

分层依据

  • 面对变化
  • 基于关注点,为不同的目的划分层次

分层规范

  • 严格分层架构 中,某层只能与位于其直接下方的层发生耦合。
  • 而在 松散分层架构 中,则允许某层与它的任意下方层发生耦合。

模式一:经典四层架构

  • 《领域驱动设计-软件核心复杂性应对之道》by Eric Evans
  • 分层定义

    • 图示

    四层 - 1) User Interface为用户界面层(或表示层),负责向用户显示信息和解释用户命令。 - 2) Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。 - 3) Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。 - 4) Infrastructure层为基础实施层 (“L”型),向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。

  • 注意

    • 用户接口层、应用层、领域层如果要使用基础设施层中的能力,只能通过 IOC 的方式进行依赖注入,这也遵从了面向对象编程中的依赖倒置原则
  • 一种实践

    • 1) User Interface层主要是Restful消息处理,配置文件解析,等等。
    • 2) Application层主要是多进程管理及调度,多线程管理及调度,多协程调度和状态机管理,等等。
    • 3) Domain层主要是领域模型的实现,包括领域对象的确立,这些对象的生命周期管理及关系,领域服务的定义,领域事件的发布,等等。
    • 4) Infrastructure层主要是业务平台,编程框架,第三方库的封装,基础算法,等等。

模式二:DCI & 五层架构

  • 《DCI架构:面向对象编程的新构想》by James O. Coplien & Trygve Reenskau
  • Why DCI

    • DCI要解决的根本问题:即 “系统是什么” 和 “系统做什么”。
    • 用户认知对象和及其领域,而对象须在用例中扮演角色来完成与其它对象的交互。
    • 正因为用户能把两种视角合为一体,类的对象除了支持所属类的成员函数,还可以执行所扮演角色的成员函数。
    • 因此,我们希望把角色的逻辑注入对象,让这些逻辑成为对象的一部分。
    • 比如在编译时,就为对象安排好扮演角色时可能需要的所有逻辑;甚至,在运行时才知道了被分配的角色,然后注入要用到的逻辑。
    • 算法及角色-对象映射由 Context 拥有。Context “知道” 在当前用例中应该找哪个对象去充当实际的演员,然后负责把对象“cast”成场景中的相应角色。
    • 我们只要触发 Context 里的“开场”角色,代码就会运行下去。
  • DCI 架构 (Data、Context & Interactive)

    • 1) Data 层:描述系统有哪些领域概念及其之间的关系,该层专注于领域对象的确立和这些对象的生命周期管理及关系;让程序员站在对象的角度思考系统,从而让 “系统是什么” 更容易被理解。
    • 2) Context 层:是尽可能薄的一层。Context往往被实现得无状态,只是找到合适的 role,让 role 交互起来完成业务逻辑即可。
    • 3) Interactive 层:主要体现在对role的建模,role 是每个 context 中复杂的业务逻辑的真正执行者,体现“系统做什么”。role 是对行为进行建模,联接context 和领域对象。
    • 图示

    四层 - 再论贫血/充血

    • 面向对象建模会面临一个问题:数据边界和行为边界往往不一致。
    • 遵循模块化的思想,我们通过类将行为和其紧密耦合的数据封装在一起。但是在复杂的业务场景下,行为往往跨越多个领域对象,这样的行为如果放在某一个对象中必然会导致别的对象需要向该对象暴漏其内部状态。
    • 所以面向对象发展的后来,领域建模出现两种派别之争,一种倾向于将跨越多个领域对象的行为建模在领域服务中。如果这种做法使用过度,则会导致领域对象变成只提供一堆 get 方法的哑对象,这种建模结果被称之为 贫血模型
    • 而另一派则坚定的认为方法应该属于领域对象,所以所有的业务行为仍然被放在领域对象中,这样导致领域对象随着支持的业务场景变多而变成上帝类,而且类内部方法的抽象层次很难一致。另外由于行为边界很难恰当,导致对象之间数据访问关系也比较复杂,这种建模结果被称之为 充血模型
    • DCI目前广泛被看作是对DDD的一种发展和补充,用在基于面向对象的领域建模上。显式的对role进行建模,解决了面向对象建模中的充血模型和贫血模型之争。DCI通过显式的用role对行为进行建模,同时让role在context中可以和对应的领域对象进行绑定(cast),从而 既解决了数据边界和行为边界不一致的问题,也解决了领域对象中数据和行为高内聚低耦合的问题
  • DDD with DCI

    • 引入DCI 后,DDD 四层架构模式中的 Domain 层变薄:
    • 1)Domain 层只保留了 DCI 中的 Data 层和Interaction层,通过 object 和 role 来实现。
    • 2)DCI 中的 Context 层从 Domain 层上移变成Context 层。
  • 一种实践

    • 1) User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Application层的接口。
    • 2) Application层是应用层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Context层与本次业务相关的上下文进行处理。
    • 3) Context是环境层,以上下文为单位,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
    • 4) Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
    • 5) Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等。

模式三:六边形架构 Alistair

  • 理论基础

    • 依赖倒置原则 (DIP, Dependency Inversion Principle) by Robert C. Martin 正式定义:

      • 高层模块不应该依赖于底层模块,两者都应该依赖于抽象。
      • 抽象不应该依赖于细节,细节应该依赖于抽象。
    • DDD 分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构被推平;

    • 再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。
  • 描述

    • 六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。
    • 对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。
  • 图示

四层 - 图示

x

其他模式:六边形的演变

  • 1) 洋葱架构 by Jeffrey Palermo 2008
  • 2) 干净架构 (Clean Architecture) by Robert C. Martin 2012
  • 3) Life Preserver by Russ Miles 2013
  • 三位一体 :限界上下文、六边形和微服务

x - CQRS 模式 & Event Sourcing

最后思考

领域建模之道

  • 方法论

    • “用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。
    • 《老子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用户需求。
    • 因此,建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。
  • 方法

    • 1)设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什么;而应从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律作为出发点去思考问题。
    • 2)领域模型是排除了人之外的客观世界模型;若人在领域模型中占比太多,那么各个系统的领域模型将没有差别,领域建模毫无意义;领域模型是与谁用和怎样用是无关的客观模型,它自我履行。