《领域驱动设计》
模型在领域驱动设计中的作用
- 模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品(即一个可运行的程序)。模型与实现之间的这种紧密结合在维护和后续开发期间也会很有用,因为我们可以基于对模型的理解来解释代码。
- 模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序。他们可以在无需翻译的情况下与领域专家进行沟通。而且,由于该语言是基于模型的,因此我们可借助自然语言对模型本身进行精化。
- 模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。当开发人员和领域专家在将信息组织为模型时,这一共同的语言(模型)能够促使他们高效地协作。模型与实现之间的紧密结合使来自软件早期版本的经验可以作为反馈应用到建模过程中。
软件的核心是其为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。
消化知识
1.1 有效建模的要素
- 模型和实现的绑定。最初的原型虽然简陋,但它在模型与实现之间建立了早期链接,而 且在所有后续的迭代中我们一直在维护该链接。
- 建立了一种基于模型的语言。最初,工程师们不得不向我解释基本的PCB问题,而我也必须向他们解释类图的含义。但随着项目的进展,双方都能够直接使用模型中的术语,并将它们组织为符合模型结构的语句,而且无需翻译即可理解互相要表达的意思。
- 开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。
- 提炼模型。在模型日趋完整的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,其他那些不要的概念就可以丢弃了。
- 头脑风暴和实验。语言和草图,再加上头脑风暴活动,将我们的讨论变成“模型实验室”,在这些讨论中可以演示、尝试和判断上百种变化。当团队走查场景时,口头表达本身就可以作为所提议的模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简捷, 还是表达得很笨拙。
1.2 知识消化
- 高效的领域建模人员是知识的消化者。他们在大量信息中探寻有用的部分。他们不断尝试各种信息组织方式,努力寻找对大量信息有意义的简单视图。很多模型在尝试后被放弃或改造。只有找到一组适用于所有细节的抽象概念后,工作才算成功。这一精华严谨地表示了所发现的最为相关的知识。
- 知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。
1.3 持续学习
高效率的团队需要有意识地积累知识,并持续学习。
那些早期工作还是非常重要的。关键的模型元素被保留下来,而更重要的是,早期工作启动了知识消化的过程,这使得所有后续工作更加高效:团队成员、开发人员和领域专家等都学到了知识,他们开始使用一种公共的语言,而且形成了贯穿整个实现过程的反馈闭环。
1.4 知识丰富的设计
业务活动和规则如同所涉及的实体一样,都是领域的核心,任何领域都有各种类别的概念。知识消化所产生的模型能够反映出对知识的深层理解。在模型发生改变的同时,开发人员对实现进行重构,以便反映出模型的变化,这样,新知识就被合并到应用程序中了。
- 为了实现更明确的设计,程序员和其他各位相关人员都必须理解超订的本质,明白它是 一个明确且重要的业务规则,而不只是一个不起眼的计算。
- 程序员可以向业务专家展示技术工件,甚至是代码,但应该是领域专家(在程序员指导 下)可以理解的,以便形成反馈闭环。
1.5 深层模型
有用的模型很少停留在表面。随着对领域和应用程序需求的理解逐步加深,我们往往会丢弃 那些最初看起来很重要的表面元素,或者切换它们的角度。这时,一些开始时不可能发现的巧妙 抽象就会渐渐浮出水面,而它们恰恰切中问题的要害。
交流与语言的使用
领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概 念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型语言的 语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。
2.1 模式:UBIQUITOUS LANGUAGE
将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
通过尝试不同的表示方法(它们反映了备选模型)来消除难点。然后重构代码,重新命名类、方法和模块,以便与新模型保持一致。解决交谈中的术语混淆问题,就像我们对普通词汇形成一致的理解一样。
要认识到,UBIQUITOUS LANGUAGE的更改就是对模型的更改。
领域专家应该抵制不合适或无法充分表达领域理解的术语或结构,开发人员应该密切关注那些将会妨碍设计的有歧义和不一致的地方。
2.2 “大声地”建模
讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式 将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到 图和代码中。
2.3 一个团队,一种语言
开发人员有其所需的大量术语来讨论系统技术。几乎可以肯定的是,用户也会用开发人员无法理解的、超出应用程序范畴的专用术语。这些都是对语言的扩展。但在这些语言扩展中,同一领域的相同词汇不应该反映不同的模型。
有了UBIQUITOUS LANGUAGE之后,开发人员之间的对话、领域专家之间的讨论以及代码本身所表达的内容都基于同一种语言,都来自于一个共享的领域模型。
2.4 文档和图
- 文档应作为代码和口头交流的补充。
- 文档不应再重复表示代码已经明确表达出的内容。
- 文档应当鲜活并保持最新
- 文档必须深入到各种项目活动中去
2.5 解释性模型
驱动软件开发过程的技术模型必须经过严格的精简, 以便用最小化的模型来实现其功能。而解释性模型则可以包含那些提供上下文的领域方面——这 些上下文用于澄清范围更窄的模型。
绑定模型和实现
3.1 模式:MODEL-DRIVEN DESIGN
如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析与和设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。
软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关 系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更 深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮 的UBIQUITOUS LANGUAGE(通用语言)。
从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能会是模型的改变。而其影响势必要波及接下来相应的项目活动。
完全依赖模型的实现通常需要支持建模范式的软件开发工具和语言,比如面向对象的编程。
3.2 建模范式和工具支持
为了使MODEL-DRIVEN DESIGN发挥作用,一定要在可控范围内严格保证模型与设计之间的一 致性。要实现这种严格的一致性,必须要运用由软件工具支持的建模范式,它可以在程序中直接建模型中的对应概念。
3.3 揭示主旨:为什么模型对用户至关重要
如果程序设计基于一个能够反映出用户和领域专家所关心的基本问题的模型,那么与其他设计方式相比,这种设计可以将其主旨更明确地展示给用户。让用户了解模型,将使他们有更多机会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。
3.4 模式:HANDS-ON MODELER
任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型 讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过UBIQUITOUS LANGUAGE与接触代码的人及时交换关于模型的想法。
模型驱动设计的构造块
开发一个好的领域模型是一门艺术。而模型中各个元素的实际设计和实现则相对系统化。将领域设计与软件系统中的其他关注点分离会使设计与模型之间的关系非常清晰。根据不同的特征来定义模型元素则会使元素的意义更加鲜明。对每个元素使用已验证的模式有助于创建出更易于实现的模型。
分 离 领 域
4.1 模式:LAYERED ARCHITECTURE
LAYERED ARCHITECTURE的基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。向上的通信必须通过间接的方式进行。
4.1.1 将各层关联起来
各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。
而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或OBSERVERS模式。
4.1.2 将各层关联起来
最好的架构框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型,
而不考虑其他问题。
4.2 领域层是模型的精髓
现在,大部分软件系统都采用了LAYERED ARCHITECTURE,只是采用的分层方案存在不同而已。许多类型的开发工作都能从分层中受益。然而,领域驱动设计只需要一个特定的层存在即可。
领域模型是一系列概念的集合。“领域层”则是领域模型以及所有与其直接相关的设计元素
的表现,它由业务逻辑的设计和实现组成。在MODEL-DRIVEN DESIGN中,领域层的软件构造反映出了模型概念。
软件中所表示的模型
5.1 关联
对象之间的关联使得建模与实现之间的交互更为复杂。
模型中每个可遍历的关联,软件中都要有同样属性的机制。
5.2 模式:ENTITY(又称为REFERENCE OBJECT)
主要由标识定义的对象被称作ENTITY。ENTITY(实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来决定,而不依赖于其所具有的属性。即使对于那些不发生根本变化或者生命周期不太复杂的ENTITY,也应该在语义上把它们作为ENTITY来对待,这样可以得到更清晰的模型和更健壮的实现。
ENTITY可以是任何事物,只要满足两个条件即可,
- 一是它在整个生命周期中具有连续性,
- 二是它的区别并不是由那些对用户非常重要的属性决定的。
ENTITY可以是一个人、一座城市、一辆汽车、一张彩票或一次银行交易。
当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。定义一种区分每个对象的方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。
模型必须定义出“符合什么条件才算是相同的事物”。
5.2.1 ENTITY建模
当对一个对象进行建模时,我们自然而然会考虑它的属性,而且考虑它的行为也显得非常重要。但ENTITY最基本的职责是确保连续性,以便使其行为更清楚且可预测。
保持实体的简练是实现这一责任的关键。不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住ENTITY象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。此外,应该将行为和属性转移到与核心实体关联的其他对象中。这些对象中,有些可能是ENTITY,有些可能是VALUE OBJECT。
5.2.2 设计标识操作
每个ENTITY都必须有一种建立标识的操作方式,以便与其他对象区分开,即使这些对象与它具有相同的描述属性。不管系统是如何定义的,都必须确保标识属性在系统中是唯一的,即使是在分布式系统中,或者对象已被归档,也必须确保标识的唯一性。
当对象属性没办法形成真正唯一键时,另一种经常用到的解决方案是为每个实例附加一个在类中唯一的符号(如一个数字或字符串)。一旦这个ID符号被创建并存储为ENTITY的一个属性,必须将它指定为不可变的。
5.3 模式:VALUE OBJECT
很多对象没有概念上的标识,它们描述了一个事务的某种特征。
用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。VALUE OBJECT被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。
当我们只关心一个模型元素的属性时,应把它归类为VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。
5.3.1 设计VALUE OBJECT
保持VALUE OBJECT不变可以极大地简化实现,并确保共享和引用传递的安全性。而且这样做也符合值的意义。
5.3.2 设计包含VALUE OBJECT的关联
模型中的关联越少越好,越简单越好。
5.4 模式:SERVICE
在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。
SERVICE是作为接口提供的一种操作,它在模型中是独立的,它不像ENTITY和VALUE OBJECT那样具有封装的状态。SERVICE是技术框架中的一种常见模式,但它们也可以在领域层中使用。
好的SERVICE有以下3个特征。
(1) 与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
(2) 接口是根据领域模型的其他元素定义的。
(3) 操作是无状态的。
5.4.1 SERVICE与孤立的领域层
我们需要注意区分属于领域层的SERVICE和那些属于其他层的SERVICE,并划分责任,以便将它们明确地区分开。
文献中所讨论的大多数SERVICE是纯技术的SERVICE,它们都属于基础设施层。领域层和应用层的SERVICE与这些基础设施层SERVICE进行协作。
5.4.2 粒度
上述对SERVICE的讨论强调的是将一个概念建模为SERVICE的表现力,但SERVICE还有其他有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与ENTITY和VALUE OBJECT耦合。
5.5 模式:MODULE(也称为PACKAGE)
像领域驱动设计中的其他元素一样,MODULE是一种表达机制。MODULE的选择应该取决于被划分到模块中的对象的意义。
5.5.1 敏捷的MODULE
MODULE需要与模型的其他部分一同演变。这意味着MODULE的重构必须与模型和代码一起进行。
5.5.2 通过基础设施打包时存在的隐患
除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中(如果不能放在同一个对象中的话)。
5.6 建模范式
目前,主流的范式是面向对象设计,而且现在的大部分复杂项目都开始使用对象。这种范式的流行有许多原因,包括对象本身的固有因素、一些环境因素,以及广泛使用所带来的一些优势。
5.6.1 对象范式流行的原因
十年过去了,面向对象技术已经相对成熟。业内已经提供了很多现成的解决方案,它们可以满足大部分常见的基础设施需要。
这就是目前大部分采用MODEL-DRIVEN DESIGN的项目很明智地使用面向对象技术作为系统核心的原因。它们不会被束缚在只有对象的系统里,因为对象已经成为内业的主流技术,人们目前使用的几乎所有的技术都有与之对应的集成工具。
5.6.2 对象世界中的非对象
当领域的主要部分明显属于不同的范式时,明智的做法是用适合各个部分的范式对其建模,并使用混合工具集来进行实现。混合使用的范式使得开发人员能够用最适当的风格对特殊概念进行建模。
领域对象的生命周期
每个对象都有生命周期,如图6-1所示。对象自创建后,可能会经历各种不同的状态,
直至最终消亡——要么存档,要么删除。
6.1 模式:AGGREGATE(聚合)
在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。
6.2 模式:FACTORY
当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用FACTORY进行封装。
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
6.3 模式:REPOSITORY
我们可以通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个ENTITY或VALUE。
6.3.1 REPOSITORY的查询
所有REPOSITORY都为客户提供了根据某种条件来查询对象的方法,但如何设计这个接口却有很多选择。
- 硬编码的方式来实现一些具有特定参数的查询。
- 基于SPECIFICATION(规格)的查询
6.3.2 客户代码可以忽略REPOSITORY的实现,但开发人员不能忽略
开发人员需要理解使用封装行为的隐含问题,但这并不意味着要熟悉实现的每个细节。设计良好的组件是有显著特征的。
6.3.3 REPOSITORY的实现
REPOSITORY的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。
REPOSITORY将会委托相应的基础设施服务来完成工作。
REPOSITORY的注意事项
- 对类型进行抽象
- 充分利用与客户解耦的优点
- 将事务的控制权留给客户
6.3.4 在框架内工作
在实现REPOSITORY这样的构造之前,需要认真思考所使用的基础设施,特别是架构框架。这些框架可能提供了一些可用来轻松创建REPOSITORY的服务,但也可能会妨碍创建REPOSITORY的工作。
6.3.5 REPOSITORY与FACTORY的关系
FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。
使用语言:一个扩展的示例
后面章节翻译的不知所云,暂时先放弃了!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)