《领域驱动设计》读书笔记(二):模型驱动设计的构造块

《领域驱动设计》读书笔记(二):模型驱动设计的构造块

遵循“职责驱动设计”的原则,

“契约式设计”思想。

开发一个好的领域模型是一门艺术。

模型驱动设计语言
Figure 1. 模型驱动设计语言

第 4 章 分离领域

模式:Layered Architecture

在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期内完成开发工作。

如果与领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而要想调整业务规则也很可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。考虑到程序中各个活动所涉及的大量逻辑和技术,程序本身必须简单明了,否则就会让人无法理解。

要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。

Layered Architecture 的基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。

应用分层
Figure 2. 应用分层

给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。

关注点的清晰分离可以使每一层的设计更易理解和维护。

在连接各层的同时不影响分离带来的好处,这是很多模式的目的所在。

各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。

如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或 Observers 模式。

最早将用户界面层与应用层和领域层相连的模式是 Model-View-Controller(MVC,模型—视图—控制器)框架。

只要连接方式能够维持领域层的独立性,保证在设计领域对象时不需要同时考虑可能与其交互的用户界面,那么这些连接方式就都是可用的。

最好的架构框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型,而不考虑其他问题。

不妄求万全之策,只要有选择性地运用框架来解决难点问题,就可以避开框架的很多不足之处。

领域层是模型的精髓

“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。

如果领域逻辑与程序中的其他关注点混在一起,就不可能实现这种一致性。将领域实现独立出来是领域驱动设计的前提。

模式:The Smart UI“反模式”

Smart UI是另一种设计方法,与领域驱动设计方法迥然不同且互不兼容。

如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用 Model-Driven Design 以及 Layered Architecture,那么这个项目组将会经历一个艰难的学习过程。团队成员不得不去掌握复杂的新技术,艰难地学习对象建模。(即使有这本书的帮助,这也依然是一个具有挑战性的任务!)对基础设施和各层的管理工作使得原本简单的任务却要花费很长的时间来完成。简单项目的开发周期较短,期望值也不是很高。所以,早在项目团队完成任务之前,该项目就会被取消,更谈不上去论证有关这种方法的许多种令人激动的可行性了。

即使项目有更充裕的时间,如果没有专家的帮助,团队成员也不太可能掌握这些技术。最后,假如他们确实能够克服这些困难,恐怕也只会开发出一套简单的系统。因为这个项目本来就不需要丰富的功能。

在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。

如果一个架构能够把那些与领域相关的代码隔离出来,得到一个内聚的领域设计,同时又使领域与系统其他部分保持松散耦合,那么这种架构也许可以支持领域驱动设计。

如何让一个有效的领域模型和一个富有表达力的实现同时演进。

第 5 章 软件中所表示的模型

一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是 Entity 与 Value Object 之间的根本区别。

领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用 Service 来表示,而不应把操作的责任强加到 Entity 或 Value Object 上,尽管这样做稍微违背了面向对象的建模传统。 Service 是应客户端请求来完成某事。

在领域中也可以使用 Service,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项 Service。

每个设计决策都应该是在深入理解领域中的某些深层知识之后做出的。

关联

模型中每个可遍历的关联,软件中都要有同样属性的机制。

至少有 3种方法可以使得关联更易于控制:

  1. 规定一个遍历方向。

  2. 添加一个限定符,以便有效地减少多重关联。

  3. 消除不必要的关联。

双向关联意味着只有将这两个对象放在一起考虑才能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少相互依赖,并简化设计。

通常,通过更深入的理解可以得到一个“限定的”关系。

限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联,从而得到一个简单得多的设计。

模式:Entity(又称为Reference Object)

一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of IdEntity ),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。

主要由标识定义的对象被称作 Entity。 Entity (实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来决定,而不依赖于其所具有的属性。即使对于那些不发生根本变化或者生命周期不太复杂的 Entity,也应该在语义上把它们作为 Entity 来对待,这样可以得到更清晰的模型和更健壮的实现。

Entity 可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。

标识是 Entity 的一个微妙的、有意义的属性,我们是不能把它交给语言的自动特性来处理的。

事实上,两个对象可能有相同的标识,但属性可能不同,在需要的情况下甚至可能不属于同一个类。

当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”。

Entity 最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。

不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住 Entity 对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。

有时,某些数据属性或属性组合可以确保它们在系统中具有唯一性,或者在这些属性上加一些简单约束可以使其具有唯一性。

当对象属性没办法形成真正唯一键时,另一种经常用到的解决方案是为每个实例附加一个在类中唯一的符号(如一个数字或字符串)。

ID 通常是由系统自动生成的。生成算法必须确保 ID在系统中是唯一的。

模式:Value Object

跟踪 Entity 的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。软件设计要时刻与复杂性做斗争。我们必须区别对待问题,仅在真正需要的地方进行特殊处理。然而,如果仅仅把这类对象当作没有标识的对象,那么就忽略了它们的工具价值或术语价值。事实上,这些对象有其自己的特征,对模型也有着自己的重要意义。这些是用来描述事物的对象。

用于描述领域的某个方面而本身没有概念标识的对象称为 Value Object (值对象)。 Value Object 被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。

Value Object 甚至可以引用 Entity。

Value Object 经常作为参数在对象之间传递消息。

Value Object 可以用作 Entity (以及其他 Value)的属性。

当我们只关心一个模型元素的属性时,应把它归类为 Value Object。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。 Value Object 应该是不可变的。不要为它分配任何标识,而且不要把它设计成像 Entity 那么复杂。

Value Object 所包含的属性应该形成一个概念整体。

一个脱离了其所有者控制的‚流浪‛对象可能会发生任何事情。 VALUE的改变可能会破坏所有者的约束条件。这个问题可以通过传递一个不变对象或传递一个副本来解决。

Value Object 为性能优化提供了更多选择,

复制和共享哪个更划算取决于实现环境。

复制有可能导致系统被大量的对象阻塞,

共享可能会减慢分布式系统的速度。

以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:

  • 节省数据库空间或减少对象数量是一个关键要求时;

  • 通信开销很低时(如在中央服务器中);

  • 共享的对象被严格限定为不可变时。

当在设计中将一个 Value Object 指定为不可变时,开发人员就可以完全根据技术需求来决定是使用复制,还是使用共享,因为他们没有后顾之忧——应用程序不依赖于对象的特殊实例。

需要让 Value Object 是可变的。这包括以下因素:

  • 如果 Value 频繁改变;

  • 如果创建或删除对象的开销很大;

  • 如果替换(而不是修改)将打乱集群(像前面示例中讨论的那样);

  • 如果 VALUE的共享不多,或者共享不会提高集群性能,或其他某种技术原因。

再次强调:如果一个 Value 的实现是可变的,那么就不能共享它。无论是否共享 Value Object,在可能的情况下都要将它们设计为不可变的。

模型中的关联越少越好,越简单越好。

如果说 Entity 之间的双向关联很难维护,那么两个 Value Object 之间的双向关联则完全没有意义。

模式:Service

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是 Service(服务)。

一个比较常见的错误是没有努力为这类行为找到一个适当的对象,而是逐渐转为过程化的编程。

复杂的操作很容易把一个简单对象搞乱,使对象的角色变得模糊。

一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为 Entity 或 Value Object 的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。

Service 是作为接口提供的一种操作,它在模型中是独立的,它不像 Entity 和 Value Object 那样具有封装的状态。

所谓 Service,它强调的是与其他对象的关系。

Service 往往是以一个活动来命名,而不是以一个 Entity 来命名,也就是说,它是动词而不是名词。

操作名称应来自于 Ubiquitous Language,如果 Ubiquitous Language 中没有这个名称,则应该将其引入到 Ubiquitous Language 中。参数和结果应该是领域对象。

使用 Service 时应谨慎,它们不应该替代 Entity 和 Value Object 的所有行为。

好的 Service 有以下 3 个特征:

  1. 与领域概念相关的操作不是 Entity 或 Value Object 的一个自然组成部分。

  2. 接口是根据领域模型的其他元素定义的。

  3. 操作是无状态的。

当领域中的某个重要的过程或转换操作不是 Entity 或 Value Object 的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为 Service。定义接口时要使用模型语言,并确保操作名称是 Ubiquitous Language 中的术语。此外,应该使 Service 成为无状态的。

Service 并不只是在领域层中使用。

注意区分属于领域层的 Service 和那些属于其他层的 Service,并划分责任,以便将它们明确地区分开。

应用层负责通知的设臵,而领域层负责确定是否满足临界值,

很多领域或应用层 Service 是在 Entity 和 Value Object 的基础上建立起来的,它们的行为类似于将领域的一些潜在功能组织起来以执行某种任务的脚本。

由于应用层负责对领域对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识泄漏到应用层中。

明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。

一个“操作”对象可能足以作为 Service 接口的实现。

模式:Module(也称为Package)

认知超载

Module 为人们提供了两种观察模型的方式,一是可以在 Module 中查看细节,而不会被整个模型淹没,二是观察 Module 之间的关系,而不考虑其内部细节。

每个人都会使用 Module,但却很少有人把它们当做模型中的一个成熟的组成部分。代码按照各种各样的类别进行分解,有时是按照技术架构来分割的,有时是按照开发人员的任务分工来分割的。甚至那些从事大量重构工作的开发人员也倾向于使用项目早期形成的一些 Module。

众所周知, Module 之间应该是低耦合的,而在 Module 的内部则是高内聚的。耦合和内聚的解释使得 Module 听上去像是一种技术指标,仿佛是根据关联和交互的分布情况来机械地判断它们。然而, Module 并不仅仅是代码的划分,而且也是概念的划分。一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。

在一个好的模型中,元素之间是要协同工作的,而仔细选择的 Module 可以将那些具有紧密概念关系的模型元素集中到一起。

重构 Module 需要比重构类做更多工作,也具有更大的破坏性,并且可能不会特别频繁。

让 Module 反映出对领域理解的不断变化,可以使 Module 中的对象能够更自由地演变。

Module 是一种表达机制。

Module 的选择应该取决于被划分到模块中的对象的意义。

选择能够描述系统的 Module,并使之包含一个内聚的概念集合。这通常会实现 Module 之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为 Module 基础的概念(这个概念先前可能被忽视了),基于这个概念组织的 Module 可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。

Module 的名称应该是 Ubiquitous Language 中的术语。 Module 及其名称应反映出领域的深层知识。

接受挑战,仔细地分析问题的要害所在,并据此重新组织 Module。

如果一个类确实依赖于另一个包中的某个类,而且本地 Module 对该 Module 并没有概念上的依赖关系,那么或许应该移动一个类,或者考虑重新组织 Module。

一个非常有用的框架标准是 Layered Architecture,它将基础设施和用户界面代码放到两组不同的包中,并且从物理上把领域层隔离到它自己的一组包中。

对象的一个最基本的概念是将数据和操作这些数据的逻辑封装在一起。

人的大脑把划分后的东西还原成原样的能力是有限的,如果框架把人的这种能力都耗尽了,那么领域开发人员就无法再把模型还原成有意义的部分了。

除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中(如果不能放在同一个对象中的话)。

利用打包把领域层从其他代码中分离出来。否则,就尽可能让领域开发人员自由地决定领域对象的打包方式,以便支持他们的模型和设计选择。

建模范式

将各个部分紧密结合在一起的最有效工具就是健壮的 Ubiquitous Language,它是构成整个异构模型的基础。

当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下 4条经验规则。

不要和实现范式对抗。

把通用语言作为依靠的基础。

不要一味依赖 UML。

保持怀疑态度。

第 6 章 领域对象的生命周期

主要的挑战有以下两类:

  1. 在整个生命周期中维护完整性。

  2. 防止模型陷入管理生命周期复杂性造成的困境当中。

Aggregate(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。

使用 Factory(工厂)来创建和重建复杂对象和 Aggregate(聚合),从而封装它们的内部结构。

在生命周期的中间和末尾使用 Repository(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。

使用 Aggregate 进行建模,并且在设计中结合使用 Factory 和 Repository,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。

模式:Aggregate

在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。

首先,我们需要用一个抽象来封装模型中的引用。 Aggregate 就是一组相关对象的集合,我们把它作为数据修改的单元。每个 Aggregate 都有一个根(root)和一个边界(boundary)。边界定义了 Aggregate 的内部都有什么。根则是 Aggregate 所包含的一个特定 Entity。对 Aggregate 而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他 Entity 都有本地标识,但这些标识只在 Aggregate 内部才需要加以区别,因为外部对象除了根 Entity 之外看不到其他对象。

固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及 Aggregate 成员之间的内部关系。而任何跨越 Aggregate 的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定的时间内得以解决。但在每个事务完成时, Aggregate 内部所应用的固定规则必须得到满足,

根 Entity 具有全局标识,它最终负责检查固定规则。

根 Entity 具有全局标识。边界内的 Entity 具有本地标识,这些标识只在 Aggregate 内部才是唯一的。

Aggregate 外部的对象不能引用除根 Entity 之外的任何内部对象。根 Entity 可以把对内部 Entity 的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个 Value Object 的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个 VALUE,不再与 Aggregate 有任何关联。

只有 Aggregate 的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。

我们应该将 Entity 和 Value Object 分门别类地聚集到 Aggregate 中,并定义每个 Aggregate 的边界。在每个 Aggregate 中,选择一个 Entity 作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保 Aggregate 中的对象满足所有固定规则,也可以确保在任何状态变化时 Aggregate 作为一个整体满足固定规则。

Aggregate 划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。

模式:Factory

当创建一个对象或创建整个 Aggregate 时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用 Factory 进行封装。

对象的功能主要体现在其复杂的内部配臵以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。

对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或 Aggregate 的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。

应该将创建复杂对象的实例和 Aggregate 的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建 Aggregate 时要把它作为一个整体,并确保它满足固定规则。

任何好的工厂都需满足以下两个基本需求:

  1. 每个创建方法都是原子的,而且要保证被创建对象或 Aggregate 的所有固定规则。

  2. Factory 应该被抽象为所需的类型,而不是所要创建的具体类。

一般来说, Factory 的作用是隐藏创建对象的细节,而且我们把 Factory 用在那些需要隐藏细节的地方。

当一个对象的创建主要使用另一个对象的数据(或许还有规则)时,则可以在后者的对象上创建一个 Factory Method,这样就不必将后者的信息提取到其他地方来创建前者。

Factory 与被构建对象之间是紧密耦合的,因此 Factory 应该只被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏(无论要隐藏的是具体实现还是构造的复杂性)而又找不到合适的地方来隐藏它们时,必须创建一个专用的 Factory 对象或 Service。整个 Aggregate 通常由一个独立的 Factory 来创建, Factory 负责把对根的引用传递出去,并确保创建出的 Aggregate 满足固定规则。

公共构造函数必须遵守与 Factory 相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。

当设计 Factory 的方法签名时,无论是独立的 Factory 还是 Factory Method,都要记住以下两点。

  1. 每个操作都必须是原子的。我们必须在与 Factory 的一次交互中把创建对象所需的所有信息传递给 Factory。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。可以抛出一个异常或仅仅返回 null。为了保持一致,可以考虑采用编码标准来处理所有 Factory 的失败。

  2. Factory 将与其参数发生耦合。如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数(argument)的处理。如果只是简单地将参数插入到要构建的对象中,则依赖度是适中的。如果从参数中选出一部分在构造对象时使用,耦合将更紧密。

最安全的参数是那些来自较低设计层的参数。

另一个好的参数选择是模型中与被构建对象密切相关的对象,这样不会增加新的依赖。

使用抽象类型的参数,而不是它们的具体类。

Factory 负责确保它所创建的对象或 Aggregate 满足所有固定规则,然而在把应用于一个对象的规则移到该对象外部之前应三思。

如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没有必要携带这个逻辑。在这种情况下, Factory 是放臵固定规则的合适地方,这样可以使 Factory 创建出的对象更简单。

由于 Value Object 是不可变的,因此, Factory 所生成的对象就是最终形式。因此 Factory 操作必须得到被创建对象的完整描述。

Entity Factory 则只需具有构造有效 Aggregate 所需的那些属性。对于固定规则不关心的细节,可以之后再添加。

模式:Repository

领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。

客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过 Aggregate 的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而 Entity 和 Value Object 则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些 Aggregate 根的时候,就需要使用这种访问方式。它们通常是 Entity,有时是具有复杂内部结构的 Value Object,还可能是枚举 VALUE。而其他对象则不宜使用这种访问方式,因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和 Aggregate。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。

Repository 将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时, Repository 的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对 Aggregate 根的整个生命周期的全程访问。

为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的 Aggregate 根提供 Repository。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给 Repository 来完成。

对类型进行抽象。 Repository “含有”特定类型的所有实例,但这并不意味着每个类都需要有一个 Repository。

将事务的控制权留给客户。尽管 Repository 会执行数据库的插入和删除操作,但它通常不会提交事务。

一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。寻求领域驱动设计的概念与框架中的概念之间的相似性。

Factory 负责处理对象生命周期的开始,而 Repository 帮助管理生命周期的中间和结束。

从领域驱动设计的角度来看, Factory 和 Repository 具有完全不同的职责。 Factory 负责制造新对象,而 Repository 负责查找已有对象。

Factory 的工作是用数据来实例化一个可能很复杂的

通常,在领域中将新对象和原有对象区分开是很重要的,而将它们组合在一起的框架实际上只会使局面变得混乱。

允许例外情况的另一个原因是性能。

Ubiquitous Language 可能有助于将对象和关系组件联系起来,使之成为单一的模型。

第 7 章 使用语言:一个扩展的示例

记住,一般情况下,模型的精化、设计和实现应该在迭代开发过程中同步进行。

一般来说,当为了更好地支持设计而对模型进行精化时,也应该让模型反映出对领域的新理解。

应用层类是协调者,它们只是负责提问,而不负责回答,

建模和设计并不总是一个不断向前的过程,如果不经常进行重构,以便利用新的知识来改进模型和设计,那么建模和设计将会停滞不前。

我们应该寻找紧密关联的概念,并弄清楚我们打算向项目中的其他人员传递什么信息。

《分析模式》一书介绍了一种用于解决这类问题的模式: Enterprise Segment(企业部门单元)。 Enterprise Segment 是一组维度,它们定义了一种对业务进行划分的方式。