一、领域驱动设计-战略篇

1、写在前面

**领域驱动设计(Domain-Driven Design,DDD)**是一个有关软件开发设计的方法论,它提出了从业务设计到代码实现一致性的要求,不再对分析模型和实现模型进行区分。简言之,从代码结构我们就可以直接理解业务的设计,命名得当的话,非程序人员也可以“读”代码。

2003 年的时候,Eric Evans 发表了一篇著作**《Domain-driven Design: Tackling Complexity in the Heart of Software》**,正式定义了领域的概念,开始了 DDD 的时代,但 DDD 的发展确是一直不温不火,这个时候 spring 的贫血模型,则是风靡全球。

2013 年,Vaughn Vernon 写了一本**《Implementing Domain-Driven Design(实现领域驱动设计)》**进一步定义了 DDD 的领域方向,并且给出了很多落地指导,它让人们离 DDD 又进了一步。

而后,随着业务复杂性的提高,人们发现单体应用带来的迭代难,重构难,维护难等问题,越来越难以解决,于是在分而治之的思想下,诞生了微服务,一改以往单体应用,而拆分为多个子应用,一下子让人眼前一亮,于是我们没日没夜地拆分服务,加之微服务提供的注册中心、熔断、限流等解决方案,我们用得不亦乐乎。

但在人们在踩过诸多拆分服务的坑(拆分过细导致服务爆炸、拆分不合理导致频繁重构等)后,人们开始思考,到底有没有一种方法论可以指导人们更加合理地拆分服务呢?众里寻他千百度,DDD 却在灯火阑珊处,有了 DDD 的指导,加之微服务,再应对复杂业务场景上,才算是有了一个相对完美的解决方案。

一句话总结:领域驱动设计(DDD)既是一种开发思想体系,也是一种软件开发的方法论,它旨在管理为复杂问题域编写的软件的创建和维护工作。DDD 是模式、原则和实践的集合,它可以被应用到软件设计,以管理复杂性。

2、为什么使用 DDD

  • a、DDD 要求领域专家和开发者一同工作,这样开发出来的软件能够准确传达业务规则。
  • b、准确传达业务规则 的意思是说,软件就像是领域专家是编码人员时所开发出来的一样。
  • c、可以帮助团队人员的自我提高,没有任何一个领域专家或管理者敢说自己对业务已经了如指掌了,业务知识需要长期的学习。在 DDD 中,每个人既是学习者,同时也是知识的贡献者
  • d、在领域专家、开发者和软件本身之间不存在“翻译”,也就是当大家都是用相同的语言进行交流时,每个人都能快速听懂他人所说。
  • e、DDD 中,设计就是代码,代码也是设计,两者是统一的,设计是关于代码如何工作的,最好的编码设计来源于多次试验,这得益于敏捷的发现过程。
  • f、DDD 同时提供了战略设计和战术设计两种方式,战略设计帮助我们理解哪些设计是最重要的;哪些既有的软件资产,可以直接拿来使用的;那些人员应该被加入到团队中;战术设计则是帮助我们创建 DDD 模型中的各个部件。
  • g、使用 DDD 的业务价值
    • 1)、你获得了一个非常有用的领域模型
    • 2)、你的业务得到了更准确的定义和理解
    • 3)、领域专家可以为软件设计做出贡献
    • 4)、更好的用户体验
    • 5)、清晰的模型边界
    • 6)、更好的企业架构
    • 7)、敏捷、迭代式和持续建模
    • 8)、使用战略和战术新工具

实施DDD所面临的挑战

  • a、为创建通用语言腾出时间和精力
  • b、持续地将领域专家引入项目
  • c、改变开发者对领域的思考方式(需要加入从业务角度看问题的思考方式)

3、DDD 设计和传统设计比较

1)、Spring 传统三层架构

其中 controller + jsp/thymeleaf 等可以认为是用户展示层,相信大部分同学都是从 SSM 框架入门的,大一些的项目可能会是前后端分离,即后端仅剩 controller 用于控制和前端的页面及数据交互.

如上图所示, spring 所提倡的简单的 java beans (即 pojo) ,相比以前的 ejb 大大简化了开发难度。

2)、DDD下的分层架构

变化

  • 1、controller 层更加纯粹
    • 原三层架构, controller 可能会聚合多个 service 层的调用
    • 新的分层架构,纯粹作为与前端或外部系统交互的数据转换层,不再包含业务逻辑.
  • 2、service 层改为 Appliaction Services,当然不是说改个名字就是变化,主要体现在对service进行分级,打薄,
    • Appliaction Services作为更高级别的抽象
    • 新增领域层,抽离原service层的部分逻辑到领域层的领域服务和领域对象中.
  • 3、领域层分为聚合和领域服务.其中聚合包括实体+值对象,实体和值对象不像 spring 提倡的贫血模型(仅包含get set方法的简单对象),是包含了业务逻辑的丰富对象(即DDD所提倡的充血模型对象),传统三层架构更像是面向过程编程,业务全部集中在service层,或许大家都在service层写过大量的数据校验代码,整个业务方法冗长,即使做了方法的提取封装,但整体仍然会有凌乱的感觉,但DDD则更符合面向对象的设计,按照DDD的架构分层,各层级之间的分级更加明确,service层的含义表述则会更加清晰简洁,而聚合和领域服务的粒度更小,更加的内聚.
  • 当然 DDD 与传统设计的远不止以上的区别,上面的视角仅仅是从架构设计的角度

4、战略设计阶段:DDD 中的重要的一些概念

1)、战略设计

主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。它是从高层业务的角度,来审视我们的软件系统.

如:战略设计角度来看,一套基础的电商业务应该包含如下领域,支付域、交易域、商品域、库存域、履约域。不同领域之间通过限界上下文来划分边界。

2)、战术设计

主要关注的是技术层面的实施,从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现

3)、领域 & 子领域

DDD 的领域就是这个边界内要解决的业务问题域。既然领域是用来限定业务边界和范围的,那么就会有大小之分,领域越大,业务范围就越大,反之则相反。领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

4)、核心域 & 通用域 & 支撑域

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域(每种子域一般包括多个限界上下文)。

  • 核心域:决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力。从战略角度讲,企业应该给予核心域最高的优先级、最资深的领域庄家和最优秀的开发团队。实施 DDD 的过程中,我们将主要关注核心域。
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
  • 支撑域:有时我们会创建或购买某个限界上下文来支撑我们的业务,它对应业务某些重要的方面,但却不是核心,这样的子域便是支撑域。

5)、通用语言 & 限界上下文

在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。这两者相辅相成,通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。

通用语言

团队交流达成共识后,可以简单清晰的描述业务规则和业务含义的语言,就是通用语言。它主要解决各岗位的沟通障碍问题,促进不同岗位的和合作,确保业务需求的正确表达。通用语言贯穿于整个设计过程,基于通用语言可以开发出可读性更好的代码,能准确的把业务需求转化为代码。

限界上下文

限界上下文主要用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。

6)、上下文映射图

上下文映射图就通过画图的方式展示N(N>=2)个上下文之间的映射关系。其中 U(UpStream)代表上游,D(DownStream)代表下游。上下文之间存在多种组织和集成模式,分为合作关系、共享内核、客户方-供应方开发、遵奉者、防腐层、开放主机服务、发布语言、另谋他路、大泥球

7)、领域专家

领域专家并不是一个职位,他表示的是任何一个精通业务的人,相比于软件设计者和开发者,他可能了解更多有关业务领域的背景和知识,他可能是项目经理可能是产品经理可能是业务经理甚至有可能是一个保安大叔。

5、一些常用的架构

1)、架构演变的原则

架构设计的最高原则,即建立一个高内聚松耦合的软件系统架构

2)、整洁架构

最内层的是领域模型,封装了业务规则,不依赖任何其他组件,向外都是接口层。领域模型就是业务逻辑的模型,它应该是完全纯粹的,无论你选择什么框架,什么数据

库,或者什么通信技术,按照整洁架构的思想,都不应该去污染领域模型。

3)、六边形架构(端口与适配器)

除了最中心的领域模型,数据库等基础设施可看作是南向网关, controller(REST Services) 是北向网关.内层领域模型通过接口和外部交互,业务汇聚在领域模型中.输入–>领域模型—>输出.

基于六边形架构的通用性,可用它来支撑系统中的其他架构,如面向服务 SOA 架构,REST、事件驱动架构或者命令和查询职责分离 CQRS 架构。

4)、分层架构

该图中,Aggregate为聚合,包括多个实体和值对象,实体是采用充血模型建立的对象,包含数据和行为.(贫血模型和充血模型至今仍有争论,尚无结果,故可有选择的使用充血模型)

个人认为的的最佳实践:

图中领域层,断开聚合(Agregate)与资源库(Responsitoryes)的依赖,聚合中仅放置自给自足的方法,对外部完全无依赖,与资源库相关的交互放到领域层的领域服务(Services)中.

5)、面向服务架构(SOA:Service-Oriented Architecture)

SOA 对于不同的人来说具有不同的意思,所以这里只给出一些设计 SOA 的原则

  • 原则一:服务契约,通过接口文档、服务阑述自身的目的与功能
  • 原则二:松耦合,服务将依赖关系最小化
  • 原则三:服务抽象,服务只发布接口,面向客户端隐藏内部实现
  • 原则四:服务重用性,一种服务可以被其他服务所重用
  • 原则五:服务自治性,服务自行控制环境与资源,以保持独立性,这有助于保持服务的一致性和可靠性
  • 原则六:服务无状态性,服务负责消费方的状态管理,还不能与服务的自治性发生冲突
  • 原则七:服务可发现性,客户端可以通过服务元数据,来查找和理解服务
  • 原则八:服务可组合性,一种服务可以由其他服务组合而成,而不管其他服务的大小跟复杂度如何

6)、命令与职责查询分离(CQRS:Command Query Responsibility Segregation

CQRS本质上是一种读写分离设计思想,它具有以下特点

1、如果一个方法修改了对象的状态,该方法就是一个命令(Command),他不应该返回数据。在Java中,这样的方法应该声明为void

2、如果一个方法返回了数据,这个方法便是一个查询(Query),它不应该通过直接或间接的手段修改对象的状态,在Java中,这样的方法应该以其返回的数据进行声明。

查询

查询操作并不会修改数据库中的内容,所以查询本身是一种幂等操作,以同一个查询条件在系统不改变的情况下反复执行会返回相同的结果,我们可以针对这种特性提供数据缓存来提高系统性能;同时因为不影响数据库,查询逻辑是不会产生数据一致性问题。查询往往会存在较高的使用频率。

命令

命令操作会直接修改数据库,并针对多个领域模型的情况下我们需要增加来保证操作的原子性。而对于一个命令操作,我们往往是不直接依赖命令的返回值的,所以通常可以异步执行命令操作。对于一般系统来说,往往命令操作的使用频次会较低。

针对查询与命令的需求特点才有了CQRS的设计,具体设计上有以下几种分类

a、CQ两端数据库共享,只在上层代码分离

CQ两端在上层代码进行分离个字单独维护,例如命令型的都用xxxManagerController、xxxManagerService来定义,而查询则直接用xxxController、xxxService定义。

b、关注性能

在上述a的基础上,由于更加关注性能,可在数据库层面采用硬件分离的读写分离模型

c、Command采用事件溯源 (EventSourcing)

d、挑战与建议

挑战

  • 复杂性设计:尽管CQRS基础理念较为容易理解,但是这种模式会导致系统的构建复杂度上升,尤其是进一步使用事件溯源模式时。
  • 消息队列处理:在进行高性能设计的时候,通常会使用消息处理命令和发布更新事件。在此情况下,应用程序必须处理消息失败或重复的消息。
  • 最终一致性:如果分离读取和写入数据库,读取数据可能会过时。 必须更新读取模型存储,以反映对写入模型存储区所做的更改,并且在用户根据过时的读取数据发出请求时,可能很难检测到这种情况。

建议

对于以下场景不建议引入CQRS:

  • 领域或者业务十分简单。
  • 基本的CRUD就可以支撑完整的系统数据访问需求。

5)、三种架构(分层、六边形、整洁)的对应图

关于事务的理解见下图:

代码结构:

6、参考资料

  • 《实现领域驱动设计》
  • 《领域驱动设计模式、原理与实践》
  • 领域驱动设计-理论心得篇:https://zhuanlan.zhihu.com/p/350033901
  • DDD领域驱动设计总结:https://zhuanlan.zhihu.com/p/351162895
  • DDD 领域概念字典:https://zhuanlan.zhihu.com/p/344747111