【DDD】学习笔记-领域模型驱动设计

news2024/9/24 5:24:32

领域模型驱动设计自然是以提炼和转换业务需求中的领域知识为设计的起点。在提炼领域知识时,没有数据库的概念,亦没有服务的概念,一切围绕着业务需求而来。尤其是领域建模的分析阶段,应该只关注问题域,模型表达的是业务领域的概念,而非实现的概念。领域分析模型应由领域专家作为主导,甚至由领域专家创建,完全独立于软件开发技术。Martin Fowler 在《分析模式》中就提到:“这种独立性可以使技术不会妨碍对问题的理解,并使得最终的模型能够适用于所有类型的软件技术。”

在分析之初,不考虑任何技术实现手段,一切围绕着领域知识进行建模,是领域模型驱动设计的关键。

领域分析模型与抽象

领域分析模型必须遵循统一语言,由领域概念及它们之间的关系构成。从与现实世界的映射来看,领域概念可以分为显式和隐式两种类型。显式概念是在现实世界中被明确无误地表达出来的,例如电商领域中的商品、顾客、购物车、订单等概念。隐式概念往往隐藏在领域逻辑中,不被明确地表达,但这并不意味着不重要,例如电商领域中促销模型的促销产品(Promotion Product),就是通过对促销领域的深度分析挖掘出来的领域概念。隐式概念的获得往往意味着对领域理解的一次突破。

领域分析模型中的每个领域概念其实都是对现实世界中业务概念的一次抽象。抽象具有不同的层次,这取决于你对业务概念粒度和特征的理解。不同的抽象层次传递了不同的知识。抽象层次越高,需要关注的概念就越少,从而让分析模型变得更简单。然而,高度的抽象亦可能遮掩住一些存在差异的业务事实,使得模型丢失一些重要而具体的领域知识。例如,我在《领域驱动战略设计实践》中建立的项目管理领域模型,就是通过对瀑布、RUP、XP 和 Scrum 这四种不同的软件开发过程进行抽象获得的:

48236024.png

模型中的一个抽象概念可以代表多个领域概念,从而使得整个领域模型化繁为简,并保持了更好的可扩展性。上述模型中的 Iteration 既代表了 XP 的一次迭代,也代表了 Scrum 中的一次冲刺(Sprint)。倘若未来还有别的软件开发过程提供了其他代表迭代的概念,则 Iteration 的抽象概念仍然足以涵盖新的知识,保证了模型的可扩展。同时,这个抽象概念并不能直观地体现 Scrum 冲刺的含义与特征,丢失了之所以命名为冲刺的关键语义。因此,在针对领域进行分析建模时,需要把握好抽象的分寸,既要传递准确的领域知识,又不至于让整个分析模型变得过于庞大,以至于阻碍领域专家和开发团队之间的交流。

在对领域概念进行抽象时,需要结合具体的业务场景进行分析。不同的业务场景会带来观察领域概念视角的差别,这也是领域驱动设计之所以要引入限界上下文的原因之一。

例如,在银行系统中,管理客户的业务场景包含了个人(Individual)和组织(Organization)两个抽象概念。为了降低管理客户的复杂度,可以在这两个概念之上建立更高的一层抽象概念:客户(Customer)。当客户购买了一种金融产品(Product)后,为了更好地管理客户,需要建立客户与产品之间的关系。

此时,引入的客户抽象概念就可以抹去个人客户与组织客户之间的差异,使得我们无需分别维护个人、组织与产品三者之间的关系。同样是客户购买了金融产品,在交易业务场景中,由于个人业务与对公业务的差异较大,具有完全不同的业务流程和业务规则,如果仍然使用客户抽象来建立分析模型,就会因为过度抽象带来不必要的间接层,为设计模型带来错误的指导,例如创建了不合理的继承体系,并在实现的代码中引入频繁的强制类型转换。这里所谓的“场景”,可以理解为限界上下文,它维护了领域模型的边界:

76854993.png

既然建立领域分析模型与抽象息息相关,那么明确抽象的意义和它要求的能力,可以指导设计师如何进行分析建模。抽象是我们观察客观世界的一种方法,从大量的具体事物中抽取和概括它们共同的方面、本质属性与关系。在运用抽象思维观察客观世界时,可以通过分类的方式辨别客观事物。这种方式需要抓住一类事物迥异于其他事物的核心特征,并将其定义为该事物的内在本质,从而形成一组概念的类别集合。

例如,生物学家将有生命的个体定义为“生物”,这类事物的内在本质是生物能够新陈代谢及遗传。整个世界由生物和非生物构成,这就是生物学家通过抽象分析获得的“世界观”。生物学家还为生物定义了多个抽象层次,依次为域、界、门、纲、目、科、属和种。显然,抽象的层次越低,属于同一类别的生物共同特征就越多。这种分类法需要对客观世界具有由现象到本质的归纳能力

观察客观世界的另一种抽象方法是提取共同特征。这种抽象方法不需要去探究内在的原理与本质,仅仅通过观察事物表面的特征进行抽象。通过寻找一些共同的特征,我们甚至可以将一些风马牛不相及的事物抽象到同一个概念下。例如,麻雀、飞机、蝙蝠和竹蜻蜓完全属于不同的类别,但它们却具有一个完全相同的特征——可以飞行。这种共同特征提取法强调从可变性中找到共性的概况能力

考察一个建模分析人员的水平,其实就取决于归纳与概括这两种抽象能力。在分析建模过程中,我们运用抽象方式的不同,得到的分析模型也将有所不同。例如,分析新闻领域,我们寻找到文章(Article)、视频(Vedio)和音频(Audio)等相对具体的概念。如果从新闻页面的角度观察这些概念,可以发现这些概念都具备共同特征:为页面提供内容(Content)。由此得到的抽象模型为:

img

如果要对这些概念进行分类,就能辨别出文章与视频、音频并不属于同一个分类维度。我们可以将新闻网站发布的内容皆认为是文章,文章的内容却可以由文本、音频与视频混合组成。这时,文本、音频与视频其实都属于媒体(Media)分类:

img

在抽象模型时,还要注意控制抽象的范畴,否则会导致创建太多的抽象,形成错误的继承体系。当我们创建父类与子类的继承体系时,可能会过多考虑子类对父类的重用,却忽略了继承其实是一种“差异化设计(Design by Difference)”的体现。在对领域概念进行抽象时,我们应该仅针对存在差异的部分进行所谓的“泛化”,并由不同的子类去实现各自差异的部分。不要扩大局部差异,导致对整体概念进行错误的抽象。

例如,软件公司的员工分为需求分析人员、架构师、开发人员和测试人员。在建模时,我们要注意这里的员工分类其实扩大了差异。虽然需求分析人员、架构师、开发人员和测试人员都是(is)员工,但他(她)们之间的差异并非员工的差异,而是角色的差异:

img

这种抽象机制其实体现了继承和组合的区别。它提示我们在分析建模时不要因为概念关系上存在“是(is)”的关系时,就主观地做出抽象的判断,而需要深挖这些概念之间的不变性与可变性,然后从变化的部分寻找到抽象的特征。这种抽象甚至不仅仅包括对概念的抽象,也可以是对行为的抽象。对行为的抽象往往可以提高领域模型的设计质量,因此我将这一抽象放到了领域设计建模过程中。

领域设计模型与设计要素

从领域分析模型到领域设计模型,是对代表领域知识的模型概念的进一步甄别与完善。Eric Evans 在领域驱动设计中提出的设计要素在领域设计模型中扮演了非常重要的角色。这些设计要素既是对模型的约束,也是对设计的约束,可以认为是领域驱动设计中的设计模式。下图是 Eric Evans 描绘的战术设计要素:

36866269.png

领域驱动设计提出的这些设计要素在设计模型中扮演了非常重要的角色。首先,我们可以进一步将分析模型中的领域概念定义为实体(Entity)或值对象(Value Object)。二者的区分有助于管理领域对象的生命周期,通过引入不变的值对象还可以减少并发的成本。确定聚合(Aggregate)的边界,并明确它包含哪些实体与值对象,使得领域模型可以遵守业务规则中的不变量(Invariable)约束和一致性约束。领域事件(Domain Event)的识别,可以帮助我们确认业务流程中那些已经发生的事实(Fact),并围绕着领域事件确定事件的发布者与订阅者,从而让这些概念能够流动起来。通过资源库(Repository)与工厂(Factory)模式的运用,有利于管理领域对象的生命周期,并通过对资源库的抽象保证领域逻辑不受数据库持久化机制的影响。

实体、值对象、领域服务与领域事件是从设计角度对领域设计对象做出的分类,另一种分类方式则从履行职责的角度,探讨了领域对象在业务场景中协作时各自扮演的角色。这一分类来自 Rebecca Wirfs-Brock 的著作《对象设计:角色、职责和协作》。在书中,她总结了对象角色的构造性(Role Stereotypes):

“在一个应用系统中,各种角色都具有自身的特征,这些特征就是构造型(Stereotypes)。……从高层概念进行思考,忽略具体行为来识别对象的构造型,是非常有必要的。通过简化和特征化描述,我们能够轻易地辨明对象的角色。”

下图列出了主要的角色构造型:

35918189.png

角色构造型可以用来集中描述对象的职责,以下是 Rebecca 对这些构造型职责的简单描述:

  • 信息持有者:掌握并提供信息
  • 服务提供者:执行工作,通常为其他对象提供服务
  • 构造者:维护对象之间的关系,以及与这些关系相关的信息
  • 协调者:通过向其他对象委托任务来响应事件
  • 控制器:进行决策并指导其他对象的行为

这两种分类并不矛盾,我们完全可以将领域驱动设计的设计要素归纳到角色构造型中。由于角色体现了职责的履行,就可以结合职责驱动设计来理解它们,并指导领域驱动的设计建模。

例如,我们可以将实体与值对象视为“信息持有者”角色。遵循该角色的设定,就可以优先考虑将与信息相关的行为分配给这些信息的持有者。这实际上遵循了 Larman 提出的“信息专家模式”,将数据和行为封装在一起,避免了贫血模型的出现。领域服务扮演了服务提供者的角色,它能为领域对象提供业务支持,实现单个信息持有者无法完成的功能。因此,领域服务往往需要服务提供者与信息持有者以“各司其职”的方式完成对象之间的协作。

若要提供完整的业务价值,则应由扮演协调者角色的应用服务来承担,它对外公开的接口恰好对应一个具有业务价值的主用例(Use Case),对内却仅仅做好各个领域对象之间的协调,而将业务逻辑都委派给各自的领域对象。领域驱动设计要素中的工厂属于构造者角色,负责创建复杂的领域对象,尤其是聚合根实体。一些领域服务还扮演了控制器角色,通过它决策并指导其他对象的行为。在上述角色构造型中,还缺少了改进设计质量的两个构造型,即扮演网关(适配器)角色的资源库或客户端以及面对外部调用者的远程服务。因此,结合领域驱动设计与职责驱动设计,我们创建了一个整合后的角色构造型:

54462066.png

上图不仅表达了各个角色构造型,还给出了它们各自履行的职责,以及可能的协作方式。以报税功能为例,系统需要定期根据用户提交的收入信息生成税务报告文件。首先,需要获得符合条件的税务报告,然后将其转换为 HTML 格式的数据流,最后以 HTML 格式的呈现方式生成 PDF 文件。对外而言,生成税务报告文件是一个完整的服务,客户端的调用者无需了解该服务的实现细节。这一职责可以分别由 TaxReportResource 远程服务与 TaxReportAppService 应用服务承担,前者响应远程客户端的请求,后者提供具有业务价值的行为。根据领域驱动设计对应用服务的定义,TaxReportAppService 应用服务并不真正实现具体的业务逻辑,而是负责将调用请求委派给 TaxReportGenerator 领域服务。

领域服务的内部实现需要多个对象共同协作。首先通过 TaxReportRepository 获得 TaxReport 实体对象,该实体对象作为聚合根是报告信息的持有者,封装了税务报告的数据验证行为和组装行为。HtmlReportProvider 服务负责将报告对象转换为 HTML 格式的数据流,由 PdfReportWriter 服务将该数据流写入 PDF 文件,生成税务报告文件。整个协作时序如下图所示:

77257885.png

在建立领域设计模型时,同样需要重视抽象的重要意义,尤其是对行为的抽象。通过对行为的抽象,可以演化出诸多细粒度的具有纯函数意义的领域服务。例如,假设生成的税务报告不仅仅要求生成 PDF 文件,还允许生成可编辑的 Word 文档。为了满足设计模型的可扩展性,就有必要对写入税务报告的行为做进一步抽象,例如,定义 ReportWriter 接口,以此来隔离与封装变化。这实际上是面向对象设计原则与设计模式的运用。

例如,组装 TaxReport 的行为其实是对 Questionaire 树形数据结构的转换,该树形结构如下所示:

Section ->
        SubSection ->
                  QuestionGroup->
                          Question->
                              PrimitiveQuestionField

Section 是树形结构的根节点,PrimitiveQuestionField 是叶节点,其余类型皆为枝节点。每个枝节点(包括根节点)都可以添加下一层的子节点。所有节点都提供了转换功能,叶节点只需要转换自身的内容,而枝节点在转换了自身的内容之后,还会调用所有子节点的转换功能,若子节点也是枝节点,则继续递归调用。

由于这种转换功能的目的是将问卷调查中对应节点的值按照规定格式进行转换,并在最终生成的税务报告中呈现,因此在设计时可以借用控件呈现的隐喻,定义转换功能为 render() 方法。每个枝节点和叶节点都定义了 render() 方法,二者的差别在于枝节点是一个容器,可以添加子节点。这时,就可以运用 GoF 设计模式中的合成模式(Composite Pattern):

86050962.png

GOF 的《设计模式》认为,合成模式的意图为“将对象合成为树形结构以表示‘部分—整体’的层次结构。合成模式使得用户对单个对象和合成对象的使用具有一致性。”Element 接口保证了枝节点和叶节点 render() 行为的统一,通过这一抽象,使得调用者无需考虑这两种节点的差别,简化了调用。

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

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

相关文章

自用metadata.pegasus.txt bat生成脚本

下面代码保存为一个bat文件,放在存放rom的文件夹下运行 echo off setlocal enabledelayedexpansion :menu cls echo Please select a game ROM type: echo 1. FC echo 2. PSP echo. set /p choice"Enter your choice (1 or 2): " if "%choice%…

林浩然与杨凌芸的Java奇遇记:内部类的四重奏

林浩然与杨凌芸的Java奇遇记:内部类的四重奏 Lin Haoran and Yang Lingyun’s Java Adventure: A Quartet of Inner Classes 在一个充满代码香气的午后,程序员界的才子林浩然与机智灵动的女程序员杨凌芸,在Java的世界里展开了一场别开生面的“…

FPGA_vga显示

一 VGA 1.1 VGA VGA是视频图像阵列,是一种使用模拟信号进行视频传输的标准协议。 1.2 VGA接引脚定义 VGA分公母两种,RGB显示标准。 1.3 VGA显示器 VGA显示器采用图像扫描的方式进行图像显示,将构成图像的像素点,在行同步信号…

2024年阿里云服务器活动价格表

2024年2月阿里云服务器租用价格表更新,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、ECS u1实例2核4G、5M固定带宽、80G ESSD Entry盘优惠价格199元一年,轻量应用服务器2核2G3M带宽轻量服务器一年61元、2核4G4M带宽轻量服务器一年165元12个月、2核…

Python 深入理解 os 和 sys 模块

Python 深入理解 os 和 sys 模块 OS 介绍代码智能连接(拼接)路径创建目录展示(列出目录)删除文件重命名文件或目录 sys 介绍代码命令行参数处理 (sys.argv)标准输入输出重定向 (sys.stdin, sys.stdout, sys.stderr):解…

备战蓝桥杯---搜索(进阶3)

看一道比较难又有趣的题: 下面是分析: 我们不妨把属性值看成点,一个装备可以看成一条边(只能选一个端点)不存在有装备属性值的当成一个点,于是我们便形成了树或图,如果是树的话,有一…

请立即收藏!春节必备AI工具盘点:一键拜年短信、海报!AI全家福!

大家好,我是木易,一个持续关注AI领域的互联网技术产品经理,国内Top2本科,美国Top10 CS研究生,MBA。我坚信AI是普通人变强的“外挂”,所以创建了“AI信息Gap”这个公众号,专注于分享AI全维度知识…

Maven之安装自定义jar到本地Maven仓库中

Maven之安装自定义jar到本地Maven仓库中 文章目录 Maven之安装自定义jar到本地Maven仓库中1. 命令行窗口安装方式1. 常用参数说明2. 安装实例 2. IDEA中安装方式3. 使用 1. 命令行窗口安装方式 安装指定文件到本地仓库命令:mvn install:install-file; 在windows的cm…

iOS 需求 多语言(国际化)App开发 源码

一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络&#xff0…

正点原子--STM32基本定时器学习笔记(2)

目录 1. 相关寄存器介绍 1.1 控制寄存器 1(TIMx_CR1)​编辑 1.2 DMA/中断使能寄存器(TIMx_DIER) 1.3 状态寄存器(TIMx_SR) 1.4 计数器(TIMx_CNT) 1.5 预分频器(TIMx_PSC) 1.6 自动重装载寄存器(TIMx_ARR) 2. 工程建立 3. 导入tim.c文件 4. 相关HAL库函数介绍 4.1 H…

购物车商品数量为0判断是否删除

当编辑商品的数量为1,再减的话,我们搞个模态提示,让用户决定是否要删除这个商品? //商品数量的编辑功能handleItemNumEdit(e){const {operation,id}e.currentTarget.dataset;console.log(operation,id);let {cart}this.data;let …

13. UE5 RPG限制Attribute的值的范围以及生成结构体

前面几章,我们实现了通过GameplayEffect对Attribute值的修改,比如血量和蓝量,我们都是有一个最大血量和最大蓝量去限制它的最大值,而且血量和蓝量最小值不会小于零。之前我们是没有实现相关限制的,接下来,我…

3.1-媒资管理之需求分析+搭建Nacos

文章目录 媒资管理模块1 模块需求分析1.1 模块介绍1.2 业务流程1.2.1 上传图片1.2.2 上传视频1.2.3 处理视频1.2.4 审核媒资 2.2 搭建Nacos2.2.1 服务发现中心2.2.2 配置中心2.2.2.1 配置三要素2.2.2.3配置content-api 2.2.3 公用配置2.2.4 配置优先级2.2.5 导入配置文件2.2.6 …

三、设计模式相关理论总结

一、面向对象编程 1.1 概述 简称Object Oriented Program(OOP),指以类或对象作为基础组织单元,遵循封装、继承、多态以及抽象等特性,进行编程。其中面向对象不一定遵循封装、继承、封装和多态等特性,只是前人总结的套路规范&…

前端JavaScript篇之对闭包的理解

目录 对闭包的理解用途循环中使用闭包解决 var 定义函数的问题 对闭包的理解 闭包是指一个函数能够访问并操作其词法作用域(定义时所在的作用域)之外的变量的能力。它可以通过在一个函数内部创建另一个函数来实现。内部函数可以访问外部函数的局部变量、…

Python算法题集_两数相加

Python算法题集_两数相加 题2:两数相加1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【直接相加】2) 改进版一【对齐链表】3) 改进版二【数组求和】 4. 最优算法 本文为Python算法题集之一的代码示例 题2:两数相加 1.…

相机图像质量研究(10)常见问题总结:光学结构对成像的影响--光圈

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结:光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结:光学结构对成…

Quicker读取浏览器的书签(包括firefox火狐)

从edge换了火狐,但是quicker不能读取本地的bookmarks文件了,就研究了一下。 方法1:读取本地Bookmarks文件(仅谷歌内核浏览器) 谷歌内核的浏览器本地会有Bookmarks文件,放了所有的书签数据,直接…

我的docker随笔43:问答平台answer部署

本文介绍开源问答社区平台Answer的容器化部署。 起因 笔者一直想搭建一个类似stack overflower这样的平台,自使用了Typora,就正式全面用MarkdownTyporagit来积累自己的个人知识库,但没有做到web化,现在也还在探索更好的方法。 无…

MyBatisPlus基础操作之增删改查

目录 一、基本使用 1.1 插入数据 1.2 删除操作 1.3 更新操作 二、条件构造器Wrapper 2.1 常用AbstractWrapper方法 2.1.1 示例一 2.2.2 示例二 2.2.3 示例三 2.2 常用QueryWrapper方法 2.2.1 示例一 2.2.2 示例二 2.2.3 示例三(常用) 2.3 常…