DDD领域驱动设计详解

DDD领域驱动设计详解1 领域驱动概述1 1 领域驱动简介1 2 领域驱动优点1 3 领域驱动解决复杂度方式1 4 领域驱动疑问2 领域驱动核心知识2 1

1. 领域驱动概述

1.1 领域驱动简介

领域驱动设计是Eric Evans在2004年发表的Domain Driven Design(领域驱动设计,DDD)著作中提出的一种从系统分析到软件建模的一套方法论。以领域为核心驱动力的设计体系。

从领域驱动定义来看,领域驱动设计-软件核心复杂性应对之道,从Eric 定义中可以看出,领域驱动设计是为了解决复杂的软件设计,而且只是解决软件复杂性的一种方式,并不是唯一选择。另外不是所有的业务服务都合适做DDD架构,DDD适合产品化,可持续迭代,业务逻辑足够复杂的业务系统,对于系统初期业务逻辑相对比较简单的应用,传统MVC架构更具有优势,可以减少一部分认知成本与开发成本。而且领域驱动设计并不是万金油,只是解决复杂软件的一种方案,领域驱动设计本身只提供了理论思想,具体的落地方案一定是结合具体的业务场景实现的。目前市面上也有很多依据领域驱动思想落地的开源框架可以参考。

从领域驱动对应关系来看,一方面目前很多建设中台的时候大多采用DDD思想落地,DDD很多思想比如领域划分,领域事件,领域服务,边界上下文划分,充血模型,代码防腐,统一语义等等可以很好的帮助实现中台的落地,但是中台落地DDD并不是唯一选择。另一方面对于DDD的这些思想,与DDD的关系更多是聚合关系,而不是组合关系,也就是在具体应用开发中,即使采用传统的MVC架构,这些思想依然可以很好的发挥其作用。

1.2 领域驱动优点

DDD最大的好处是:接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。DDD让你首先考虑的是业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同导致编程世界观不同。

1.面向对象设计,数据行为绑定,告别贫血模型。
2.优先考虑领域模型,而不是切割数据和行为。
3.业务语义显性化,准确传达业务规则。
4.代码即设计,通过领域设计即可很清晰的实现代码。
5.它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现业务和技术统一的架构演进。

领域驱动设计,又称"软件核心复杂性应对之道"。是一套基于对象思维的业务建模设计思想,相对于 CRUD 系统有更高的灵活性,是业务人员处理复杂问题的有效手段。

通用语言:“一个团队,一种语言”,将模型作为语言的支柱。确保团队在内部的所有交流中,代码中,画图,写东西,特别是讲话的时候都要使用这种语言。例如账号,转账,透支策略,这些都是非常重要的领域概念,如果这些命名都和我们日常讨论以及PRD中的描述保持一致,将会极大提升代码的可读性,减少认知成本。说到这,稍微吐槽一下我们有些工程师的英语水平,有些神翻译让一些核心领域概念变得面目全非。
显性化:就是将隐式的业务逻辑从一推if-else里面抽取出来,用通用语言去命名、去写代码、去扩展,让其变成显示概念,比如"透支策略"这个重要的业务概念,按照事务脚本的写法,其含义完全淹没在代码逻辑中没有突显出来,看代码的人自然也是一脸懵逼,而领域模型里面将其用策略模式抽象出来,不仅提高了代码的可读性,可扩展性也好了很多。

在这里插入图片描述

1.3 领域驱动解决复杂度方式

首先, 典型的DDD实现了业务复杂度和技术复杂度的隔离,通过分层架构隔离了关注点,举个例子,在传统的DDD四层架构中,DDD划分出了领域层、仓储层、基础设施层、接口层;

在领域层中,存放业务逻辑的关注点,即所谓的领域行为;在应用层中,DDD暴露出了 业务用例级别 (Use Case)的服务接口,粘合业务逻辑与技术实现;在基础设施层中,DDD集中放置了支撑业务逻辑的技术实现,如:MQ消息发送、对缓存的操作等;在仓储层中,DDD放置了和领域状态相关的逻辑,打通了领域状态持久化与存储设施之间的联系。

除了划分不同分层外,DDD还提出了一个建设性的概念: “限界上下文(Bounded Context)”,通过限界上下文对业务流程分而治之,切分为不同的子系统,在每个子系统中利用DDD的分层架构/六边形架构等思想分别进行逻辑分层。通过这样的分治之后,DDD帮我们将业务复杂度隔离到了每个细分的领域内部,而且DDD本身的分治思想,也帮助我们隔离了业务需求和技术需求的关注点。
在这里插入图片描述这是一个典型的领域驱动设计分层架构,蓝色区域的内容与业务逻辑有关,而灰色区域的内容则与技术实现有关。这二者泾渭分明,最后汇合在应用层。
应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(DI,Dependency Injection)的方式将二者结合起来。充分体现了DDD能够隔离技术复杂度与业务复杂度的特点。

1.4 领域驱动疑问

对于领域驱动设计与传统架构设计,不同的人有不同的见解,也可以理解,这里梳理领域驱动设计也不是认为传统架构有什么问题,具体采用什么样的架构设计一定是根据现有架构并结合当前实际的业务场景所决定的,任何撇开业务场景谈架构都是耍流氓,就像撇开剂量谈药性一样荒诞。

其次,领域驱动设计全称叫领域驱动设计软件核心复杂性应对之道,是为了更好的解决复杂软件的架构设计,但是这并不意味着领域驱动设计就是万金油,可以解决所有的问题。面对复杂的业务也会存在复杂的聚合,也会存在性能问题,也需要做幂等,也需要高可用,高性能,高并发等等,会与传统架构一样存在这些问题,不同的架构设计工具解决不同的问题,与领域驱动也并不冲突。

不管是领域驱动设计还是传统三层架构都是面向对象设计,很多地方认为传统是面向数据,面向过程,数据也是对象,过程也是对象,只不过领域驱动更加贴合业务,以业务为核心展开的设计开发,对于聚合问题,业务的复杂必然带来聚合复杂,也是正常现象,不是说使用了DDD就变得不复杂了,对于性能问题,与DDD完全两个概念,领域驱动解决的是软件核心复杂性应对之道,是为了更好的应对复杂业务,使技术实现与业务更好的分离,使外部调用与内部系统相隔离,采用充血,聚合,代码防腐,领域事件,领域服务。对于性能的问题采用相应性能对应的技术手段去解决,而不应该把问题归咎于领域驱动设计。

2. 领域驱动核心知识

2.1 领域知识概念

DDD的核心知识体系主要包括领域、子域、核心域、支撑域、通用域、限界上下文、实体、值对象、聚合、聚合根等概念。
在这里插入图片描述

2.2 领域战略战术设计

DDD有战略设计和战术设计之分。战略设计主要从高层"俯视"我们的软件系统,帮助我们精准地划分领域以及处理各个领域之间的关系;而战术设计则从技术实现的层面教会我们如何具体地实施DDD。

战略建模-Strategic Modeling
限界上下文(Bounded Context)
上下文映射图(Context Mapping)

战术建模-Tactical Modeling:
聚合-Aggregate
实体-Entity
值对象-Value Objects
资源库-Repository
领域服务-Domain Services
领域事件-Domain Events
模块-Modules

在这里插入图片描述在这里插入图片描述

3. 领域驱动战略设计

3.1 战略设计概述

需要指出的是,DDD绝非一套单纯的技术工具集,但是我所看到的很多程序员却的确是这么认为的,并且也是怀揣着这样的想法来使用DDD的。过于拘泥于技术上的实现将导致DDD-Lite。简单来讲,DDD-Lite将导致劣质的领域对象,因为我们忽略了DDD战略建模所带来的好处。
DDD的战略设计主要包括领域/子域、通用语言、限界上下文和架构风格等概念。

3.2 领域与子域

在这里插入图片描述

3.3 限界上下文

在这里插入图片描述

3.4 领域场景分析

在这里插入图片描述

3.5 四色建模法

在这里插入图片描述

3.6 事件风暴结果图

在这里插入图片描述

3.7 限界上下文依赖结果图

在这里插入图片描述

4. 领域驱动战术设计

4.1 战术设计概述

领域驱动设计,整体包括战略和战术两部分,其中战略部分的落地需要团队合作、开发过程、流程制度等一系列支持,实施阻力相对较大。相反,战术部分,是一组面向业务的设计模式,是基于技术的一种思维方式,相对开发人员来说比较接地气,是提升个人格局比较好的切入点。

战略设计为我们提供一种高层视野来审视我们的软件系统,而战术设计则将战略设计进行具体化和细节化,它主要关注的是技术层面的实施,也是对我们程序员来得最实在的地方。

战术模式包含若干构造块模式,以便能够构建有效的领域模型。
战术模式严重依赖于领域模型和通用语言,通过技术模式将领域模型和通用语言中的概念映射到代码实现中。随着模型的进化,代码实现也会进行重构,以更好的体现模型概念。当然,从技术重构角度也会发现一些隐含领域知识(概念),这些新的发现也会对领域模型产生影响。

战术模式和通用语言一样,都工作在特定限界上下文内,其应用边界受限界上下文的保护。

4.2 战术模式

战术模式的作用是管理复杂性并确保领域模型中行为的清晰明确。可以使用这些模式来捕获和传递领域中的概念、关系、规则。

每个构造块模式都具有单一职责:
1.代表领域中的概念。如实体、值对象、领域服务、领域事件、模块等;
2.用于管理对象的生命周期。如聚合、工厂、仓库等;
3.用于集成或跟踪。领域事件、事件溯源等。

在这里插入图片描述

4.3 领域建模模式

他们表述实现与模型间的关系,将分析模型绑定到代码实现模型。主要用于在代码中表述模型元素的模式。

1.实体
实体表述的是领域中的概念,它是由身份而不是属性来定义的。
实体的身份标识在生命周期中保持不变,但其属性会发生变化。实体以身份标识作为唯一凭证,沿着时间轴,记录了实体所有变更事件。
实体的一个实例是产品,一旦产品被生成好,其唯一身份就不会发生变化,但是其描述信息、价格等可以被多次修改。

2.值对象
值对象代表仅通过数据区分的领域元素和概念。用作模型中元素的描述,它不具有唯一标识。
值对象不需要唯一标识,是因为它总是与另一个对象相关联,是在一个特定上下文中被解析的。通常,其生命周期会依附于它的关联对象(在这里,主要是实体对象)。
值对象会当做不变对象来设计,在完成创建后,其状态就不能改变了。
值对象比较好的例子就是现金,你无需关系货币的身份,只关心它的价值。如果有人用一张五美元钞票交换你的五张一美元钞票,也不会改变五美元本身。

3.领域服务

在模型中,领域服务封装了不能自然建模为值对象和实体的逻辑、流程和概念。
它本身不具有身份和状态。它的职责是使用实体和值对象编排业务逻辑。
领域服务的一个好例子是运输成本计算器,只要给出一组拖运货物和重量,它就能计算运输成本。

4.模块

模块主要用于组织和封装相关概念(实体、值对象、领域服务、领域事件等),这样可以简化对较大模型的理解。
应用模块可以在领域模型中促成低耦合和搞内聚的设计。
模块作用于单个领域,用于分解模型规模。子域用于限定领域模型适用范围(有界上下文)。

4.4 对象生命周期模式

相对来说,之前提到的模式重点在于表达领域概念。而对象生命周期模式,有点侧重于技术,用于表示领域对象的创建和持久化。

1.聚合
实体和值对象会相互协作,形成复杂的关联关系。我们需要在满足不变条件的前提下,将其拆分为一个个概念上的整体。通常,面对复杂的对象关系,在执行领域对象操作时,难以保证一致性和并发性。领域驱动设计由聚合模式来确保操作的一致性和事务的并发边界。大模型会通过不变性条件来划分,并组成概念化整体的实体和对象组,这个概念化整体便是聚合。

聚合根之间的关系应该通过保持对另一个聚合根 ID 的引用,而非对对象本身的引用来实现。这一原则有助于保持聚合之间的边界并避免加载不必要的对象。

不变性,是在领域模型中强制实现一致性的规则。无论何时对实体或聚合进行变更都要应用该业务规则。聚合外部的对象只能引用另一个聚合的聚合根,聚合中对象的任何变更都需要通过聚合根来完成。聚合根封装聚合数据并公开行为以对其进行修改。
在这里插入图片描述
2.工厂
如果实体或值对象的创建过程非常复杂,可以将其委托给工厂。工厂会确保在领域对象使用之前就满足所有的不变条件。
如果领域对象很简单并且不具有特殊的不变条件,可以使用构造函数代替工厂。当从持久化存储中重建领域对象时,也可以使用工厂。
在这里插入图片描述
3.仓库

仓库主要用于持久化一个聚合。将聚合作为原子单元进行处理,因此,仓库操作的最小单元就是聚合,每个聚合会对应一个仓库。
仓库是用来检索和存储聚合的机制,是对基础框架的一种抽象。
其他模式。在这里插入图片描述

4.4 其他模式

1.领域事件
领域事件表示问题空间中发生了一些业务人员关心的事情。主要用于表示领域概念。

使用领域事件主要有以下两种场景:
1.记录模型的变更历史;
2.作为跨聚合通信方式。

2.事件溯源

传统的仅快照式持久化的一个替代项便是事件溯源。作为实体状态存储的替代,可以存储引发该状态的系列事件。存储所有的事件会提高业务的分析能力,不仅可以得知实体当前状态,还可以得知过去任意时点的状态。
在这里插入图片描述

4.5 总结

实体
        由唯一标识符定义
        标识符在整个生命周期保存不变
        基于标识符进行相等性检查
        通过方法对属性进行更新

值对象
描述问题域中的概念和特征
不具备身份
不变对象

领域服务
处理无法放置在实体或值对象中的领域逻辑
无唯一标识
无状态服务

模块
分解、组织和提高领域模型的可读性
命名空间,降低耦合,提供模型高内聚性
定义领域对象组间的边界
封装比较独立的概念,是比聚合、实体等更高层次的抽象

聚合
将大对象图分解成小的领域对象群,降低技术实现的复杂性
表示领域概念,不仅仅是领域对象集合
确定领域一致性边界,确保领域的可靠性
控制并发边界

工厂
将对象的使用和构造分离
封装复杂实体和值对象的创建逻辑
保障复杂实体和值对象的业务完整性

仓库
是聚合根在内存中的集合接口
提供聚合根的检索和持久化需求
将领域层与基础实施层解耦
通常不用于报告需求

领域事件
业务人员所关心的事件,是通用语言的一部分
记录聚合根的所有变更
处理跨聚合的通信需求

事件溯源
使用历史事件记录替换快照存储
提供对历史状态的查询

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

5. 领域驱动架构模型

5.1 领域驱动基本架构

5.1.1 分层架构

在这里插入图片描述

5.1.2 六边形理论

在这里插入图片描述

5.1.3 CQRS架构设计

在这里插入图片描述

5.2 领域驱动分层架构

在这里插入图片描述

1.用户接口层:面向前端用户提供服务和数据适配。这一层聚集了接口和数据适配相关的功能。
2.应用层:实现服务组合与编排,主要适应业务流程快速变化的需求。这一层聚集了应用服务和时间订阅相关的功能。
3.领域层:实现领域模型的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和领域事件,通过个领域对象的协同和组合形成领域模型的核心业务能力。
4.基础设施层:它贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。
  • 1
  • 2
  • 3
  • 4

5.3 服务调用

在这里插入图片描述

5.4 服务封装与组合

在这里插入图片描述

5.5 领域架构对应关系

在这里插入图片描述

6. 领域驱动落地框架

6.1 leave-sample

中台架构与实现 DDD和微服务,清晰地提供了从战略设计到战术设计以及代码落地。
leave-sample地址:https://gitee.com/serpmelon/leave-sample

6.2 dddbook

阿里技术专家详解DDD系列,例子精炼,项目代码结构与rdfa相似,极具参考价值。
dddbook地址:https://developer.aliyun.com/article/719251

6.3 Xtoon

xtoon-boot是基于领域驱动设计(DDD)并支持SaaS平台的单体应用开发脚手架。重点研究如何应用。xtoon-boot提供了完整落地方案和企业级手脚架;
gitee地址:https://gitee.com/xtoon/xtoon-boot
github地址:https://github.com/xtoon/xtoon-boot

6.4 DDD Lite

DDD 领域驱动设计微服务简化版,简洁、高效、值得重点研究,主要问题是持久化采用的JPA,担心技术人员不熟悉,理论篇。
gitee地址:https://gitee.com/litao851025/geekhalo-ddd

快速入门:https://segmentfault.com/a/1190000018464713
快速构建新闻系统:https://segmentfault.com/a/1190000018254111

6.5 ruoyi_cloud

若依快速开发平台,以该项目建立对阳光智采和rdfa的技术框架基准线。
gitee地址:https://gitee.com/y_project/RuoYi-Cloud

6.6 Cola框架

cola框架是阿里大佬张建飞(Frank) 基于DDD构建的平台应用框架。“让COLA真正成为应用架构的最佳实践,帮助广大的业务技术同学,脱离酱缸代码的泥潭!”

csdn地址:https://blog.csdn.net/significantfrank/article/details/110934799

6.7 Axon Framework

Axon Framework 是用来帮助开发人员构建基于命令查询责任分类(Command Query Responsibility Segregation: CQRS)设计模式的可伸缩、可扩展和可维护应用程序的框架。你只需要把工作重心放在业务逻辑的设计上。通过一些 Annotation ,Axon 使得你的代码和测试分离。

https://www.oschina.net/p/axon
https://www.jianshu.com/p/15484ed1fbde

7. 领域驱动实践

7.1 贫血模型和充血模型

1.贫血模型概念
贫血模型,所谓的贫血模型是在定义对象时,指定以对象的属性信息,却没有对象的行为信息,比如,定义Employee对象会包含id,name,age,sex,role,phone等信息,最后再通过添加一些对象属性的get/set方法来赋值取值操作。
这些贫血对象在设计之初就被定义为只能包含数据,不能加入领域逻辑;所有的业务逻辑是放在所谓的业务层(xxxService, xxxManager对象中),需要使用这些模型来传递数据。

2.充血模型概念
充血模型,在定义对象时不但包含对象的属性信息,还包括对象的行为信息。所以充血模型是一种有行为的模型,模型中状态的改变只能通过模型上的行为来触发,同时所有的约束及业务逻辑都收敛在模型上。

3.贫血模型和充血模型的区别
贫血模型是事务脚本模式,贫血模型相对简单,模型上只有数据没有行为,业务逻辑由xxxService、xxxManger等类来承载,相对来说比较直接,针对简单的业务,贫血模型可以快速的完成交付,但后期的维护成本比较高,很容易变成我们所说的面条代码。
充血模型是领域模型模式,充血模型的实现相对比较复杂,但所有逻辑都由各自的对象来负责,职责比较清晰,方便后期的迭代与维护。充血模型更加符合现实中的对象,因为一个员工在现实世界里不只有姓名,年龄,电话等,还可以工作,吃饭,睡觉等行为,只有属性信息的对象不是一个完整的对象。
面向对象设计主张将数据和行为绑定在一起也就是充血模型,而贫血领域模型则更像是一种面向过程设计,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。
贫血领域模型的根本问题是,它引入了领域模型设计的所有成本,却没有带来任何好处。最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消。如果将所有行为都写入到Service对象,那最终你会得到一组事务处理脚本,从而错过了领域模型带来的好处。而且当业务足够复杂时, 你将会得到一堆爆炸的事务处理脚本。

4.贫血模型与充血模型案例验证
员工贫血模型

@Data
public class Person {
    /**
     * 姓名
     */
    private String name;
<span class="token comment">/**
 * 年龄
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Integer</span> age<span class="token punctuation">;</span>

<span class="token comment">/**
 * 生日
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Date</span> birthday<span class="token punctuation">;</span>

<span class="token comment">/**
 * 当前状态
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Stauts</span> stauts<span class="token punctuation">;</span>

}

public class PersonServiceImpl implements PersonService {
public void sleep(Person person) {
person.setStauts(SleepStatus.get());
}
public void setAgeByBirth(Person person) {
Date birthday = person.getBirthday();
if (currentDate.before(birthday)) {
throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
}
int yearNow = cal.get(Calendar.YEAR);
int dayBirth = bir.get(Calendar.DAY_OF_MONTH);
/大概计算, 忽略月份等,年龄是当前年减去出生年/
int age = yearNow - yearBirth;
person.setAge(age);
}
}

public class WorkServiceImpl implements WorkService{
public void code(Person person) {
person.setStauts(CodeStatus.get());
}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

这一段代码就是贫血对象的处理过程,Person类, 通过PersonService、WorkingService去控制Person的行为,第一眼看起来像是没什么问题,但是真正去思考整个流程。WorkingService, PersonService到底是什么样的存在?与真实世界逻辑相比, 过于抽象。基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了 OOP 的封装特性,实际上是一种面向过程的编程风格。但是,现在几乎所有的 Web 项目,都是基于这种贫血模型的开发模式,甚至连 Java Spring 框架的官方 demo,都是按照这种开发模式来编写的。
面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。

员工充血模型

@Data
public class Person extends Entity {
    /**
     * 姓名
     */
    private String name;
<span class="token comment">/**
 * 年龄
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Integer</span> age<span class="token punctuation">;</span>

<span class="token comment">/**
 * 生日
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Date</span> birthday<span class="token punctuation">;</span>

<span class="token comment">/**
 * 当前状态
 */</span>
<span class="token keyword">private</span> <span class="token class-name">Stauts</span> stauts<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">code</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setStauts</span><span class="token punctuation">(</span><span class="token class-name">CodeStatus</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setStauts</span><span class="token punctuation">(</span><span class="token class-name">SleepStatus</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> 
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setAgeByBirth</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Date</span> birthday <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getBirthday</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">Calendar</span> currentDate <span class="token operator">=</span> <span class="token class-name">Calendar</span><span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>currentDate<span class="token punctuation">.</span><span class="token function">before</span><span class="token punctuation">(</span>birthday<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalArgumentException</span><span class="token punctuation">(</span><span class="token string">"The birthday is before Now,It's unbelievable"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">int</span> yearNow <span class="token operator">=</span> currentDate<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token class-name">Calendar</span><span class="token punctuation">.</span>YEAR<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">int</span> yearBirth <span class="token operator">=</span> birthday<span class="token punctuation">.</span><span class="token function">getYear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">/*粗略计算, 忽略月份等,年龄是当前年减去出生年*/</span>
    <span class="token keyword">int</span> age <span class="token operator">=</span> yearNow <span class="token operator">-</span> yearBirth<span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setAge</span><span class="token punctuation">(</span>age<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>       

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

贫血模型和充血模型的区别

/**
 * 贫血模型
 */
public class Client {
<span class="token annotation punctuation">@Resource</span>
<span class="token keyword">private</span> <span class="token class-name">PersonService</span> personService<span class="token punctuation">;</span>

<span class="token annotation punctuation">@Resource</span>
<span class="token keyword">private</span> <span class="token class-name">WorkService</span> workService<span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Person</span> person <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Person</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    personService<span class="token punctuation">.</span><span class="token function">setAgeByBirth</span><span class="token punctuation">(</span>person<span class="token punctuation">)</span><span class="token punctuation">;</span>
    workService<span class="token punctuation">.</span><span class="token function">code</span><span class="token punctuation">(</span>person<span class="token punctuation">)</span><span class="token punctuation">;</span>
    personService<span class="token punctuation">.</span><span class="token function">sleep</span><span class="token punctuation">(</span>person<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}
/**

  • 充血模型
    */
    public class Client {

    public void test() {
    Person person = new Person();
    person.setAgeByBirth();
    person.code();
    person.sleep();
    }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

上面两段代码很明显第二段的认知成本更低, 这在满是Service,Manage 的系统下更为明显,Person的行为交由自己去管理, 而不是交给各种Service去管理。

7.2 DDD实现银行转账案例

银行转账事务脚本实现在事务脚本的实现中,关于在两个账号之间转账的领域业务逻辑都被写在了MoneyTransferService的实现里面了,而Account仅仅是getters和setters的数据结构,也就是我们说的贫血模型:

public class MoneyTransferServiceTransactionScriptImpl implements MoneyTransferService {
    private AccountDao accountDao;
    private BankingTransactionRepository bankingTransactionRepository;
    //...
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token class-name">BankingTransaction</span> <span class="token function">transfer</span><span class="token punctuation">(</span>
        <span class="token class-name">String</span> fromAccountId<span class="token punctuation">,</span> <span class="token class-name">String</span> toAccountId<span class="token punctuation">,</span> <span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Account</span> fromAccount <span class="token operator">=</span> accountDao<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>fromAccountId<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">Account</span> toAccount <span class="token operator">=</span> accountDao<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>toAccountId<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">//. . .</span>
    <span class="token keyword">double</span> newBalance <span class="token operator">=</span> fromAccount<span class="token punctuation">.</span><span class="token function">getBalance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> amount<span class="token punctuation">;</span>
    <span class="token keyword">switch</span> <span class="token punctuation">(</span>fromAccount<span class="token punctuation">.</span><span class="token function">getOverdraftPolicy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">case</span> NEVER<span class="token operator">:</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>newBalance <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
                <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DebitException</span><span class="token punctuation">(</span><span class="token string">"Insufficient funds"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>
        <span class="token keyword">case</span> ALLOWED<span class="token operator">:</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>newBalance <span class="token operator">&lt;</span> <span class="token operator">-</span>limit<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
                <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DebitException</span><span class="token punctuation">(</span>
                        <span class="token string">"Overdraft limit (of "</span> <span class="token operator">+</span> limit <span class="token operator">+</span> <span class="token string">") exceeded: "</span> <span class="token operator">+</span> newBalance<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">break</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    fromAccount<span class="token punctuation">.</span><span class="token function">setBalance</span><span class="token punctuation">(</span>newBalance<span class="token punctuation">)</span><span class="token punctuation">;</span>
    toAccount<span class="token punctuation">.</span><span class="token function">setBalance</span><span class="token punctuation">(</span>toAccount<span class="token punctuation">.</span><span class="token function">getBalance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">BankingTransaction</span> moneyTransferTransaction <span class="token operator">=</span>
            <span class="token keyword">new</span> <span class="token class-name">MoneyTranferTransaction</span><span class="token punctuation">(</span>fromAccountId<span class="token punctuation">,</span> toAccountId<span class="token punctuation">,</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    bankingTransactionRepository<span class="token punctuation">.</span><span class="token function">addTransaction</span><span class="token punctuation">(</span>moneyTransferTransaction<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> moneyTransferTransaction<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

上面的代码大家看起来应该比较眼熟,因为目前大部分系统都是这么写的。其实我们是有办法做的更优雅的,这种优雅的方式就是领域建模,唯有掌握了这种优雅你才能实现从工程师向应用架构的转型。同样的业务逻辑,接下来就让我们看一下用DDD是怎么做的。银行转账领域模型实现如果用DDD的方式实现,Account实体除了账号属性之外,还包含了行为和业务逻辑,比如debit( )和credit( )方法。

// @Entity
public class Account {
    // @Id
    private String id;
    private double balance;
    private OverdraftPolicy overdraftPolicy;
<span class="token comment">//. . .</span>
<span class="token keyword">public</span> <span class="token keyword">double</span> <span class="token function">balance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">return</span> balance<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">debit</span><span class="token punctuation">(</span><span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>overdraftPolicy<span class="token punctuation">.</span><span class="token function">preDebit</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>balance <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>balance <span class="token operator">-</span> amount<span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>overdraftPolicy<span class="token punctuation">.</span><span class="token function">postDebit</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">credit</span><span class="token punctuation">(</span><span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>balance <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>balance <span class="token operator">+</span> amount<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

而且透支策略OverdraftPolicy也不仅仅是一个Enum了,而是被抽象成包含了业务规则并采用了策略模式的对象.

public interface OverdraftPolicy {
    void preDebit(Account account, double amount);
<span class="token keyword">void</span> <span class="token function">postDebit</span><span class="token punctuation">(</span><span class="token class-name">Account</span> account<span class="token punctuation">,</span> <span class="token keyword">double</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>

}

public class NoOverdraftAllowed implements OverdraftPolicy {
public void preDebit(Account account, double amount) {
double newBalance = account.balance() - amount;
if (newBalance < 0) {
throw new DebitException("Insufficient funds");
}
}

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">postDebit</span><span class="token punctuation">(</span><span class="token class-name">Account</span> account<span class="token punctuation">,</span> <span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>

}

public class LimitedOverdraft implements OverdraftPolicy {
private double limit;
//...
public void preDebit(Account account, double amount) {
double newBalance = account.balance() - amount;
if (newBalance < -limit) {
throw new DebitException(
"Overdraft limit (of " + limit + ") exceeded: " + newBalance);
}
}

<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">postDebit</span><span class="token punctuation">(</span><span class="token class-name">Account</span> account<span class="token punctuation">,</span> <span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>

}

//而Domain Service只需要调用Domain Entity对象完成业务逻辑即可。
public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService {
private AccountRepository accountRepository;
private BankingTransactionRepository bankingTransactionRepository;
//...

<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token class-name">BankingTransaction</span> <span class="token function">transfer</span><span class="token punctuation">(</span>
        <span class="token class-name">String</span> fromAccountId<span class="token punctuation">,</span> <span class="token class-name">String</span> toAccountId<span class="token punctuation">,</span> <span class="token keyword">double</span> amount<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Account</span> fromAccount <span class="token operator">=</span> accountRepository<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>fromAccountId<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">Account</span> toAccount <span class="token operator">=</span> accountRepository<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>toAccountId<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">//. . .</span>
    fromAccount<span class="token punctuation">.</span><span class="token function">debit</span><span class="token punctuation">(</span>amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    toAccount<span class="token punctuation">.</span><span class="token function">credit</span><span class="token punctuation">(</span>amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">BankingTransaction</span> moneyTransferTransaction <span class="token operator">=</span>
            <span class="token keyword">new</span> <span class="token class-name">MoneyTranferTransaction</span><span class="token punctuation">(</span>fromAccountId<span class="token punctuation">,</span> toAccountId<span class="token punctuation">,</span> amount<span class="token punctuation">)</span><span class="token punctuation">;</span>
    bankingTransactionRepository<span class="token punctuation">.</span><span class="token function">addTransaction</span><span class="token punctuation">(</span>moneyTransferTransaction<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> moneyTransferTransaction<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

通过上面的DDD重构后,原来在事务脚本中的逻辑,被分散到Domain Service,Domain Entity和OverdraftPolicy三个满足SOLID的对象中。