当我们谈论DDD时我们在谈论什么

news2024/11/14 22:03:47

谈论到 DDD,我们会聊事件风暴,会聊限界上下文,会聊六边形架构,会聊实体值对象。这些概念各不相同,相关的概念也很不一样,但都属于DDD的范畴。见过了很多DDD的讨论和工作坊,我发现大家唇枪舌剑无法达成一致,往往是因为各自脑中的问题并不相同。

我尝试在软件设计领域,将这些问题划分到几个相互独立的范畴,这可以帮助我和其他人讨论,在明确范围内可以更好的交流。

一种比较经典的方式是划分为战略设计和战术设计。由于领域模型设计复杂度也很高,所以我又把领域模型设计从战术设计中划分出来,形成单独的范畴,以便更好的讨论。

下面我将讨论这三个范畴的概念和方法。

DDD战略设计

在这个范畴里,主要讨论目标是复杂的业务需求。
有多复杂呢?可能需要多个团队分工合作,或者一个团队分阶段开发,需要被设计成多个独立部署运行的服务,会有多个代码库。

这个范畴可以有很多名字,比如DDD战略设计、进程间架构、微服务架构设计等。

在这个范畴里讨论的主要问题是,如何将这个复杂的业务需求合理的分成多个部分,从而分而治之。

为什么要分成多个部分?因为解决复杂问题的一个有效方法是将其分解为多个相对简单的问题,然后分别解决。如果不进行分解,这个复杂问题往往会让我们在解决过程中陷入困境,就算设计出了解决方案,也往往由于解决方案过于复杂导致团队的认知超载。

划分方法

既然战略设计需要将整个业务需求分成多个部分,那么如何找到用于划分的接缝呢?

我看到行业里有这样一些方法:

  • 限界上下文

在《领域驱动设计》中,Eric提出了限界上下文。从领域模型设计的角度,为了让模型保持完整独立和清晰,需要识别出限界上下文,让其作为模型的边界。在书中并没有完善的识别方法,更多的是提出一些概念。限界上下文往往被用来辅助判断接缝的正确性。

在一个限界上下文中,领域知识是相对完整的。

  • 核心域

在《领域驱动设计》中,Eric提出了精炼及核心域。在模型中识别出最有价值的核心域,将其独立出来。

由于只提到了核心域,所以这也不是一个完整的划分的方法。我曾在如何划分限界上下文博客中基于此方法上提出了一种分解问题域的方法。

  • 事件风暴工作坊

事件风暴工作坊可能是最早用来指导划分限界上下文的方法。

对前一步(事件风暴)产生的聚合进行分组,通过业务的内聚性和关联度划分边界,结合限界上下文的定义进行判断,并给出上下文名称。

——[服务化设计阶段路径方案]

但是「业务的内聚性和关联度」着实不是一个好的划分依据。而事件风暴的创始人Alberto曾经提出过通过关键事件识别不同的阶段进而识别限界上下文的方法,看上去是一个更加靠谱的方法。

  • 8X Flow

8X Flow中提出了一套相对完整的划分方法。首先定义「业务」和「领域」,然后将「业务」和「领域」划分开来,接着基于合同将业务划分成了不同的上下文,最终完成了划分。

  • 现代企业架构白皮书

现代企业架构白皮书提出通过职责类型划分。流转类识别不同的业务流程阶段,规格类提取业务规则,视图类专为统计报表而存在,配置类提供配置工具。

重新思考

我也尝试过一些其他的划分方法,比如通过时间阶段划分,通过使用者不同划分,通过使用场景不同划分,通过变化频率不同划分。这些方法和上面的一些方法都有些相似。

不好的划分方法可能会导致分布式单体:每次变化不得不修改多个服务、每次部署必须同时部署多个服务,服务之间有非常多的通信,同一个团队管理着多个服务,服务之间共享数据库、同样的代码和模型。

也许我们可以总结出一些原则,来帮助我们验证划分是否合理。比如高内聚低耦合,比如服务有明确的边界且能自治,可以独立演进,比如尽可能减少对于其他服务的依赖。

DDD战术设计

在战略层面划分好了服务后,我们来看看一个服务内部。

在这个范畴里,主要讨论在一个服务内部,如何划分和组织代码。

和上一节类似,在代码也有不同的职责;和上一节不同,对于代码层面的划分,已经有相对成熟的方法。

这个范畴可以有很多名字,比如DDD战术设计、进程内架构、分层架构等。

需要指出的是,在一个服务内部,如果领域模型足够复杂,在分离领域逻辑和技术实现细节前,也需要先按照模块进行一次划分,然后再按上述的领域逻辑和技术实现细节的方式划分。相关讨论可以参见前缀分包vs后缀分包。

划分方法

  • 《领域驱动设计》中的分层架构

Eric在2003年提出的分层架构。和传统的展示层+业务逻辑层+数据访问层的三层架构相比多了一层,主要区别是将业务逻辑层分成了应用层和领域层。

《领域驱动设计》中的分层架构

图片引自《领域驱动设计》第4章

其中「应用层」这个概念,也指明了它和领域层的区别:领域层专注表达领域概念,而应用层则在领域层之上,加入了诸如持久化概念和事务概念等软件的典型概念,对外提供了满足具体场景的功能。展示层则在应用层功能之上,定义了和外部系统通信的具体形式。

这里也将数据访问层变成了基础设施层。基础设施层为其他层提供支撑其概念的具体技术实现。

  • 六边形架构

2005年六边形架构(翻译)又称端口和适配器架构,从设计模式的视角将代码划分成了负责业务逻辑的「应用」和负责同外部系统交互的「适配器」。

六边形架构

图片引自《六边形架构》

在2013的IDDD中Vaughn将六边形架构和DDD进行了结合,把「应用」又细分成了「应用程序」和「领域模型」。

六边形架构也称为端口与适配器。对于每种外界类型,都有一个适配器与之相对应。 外界通过应用层API与内部进行交互。

图片引自《实现领域驱动设计》第4章

2008年的洋葱架构也是类似的。

六边形架构从另外一个角度审视了一个理想架构,并将领域层放在中心,凸显其核心地位。

  • 整洁架构

Uncle Bob在2012提出了整洁架构,一般来说我们认为整洁架构的四层(四圈)和IDDD的六边形架构基本是对应的,只是整洁架构将适配器划分成了和框架耦合的「Frameworks & Drivers」层和负责内外层数据转换的「Interface Adapters」层。

整洁架构

图片引自《整洁架构》

整洁架构也用「用例」来描述业务实体之外的一层,对应于「应用层」,更明确的指明了这层的职责是实现各个用例。

比较有趣的是,整洁架构把Gateway接口放到了领域层之外的「用例层」。这使得领域层只关注于当前上下文的逻辑,而让用例层负责和其他上下文/资源库的协调和编排。

整洁架构也讨论了如何处理框架和架构的关系。

  • 清晰架构

2017年更有集DDD、洋葱架构、整洁架构、CQRS于一体的清晰架构出现。

重新思考

以上的架构,指导每一个具体的业务功能分解来说是非常够用的。然而在一个真实的项目中,除了每个具体功能的分层,其实还有一些对于平台和框架的配置,这些其实要和每个业务功能的代码有所区分,从代码结构上独立出来。

另外,每一层都会有一些可以复用的代码。比如领域层的基础的业务异常,应用层的事务处理,适配器层的HTTP客户端。这些不只用于单个模块或者单个服务,也可以用于多个服务;有些已经有三方工具,有些需要我们自己定义和封装。

我看到很多项目对于以上两类代码并没有区分,而是把一切不属于其他层的代码都放到了基础设施层。让可怜的基础设施层逐渐变成了垃圾桶。

领域模型设计

在战术层面划分好架构后,我们来看看位于核心的领域模型。

在这个范畴里,主要讨论基于面向对象技术,如何用领域模型来表达业务概念。

为什么要使用领域模型这种模式,而不是用Service+数据模型的模式呢?如果复杂的业务逻辑采用数据模型这种模式,那么Service里会存在大量的复杂的逻辑,代码是很难维护的。而领域模型充分利用了面向对象技术的优势,将复杂度转变为职责明确的组件组合,让各个组件相对简单,来降低认知负载,提升可维护性。这就是设计的力量。

那为什么用面向对象技术呢?面向对象思想更加符合我们认知复杂问题的方式,并且现代编程语言都普遍支持面向对象,所以DDD选择了面向对象技术。

关注点分离模式

在这个范畴里,主要还是使用《领域驱动设计》中的模式。我们以关注点分离的角度,来解析这些模式。

  • 领域对象的生命周期类型

从生命周期的角度,「领域对象」分为这样几个类型:

  1. 和应用生命周期一致,应用启动时被创建出来,应用关闭时才销毁。比如《领域驱动设计》5.4.1中的「资金转账」。
  2. 在业务过程中被创建,会被保留一段时间,不随着应用关闭销毁。比如电商系统中的「订单」。
  3. 在业务过程中被创建,在使用完成后即被销毁。比如一些在对象之间传递的参数对象。

而在《领域驱动设计》的第5章,Eric也将领域对象划分为了实体、值对象、领域服务这三个重要模式。这三个模式和生命周期是如何对应的呢?

对于类型1,和应用生命周期一致,就是领域服务这种模式。对于类型2,在业务过程中被创建,会被保留一段时间,对应于实体和值对象。而对于类型3,在业务过程中被创建随即被销毁,对应于值对象。

VALUE OBJECT 经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。

——《领域驱动设计》 5.3 值对象

  • 分离领域对象的创建、查询、保存和使用

从生命周期角度,对于这三类领域对象的创建逻辑,可以使用Factory模式,将其封装在Factory中。对于类型2的领域对象的保留及之后的查询,可以使用Repository模式,将其模拟成一个集合从而进行存取操作。

Eric把Factory和Repository被归为「支持对象」,以和其他用于表示模型的领域对象分开。

  • 分离函数和命令

使用无副作用的函数模式,把没有副作用的查询逻辑提取出来,成为无副作用的函数,而让有副作用的命令尽可能简单。

基于同样的理由,我也在考虑将有IO操作的逻辑提取出来,直接让应用层调用,而不是和其他业务逻辑组合。

  • 分离领域中的算法

使用Strategy模式,把业务逻辑中的变化点放到策略对象中,让不同的实现可以互换,从而实现关注点分离。

  • 分离领域中的规则

使用Specification模式,将领域中用于判断是非的业务规则放到规格对象中。

  • 分离做什么和怎么做

采用Intention-Revealing Interface和Cohesive Mechanism模式,把「做什么」和「怎么做」分离。让释意接口专注于表明意图,方便调用方使用;让内聚机制封装实现细节,在释意接口背后解决问题。

重新思考

我发现在OO BootCamp中得到的模型往往无法直接用于真实项目中,这让我用新的角度重新学习和思考了领域模型。

在实际项目中,设计者往往过早陷入对于一些具体模式的识别,比如实体、聚合、领域服务,而忽略了如何设计一个可以表达领域概念的模型。我们应该基于领域概念设计领域模型,然后再采用合适的模式降低领域模型的复杂度,进一步增加领域模型的表达能力。

很多项目虽然也使用了以领域为核心的架构,但是设计者仍然是数据模型/贫血模型的思考方式,把大量领域逻辑放置在了万能的Service中,让领域概念隐藏在了冗长的过程代码中,丝毫没有享受到DDD带来的收益。


徐昊在极客时间的《如何落地业务建模》课程中说DDD至少可以指代一种建模法,一种协同工作方式和一种价值观。而我在这里着重讨论了软件设计相关的建模法。

Eric在《领域驱动设计》一书中提出,软件设计应该以领域为中心,而不是技术问题。

软件的核心是其为用户解决领域相关的问题的能力。

——《领域驱动设计》 第一部分

在学习了让我们眼花缭乱的众多方法后,我们重新回到DDD的初衷,重新审视软件设计和DDD之间的关系,让DDD帮助我们提升软件设计能力。


文/Thoughtworks 祁兮
原文链接:DDD的战略设计和战术设计- Thoughtworks洞见

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/32123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【同时完成超分和MEF】

Deep Coupled Feedback Network for Joint Exposure Fusion and Image Super-Resolution (用于联合曝光融合和图像超分辨的深度耦合反馈网络) 如今,人们已经习惯了拍照来记录自己的日常生活,然而,照片实际上与真实的…

SB30100LCT-ASEMI插件肖特基二极管SB30100LCT

编辑-Z SB30100LCT在TO-220AB封装里采用的2个芯片,其尺寸都是94MIL,是一款插件肖特基二极管。SB30100LCT的浪涌电流Ifsm为200A,漏电流(Ir)为12uA,其工作时耐温度范围为-55~150摄氏度。SB30100LCT采用金属硅芯片材质,里…

解决OpenCV在Cmake时,因网络问题无法下载部分所需文件

解决OpenCV在Cmake时,因网络问题无法下载部分所需文件 在安装CUDA-Opecv进行Cmake的过程中,因为网络问题很多文件都无法下载。可以在你的opencv/.cache下可以看到,很多文件都是0kb的。这样肯定是不行的,我们要保证每个文件都要下…

深入 category 数据类型

目录 前言 1 作用 2 用法 2.1通过 pd.Categorical 创建 category 类型数据,同时指定可选项 2.2 通过 dtype 参数创建 category 类型数据 2.3 此时对数据进行排序 2.4 通过 CategoricalDtype 指定 category 数据的类型顺序 2.5 想要临时修改排序规则&…

低代码开发是未来软件开发的主流模式

低代码平台起始于20世纪80年代4GL“第四代编程语言”。2014年,Forrester research 提出低代码平台的概念,中国低代码市场进入发展期;2018 年,西门子收购低代码企业 Mendix 、美国低代码独角兽企业 Outsystem 获得 1.5 亿美元的融资…

Flutter高仿微信-第22篇-支付-二维码收款(二维码)

Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图: 实现代码: /*** Author : wangning* Email : maoning20080809163.…

安装Jenkins并在ruby中访问

1. 安装Jenkins 最近不知道为啥,根据官网Linux安装Jenkins的时候下不来安装包,提示连接超时。尝试多次无果后决定在window上安装 curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee /usr/share/keyrings/jenkins-keyring.as…

105-120-Hadoop-MapReduce-outputformat:

105-Hadoop-MapReduce-outputformat: OutputFormat 数据输出,OutputFormat接口实现类 OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat 接口。下面我们介绍几种常见的OutputFormat实现类。 1.O…

Fiddler利用Edxposed框架+TrustMeAlready来突破SSL pinning抓取手机APP数据

一、背景 在使用fiddler做代理抓取应用数据包时,如果要抓取到HTTPS数据,需要将fiddler证书导入到浏览器或手机。浏览器或手机设置好fiddler的代理地址,即可抓取到https数据包。 如果APP应用采用证书锁定后,将无法抓取到https数据…

[附源码]java毕业设计医院档案管理系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

RabbitMQ 简介

RabbitMQ 简介 首先我们先看一下常见的MQ产品 在上图我们可以知道RabbitMQ和ActiveMQ都支持AMQP协议,那么什么时AMQP呢? AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议&a…

Android启动以及 app 启动流程 笔记

Android 启动流程 init 进程是 用户控件鼻祖 zygote 进程是 java进程鼻祖 zygote进程的操作 1.native 层 做的操作 1.初始化java运行环境 虚拟机 (内存管理) 2.注册jni 3.运行 zygoteinit.main方法 进入java 2.java …

Serverless Devs 社区联合信通院邀请您参加 2022 中国 Serverless 用户调查

作者:云原生产业联盟 在创新发展需求的不断驱动下,用户关注点逐步上移,敏捷成为破局高频竞争的利器。以应用为中心、屏蔽底层复杂逻辑,灵活扩展,按需取用的服务器无感知(Serverless)技术符合云…

【GUI视频教程】GUI综合实战视频教程第3期:GUIX Studio一条龙设计主界面,底栏和窗口切换控制(2022-11-21)

视频教程汇总帖:【学以致用,授人以渔】2022视频教程汇总,DSP第10期,ThreadX第5期,BSP驱动第24期,USB实战第4期,GUI实战第3期(2022-11-21) - STM32F429 - 硬汉嵌入式论坛 …

Dubbo 服务注册与启动源码解析

Dubbo 版本&#xff1a;3.1.2 自定义标签解析 在 DubboNamespaceHandler 中会添加 DubboDeployApplicationListener&#xff0c;其继承了 ApplicationListener<ApplicationContextEvent> 关于ApplicationListener&#xff0c;可参考&#xff1a;Spring 事件基本使用 服…

YoloV5+TensorRT封装|C#调用dll实现V5+TRT目标检测

在目标检测得领域中&#xff0c;yolo系列无疑是最强得目标检测框架&#xff0c;而其中得yolov5更是扛把子得存在&#xff0c;虽然有着众多的yolo系列版本&#xff0c;但是在工业领域中yolov5还是用的最多&#xff0c;yolov5 yyds&#xff0c;&#xff0c;&#xff0c;先奉献上我…

上海亚商投顾:沪指高开低走 钠离子电池、储能概念崛起

上海亚商投顾前言&#xff1a;无惧大盘大跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 三大指数今日高开低走&#xff0c;收盘均小幅下跌&#xff0c;以中字头为首的权重股走弱&#xff0c;上证50跌超0.…

SpringBoot中如果字段为空就不返回给前端

SpringBoot中如果字段为空就不返回给前端前言测试参数的执行结果JsonInclude.Include.ALWAYSJsonInclude.Include.NON_NULLJsonInclude.Include.NON_ABSENTJsonInclude.Include.NON_EMPTYJsonInclude.Include.NON_DEFAULTJsonInclude.Include.USE_DEFAULTSJsonInclude.Include.…

2.(vue3.x+vite)使用vue-router

前端技术社区总目录(订阅之前请先查看该博客) 前端技术社区:vue3.x+vite,node篇,前端小技术,前端资料篇等相关内容的介绍 1:安装vue-router npm i vue-router 2:创建router文件 在src的目录下创建router文件夹与index.js文件 index.js文件内容如下: import {cr…

C#学习以及感受

我本来是写Java的,但是这边的代码用的是.NetCore框架 就自学了 但是学下来,发现其实这两者并没有什么本质区别 Java导包用import而c#用using Java与c#都是写一个静态的main方法来启动,c#的Main必须大写 Java和c#的数据类型基本一样,c#中的string的s可以小写,c#的布尔类型是bool…