要想创建一种灵活的、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语
言不断的试验——然而,软件项目上很少出现这样的试验。

如果语言支离破碎,项目必将遭遇严重问题。领域专家使用他们自己的术语,而技术团队所
使用的语言则经过调整,以便从设计角度讨论领域。
日常讨论所使用的术语与代码(软件项目的最重要产品)中使用的术语不一致。甚至同一个
人在讲话和写东西时使用的语言也不一致,这导致的后果是,对领域的深刻表述常常稍纵即逝,
根本无法记录到代码或文档中。
翻译使得沟通不畅,并削弱了知识消化。
然而任何一方的语言都不能成为公共语言,因为它们无法满足所有的需求。
所有翻译的开销,连带着误解的风险,成本实在太高了。项目需要一种公共语言。

未命名-5.png
未命名-6.png

未命名-7.png

未命名-8.png

当我们在讨论中使用领域模型的 UBIQUITOUS LANGUAGE 时,特别是在开发人员和领域专家一起推敲场景和需求时,通用语言的使用会越来越流利,而且我们还可以互相指点一些细微的差别。
我们自然而然地共享了我们所说的语言,而这种方式是图和文档无法做到的。

刚开始

  1. 领域专家与开发人员均不熟悉对方的沟通语言方式;
  2. 逐步迭代的找到合适的共同语言,刚开始可能互相听不懂,但逐步完善;
  3. 逐步迭代的设计领域模型,刚开始可能很简单丑陋,但逐步完善;

但在这些语言扩展中,同一领域的相同词汇不应该反映不同的模型。
未命名-9.png

  1. UML 图可以表示出对象的属性和关系,但是对应对象的行为、约束等就无法很好的体现。换言之,UML 图无法
    传达模型的两个最重要的方面,一个方面是模型所表示的概念的意义,另一方面是对象应该做哪
    些事情。但是,这并不是大问题,因为通过仔细地使用语言(英语、西班牙语或其他任何一种语
    言)就可以很好地完成这项任务。

鉴于此,我们应避免使用包罗万象的对象模型图,甚至不能使
用包含所有细节的 UML 数据存储库。相反,应使用简化的图,图中只包含对象模型的重要概
念——这些部分对于理解设计至关重要。本书中的图都是我在项目中使用过比较典型的图。它们
很简单,而且具有很强的解释能力,在澄清一些要点时,还使用了一些非标准的符号。

这就是为什么我
喜欢把典型的 UML 使用方法颠倒过来的原因。通常的用法是以图为主,辅以文本注释;而我更
愿意以文本为主,用精心挑选的简化图作为说明。

如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,
软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在
实际项目中,当设计改变时也无法维护这种关系。若分析与和设计之间产生严重分歧,那么在分
析和设计活动中所获得的知识就无法彼此共享。

将领域模型和程序设计紧密联系在一起绝对是必要的,这也使得在众多可选模型中选择最适
用的模型时,又多了一条选择标准。它要求我们认真思考,并且通常会经过多次反复修改和重新
构建的过程,但是通过这样的过程可以得到与设计关联的模型。

我曾经在一个项目中负责协调不同的应用程序开发团队,帮助开发可以驱动程序设计的领域
模型。但是管理层认为建模人员就应该只负责建模工作,编写代码就是在浪费这种技能,于是他
们不准我编写代码或者与程序员讨论细节问题。
开始项目进展的还算顺利。我和领域专家以及各团队的开发负责人共同工作,消化领域知识
并提炼出了一个不错的核心模型。但是该模型却从来没有派上用场,原因有两个。
其一,模型的一些意图在其传递过程中丢失了。模型的整体效果受细节的影响很大(这将在
第二部分和第三部分讨论),这些细节问题并不是总能在 UML 图或者一般讨论中遇到的。如果我
能撸起袖子,直接与开发人员共同工作,提供一些参考代码和近距离的技术支持,那么他们也许
能够理解模型中的抽象概念并据此进行开发。
第二个原因是模型与程序实现及技术互相影响,而我无法直接获得这种反馈。例如,程序实
现过程中发现模型的某部分在我们的技术平台上的工作效率极低,但是经过几个月的时间,我才
一点一点获得了关于这个问题的全部信息。其实只需较少的改动就能解决这个问题,但是几个月
过去了,改不改已经不重要了。因为开发人员已经自行编写出了可以运行的软件——完全脱离了
模型的设计,在那些还在使用模型的地方,也仅仅是把它当作纯粹的数据结构。开发人员不分好
坏地把模型全盘否定,但是他们又有什么办法呢?他们再也不愿意冒险任由呆在象牙塔里的架构
师摆布了。

整体设计的有效性有几个非常敏感的影响因素——那就是细粒度的设计和实现决策的质量
和一致性。在 MODEL-DRIVEN DESIGN 中,代码是模型的表达,改变某段代码就改变了相应的模型。
程序员就是建模人员,无论他们是否喜欢。所以在开始项目时,应该让程序员完成出色的建模工作。
因此:
任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何
负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型
讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过 UBIQUITOUS LANGUAGE
与接触代码的人及时交换关于模型的想法。

未命名-10.png

第四章-分离领域

第四章就讲了在复杂的项目中,应该使用分离领域。
未命名-11.png

在一个运输应用程序中,要想支持从城市列表中选择运送货物目的地这样的简单用户行为,
程序代码必须包括:(1) 在屏幕上绘制一个屏幕组件(widget);(2) 查询数据库,调出所有可能的
城市;(3) 解析并验证用户输入;(4) 将所选城市与货物关联;(5) 向数据库提交此次数据修改。
上面所有的代码都在同一个程序中,但是只有一小部分代码与运输业务相关。

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

—— 因此,需要高内聚、低耦合的代码。

用户界面层(或表示层)
负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,
不一定是使用用户界面的人
应用层
定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负
责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道
应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调
任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有
另外一种状态,为用户或程序显示某个任务的进度
领域层(或模型层)
负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节
是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域
层是业务软件的核心
基础设施层
为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,
为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持 4 个层次
间的交互模式

将领域层分离出来才是实现 MODEL-DRIVEN DESIGN 的关键。

各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元
素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用
常规的交互手段。而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用
另一种通信机制,使用架构模式来连接上下层,如回调模式或 OBSERVERS 模式[Gamma et al. 1995]。

通常,基础设施层不会发起领域层中的操作,它处于领域层“之下”,不包含其所服务的领
域中的知识。事实上这种技术能力最常以 SERVICE 的形式提供。例如,如果一个应用程序需要发
送电子邮件,那么一些消息发送的接口可以放在基础设施层中,这样,应用层中的元素就可以请
求发送消息了。这种解耦使程序的功能更加丰富。消息发送接口可以连接到电子邮件发送服务、
传真发送服务或任何其他可用的服务。但是这种方式最主要的好处是简化了应用层,使其只专注
于自己所负责的工作:知道何时该发送消息,而不用操心怎么发送。
应用层和领域层可以调用基础设施层所提供的 SERVICE。如果 SERVICE 的范围选择合理,接口
设计完善,那么通过把详细行为封装到服务接口中,调用程序就可以保持与 SERVICE 的松散连接,
并且自身也会很简单。
然而,并不是所有的基础设施都是以可供上层调用的 SERVICE 的形式出现的。有些技术组件被
设计成直接支持其他层的基本功能(如为所有的领域对象提供抽象基类),并且提供关联机制(如
MVC 及类似框架的实现)。这种“架构框架”对于程序其他部分的设计有着更大的影响。

第五章-软件中所表示的模型

本章主要讨论这些基本模型元素并理解它们,以便为后面章节的讨论打好基础。
本章的讨论从如何设计和简化关联开始。对象之间的关联很容易想出来,也很容易画出来,但实现它们却存在很多潜在的麻烦。关联也表明了具体的实现决策在 MODEL-DRIVEN DESIGN 中的重要性。
本章的讨论将侧重于模型本身,但仍继续仔细考查具体模型选择与实现问题之间的关系,我们将着重区分用于表示模型的 3 种模型元素模式:ENTITY、VALUE OBJECT 和 SERVICE。

一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚
至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是 ENTITY 与 VALUE OBJECT 之
间的根本区别。明确地选择这两种模式中的一个来定义对象,有利于减少歧义,并帮助我们做出
特定的选择,这样才能得到健壮的设计。
主要由标识定义的对象被称作 ENTITY。ENTITY 可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。
VALUE OBJECT —— 值对象,即之前实习时的 Context 对象,仅仅存储数值,不需要唯一标识。例如,从画画的角度,如果两个画笔的颜色是一样的,形状是一样的,那么画画时就不需要区分这两个画笔,此时就是值对象。
VALUE OBJECT 经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。
我们并不关心使用的是 VALUE OBJECT 的哪个实例。由于不受这方面的约束,设计可以获得更大的自由,因此可以简化设计或优化性能。在设计 VALUE OBJECT 时有多种选择,包括复制、共享或保持 VALUE OBJECT 不变。

VALUE OBJECT 为性能优化提供了更多选择,这一点可能很重要,因为 VALUE OBJECT 往往为数
众多。房屋设计软件的示例就说明了这一点。如果每个电源插座都是一个单独的 VALUE OBJECT,
那么在一所房屋的一个设计版本中可能就会有上百个这种 VALUE OBJECT。但如果把电源插座看成
是可互换的,就只需共享一个电源插座实例,并让所有电源插座都指向这个实例(FLYWEIGHT,
[Gamma et al. 1995]中的一个示例)。
复制和共享哪个更划算取决于实现环境。虽然复制有可能导致系统被大量的对象阻塞,但共
享可能会减慢分布式系统的速度。当在两个机器之间传递一个副本时,只需发送一条消息,而且
副本到达接收端后是独立存在的。但如果共享一个实例,那么只会传递一个引用,这要求每次交
互都要向发送方返回一条消息。
以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:
 节省数据库空间或减少对象数量是一个关键要求时;
 通信开销很低时(如在中央服务器中);
 共享的对象被严格限定为不可变时。400

领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用
SERVICE 来表示,而不应把操作的责任强加到 ENTITY 或 VALUE OBJECT 上,尽管这样做稍微违背了
面向对象的建模传统。
好的 SERVICE 有以下 3 个特征。
(1) 与领域概念相关的操作不是 ENTITY 或 VALUE OBJECT 的一个自然组成部分。
(2) 接口是根据领域模型的其他元素定义的。
(3) 操作是无状态的。

对象之间的关联使得建模与实现之间的交互更为复杂。
模型中每个可遍历的关联,软件中都要有同样属性的机制。

现实生活中有大量‚多对多‛关联,其中有很多关联天生就是双向的。我们在模型开发的早
期进行头脑风暴活动并探索领域时,也会得到很多这样的关联。但这些普遍的关联会使实现和维
护变得很复杂。此外,它们也很少能表示出关系的本质。
至少有 3 种方法可以使得关联更易于控制。
(1) 规定一个遍历方向。
(2) 添加一个限定符,以便有效地减少多重关联。
(3) 消除不必要的关联。
尽可能地对关系进行约束是非常重要的。双向关联意味着只有将这两个对象放在一起考虑才
能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少相互依赖,并简
化设计。理解了领域之后就可以自然地确定一个方向。

像很多国家一样,美国有过很多位总统。这是一种双向的、一对多的关系。然而,在提到‚乔
治〃华盛顿‛这个名字时,我们很少会问‚他是哪个国家的总统?‛。从实用的角度讲,我们可
以将这种关系简化为从国家到总统的单向关联。这种精化实际上反映了对领域的
深入理解,而且也是一个更实用的设计。它表明一个方向的关联比另一个方向的关联更有意义且
更重要。

通常,通过更深入的理解可以得到一个‚限定的‛关系。进一步研究总统的例子就可以知道,
一个国家在一段时期内只能有一位总统(内战期间或许有例外)。这个限定条件把多重关系简化
为一对一关系,并且在模型中植入了一条明确的规则。
未命名-12.png

Service

将 SERVICE 划分到各个层中
应用层
资金转账应用服务
 获取输入(如一个 XML 请求)
 发送消息给领域层服务,要求其执行
 监听确认消息
 决定使用基础设施 SERVICE 来发送通知
领域层
资金转账领域服务
 与必要的 Account(账户)和 Ledger(总账)对象进行交互,执行相应的借入和贷出操作
 提供结果的确认(允许转账或拒绝转账等)
基础设施层
发送通知服务
 按照应用程序的指示发送电子邮件、信件和其他信息

上述对 SERVICE 的讨论强调的是将一个概念建模为 SERVICE 的表现力,但 SERVICE 还有其他
有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与 ENTITY 和 VALUE OBJECT
耦合。

Module/Package
每个人都会使用 MODULE,但却很少有人把它们当做模型中的一个成熟的组成部分。代码按
照各种各样的类别进行分解,有时是按照技术架构来分割的,有时是按照开发人员的任务分工来
分割的。甚至那些从事大量重构工作的开发人员也倾向于使用项目早期形成的一些 MODULE。
众所周知,MODULE 之间应该是低耦合的,而在 MODULE 的内部则是高内聚的。

只要两个模型元素被划分到不同的 MODULE 中,它们的关系就不如原来那样直接,这会使我
们更难理解它们在设计中的作用。MODULE 之间的低耦合可以将这种负面作用减至最小,而且在
分析一个 MODULE 的内容时,只需很少地参考那些与之交互的其他 MODULE。

当你将一些类放到 MODULE 中时,相当于告诉下一位看到你的设
计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那么 MODULE 就是这个故
事的各个章节。

MODULE 需要与模型的其他部分一同演变。这意味着 MODULE 的重构必须与模型和代码一起
进行。但这种重构通常不会发生。更改 MODULE 可能需要大范围地更新代码。这些更改可能会对
团队沟通起到破坏作用,甚至会妨碍开发工具(如源代码控制系统)的使用。因此,MODULE 结
构和名称往往反映了模型的较早形式,而类则不是这样。

没懂这个是什么意思?600

后面还讲了一部分,但是有点晕,没看懂。

建模范式
当前主流的建模范式还是面向对象。

其他建模范式,可能会比较复杂,上手困难,或者语言难以支持,或者没有那么成熟,或者技术基础设施不够完善等问题,以至于开发运用时效果并不好。所以还是面向对象属于主流。但并不意味着永远局限在此。

在混合范式中坚持使用 MODEL-DRIVEN DESIGN
在面向对象的应用程序开发项目中,有时会混合使用一些其他的技术,规则引擎就是一个常
见的例子。一个包含丰富知识的领域模型可能会含有一些显式的规则,然而对象范式却缺少用于
表达规则和规则交互的具体语义。尽管可以将规则建模为对象(而且常常可以成功地做到),但
对象封装却使得那些针对整个系统的全局规则很难应用。规则引擎技术非常有吸引力,因为它提
供了一种更自然、声明式的规则定义方式,能够有效地将规则范式融合到对象范式中。逻辑范式
已经得到了很好的发展并且功能强大,它是对象范式的很好补充,使其可以扬长避短。
但人们并不总是能够从规则引擎的使用中得到预期结果。有些产品并不能很好地工作。有些
则缺少一种能够显示出衔接两种实现环境的模型概念相关性的无缝视图。一个常见的结果是应用
程序被割裂成两部分:一个是使用了对象的静态数据存储系统,另一个是几乎完全与对象模型失
去联系的某种规则处理应用程序。
重要的是在使用规则的同时要继续考虑模型。团队必须找到能够同时适用于两种实现范式
的单一模型。虽然这并非易事,但还是可以办到的,条件是规则引擎支持富有表达力的实现方
式。
虽然 MODEL-DRIVEN DESIGN 不一定是面向对象的,但它确实需要一种富有表达力的模型结构
实现,无论是对象、规则还是工作流,都是如此。如果可用工具无法提高表达力,就要重新考虑
选择工具。缺乏表达力的实现将削弱各种范式的优势。

当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下 4 条经验规则。
 不要和实现范式对抗。我们总是可以用别的方式来考虑领域。找到适合于范式的模型概
念。
 把通用语言作为依靠的基础。即使工具之间没有严格联系时,语言使用上的高度一致性
也能防止各个设计部分分裂。
 不要一味依赖 UML。有时固定使用某种工具(如 UML 绘图工具)将导致人们通过歪曲模
型来使它更容易画出来。例如,UML 确实有一些特性很适合表达约束,但它并不是在所
有情况下都适用。有时使用其他风格的图形(可能适用于其他范式)或者简单的语言描
述比牵强附会地适应某种对象视图更好。
 保持怀疑态度。工具是否真正有用武之地?不能因为存在一些规则,就必须使用规则引
擎。规则也可以表示为对象,虽然可能不是特别优雅。多个范式会使问题变得非常复杂。

在决定使用混合范式之前,一定要确信主要范式中的各种可能性都已经尝试过了。尽管有些
领域概念不是以明显的对象形式表现出来的,但它们通常可以用对象范式来建模。第 9 章将讨论
如何使用对象技术对一些非常规类型的概念进行建模。
关系范式是范式混合的一个特例。作为一种最常用的非对象技术,关系数据库与对象模型的
关系比其他技术与对象模型的关系更紧密,因为它作为一种数据持久存储机制,存储的就是对象。

第六章-领域对象的生命周期

首先是 AGGREGATE 通过定义清晰的所属关系
和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各
个阶段的完整性具有至关重要的作用。

生命周期的开始阶段,使用 FACTORY(工厂)来创建和重建复
杂对象和 AGGREGATE(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用
REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
使用 AGGREGATE 进行建模,并且在设计中结合使用 FACTORY 和 REPOSITORY,这样我们就能够
在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。AGGREGATE 可以划分出一个
范围,这个范围内的模型元素在生命周期各个阶段都应该维护其固定规则。 FACTORY 和
REPOSITORY 在 AGGREGATE 基础上进行操作,将特定生命周期转换的复杂性封装起来。

AGGREGATE

减少设计中的关联可以简化对象之间的遍历,并限制关系的急剧增多。但很多业务领域对象,都有十分复杂的联系,从而没有明显清晰的边界。下面就是一个例子。

假设我们从数据库中删除一个 Person 对象。这个人的姓名、出生日期和工作描述要一起被删
除,但要如何处理地址呢?可能还有其他人住在同一地址。如果删除了地址,那些 Person 对象将
会引用一个被删除的对象。如果保留地址,那么垃圾地址在数据库中会累积起来。
在多个客户对相同对象进行并发访问的系统中,这个问题更加突出。当很多用户对系统中的对象进行查询和更新时,必须防止他们同时修改互相依赖的对象。范围错误将导致严重的后果。

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

换句话说,我们如何知道一个由其他对象组成的对象从哪里开始,又到何处结束呢?在任何具有持久化数据存储的系统中,对数据进行修改的事务必须要有范围,而且要有保持数据一致性的方式(也就是说,保持数据遵守固定规则)。数据库支持各种锁机制,而且可以编写一些测试来验证。但这些特殊的解决方案分散了人们对模型的注意力,很快人们就会回到“走一步,看一步”的老路上来。

实际上,要想找到一种兼顾各种问题的解决方案,要求对领域有深刻的理解,例如,要了解特定类实例之间的更改频率这样的深层次因素。我们需要找到一个使对象间冲突较少而固定规则联系更紧密的模型。

尽管从表面上看这个问题是数据库事务方面的一个技术难题,但它的根源却在模型,归根结底是由于模型中缺乏明确定义的边界。从模型得到的解决方案将使模型更易于理解,并且使设计更易于沟通。当模型被修改时,它将引导我们对实现做出修改。

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

汽车修配厂的软件可能会使用汽车模型。如图 6-2 所示。汽车是一个具有全局标识的 ENTITY:

我们需要将这部汽车与世界上所有其他汽车区分开(即使是一些非常相似的汽车)。我们可以使用车辆识别号来进行区分,车辆识别号是为每辆新汽车分配的唯一标识符。我们可能想通过 4 个轮子的位臵跟踪轮胎的转动历史。我们可能想知道每个轮胎的里程数和磨损度。要想知道哪个轮胎在哪儿,必须将轮胎标识为 ENTITY。当脱离这辆车的上下文后,我们很可能就不再关心这些轮胎的标识了。如果更换了轮胎并将旧轮胎送到回收厂,那么软件将不再需要跟踪它们,它们会成
为一堆废旧轮胎中的一部分。没有人会关心它们的转动历史。更重要的是,即使轮胎被安在汽车上,也不会有人通过系统查询特定的轮胎,然后看看这个轮胎在哪辆汽车上。人们只会在数据库中查找汽车,然后临时查看一下这部汽车的轮胎情况。因此,汽车是 AGGREGATE 的根 ENTITY,而轮胎处于这个 AGGREGATE 的边界之内。另一方面,发动机组上面都刻有序列号,而且有时是独立于汽车被跟踪的。在一些应用程序中,发动机可以是自己的 AGGREGATE 的根。

未命名-15.png

固定规则,是指在数据变化时必须保持一致的规则,其涉及 aggregate 的内部关系。不要求实时满足规则,只需要在每次事务结束后满足这种规则。例如,一般来说,四轮骑车都需要满足四个轮子,可能在更换轮胎时,会短暂的处于三个轮胎的状态。但在更换轮胎后就应该满足四个轮胎。
现在,为了实现这个概念上的 AGGREGATE,需要对所有事务应用一组规则。
 根 ENTITY 具有全局标识,它最终负责检查固定规则。
 根 ENTITY 具有全局标识。边界内的 ENTITY 具有本地标识,这些标识只在 AGGREGATE 内部才是唯一的。
 AGGREGATE 外部的对象不能引用除根 ENTITY 之外的任何内部对象。根 ENTITY 可以把对内部 ENTITY 的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个 VALUE OBJECT 的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个 VALUE,不再与 AGGREGATE 有任何关联。
 作为上一条规则的推论,只有 AGGREGATE 的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。
 AGGREGATE 内部的对象可以保持对其他 AGGREGATE 根的引用。
 删除操作必须一次删除 AGGREGATE 边界之内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。)
 当提交对 AGGREGATE 边界内部的任何对象的修改时,整个 AGGREGATE 的所有固定规则都必须被满足。

未命名-16.png

未命名-17.png
图 6-4 展示了一个典型的采购订单(Purchase Order, PO)视图,它被分解为采购项(Line Item),
一条固定规则是采购项的总量不能超过 PO 总额的限制。当前实现存在以下 3 个互相关联的问题。
(1) 固定规则的实施。当添加新采购项时, PO 检查总额,如果新增的采购项使总额超出限制,则将 PO 标记为无效。正如我们将要看到的那样,这种保护机制并不充分。
(2) 变更管理。当 PO 被删除或存档时,各个采购项也将被一块处理,但模型并没有给出关系应该在何处停止。在不同时间更改部件(Part)价格所产生的影响也不明确。
(3) 数据库共享。数据库会出现由于多个用户竞争使用而带来的问题。
400

多个用户将并发地输入和更新各个 PO,因此必须防止他们互相干扰。
如果不锁定 PO 数据库,那么两个人的修改就会破坏固定规则。如,一个人修改第一项的数量,一个人修改第二项的数量。

即便是很多小 PO,也存在其他方法破坏这条固定规则。让我们看看“Part”。
如果在 Amanda 将长号加入订单时,有人更改了长号的价格,这不也会破坏固定规则吗?
那么,我们试着除了锁定整个 PO 之外,也锁定 Part。
工作变得越来越麻烦,因为在 Part 上出现了很多争用的情况。

现在我们可以开始改进模型,在模型中加入以下业务知识。
(1) Part 在很多 PO 中使用(会产生高竞争)。
(2) 对 Part 的修改少于对 PO 的修改。
(3) 对 Price(价格)的修改不一定要传播到现有 PO,它取决于修改价格时 PO 处于什么状态。

当考虑已经交货并存档的 PO 时,第三点尤为明显。它们显示的当然是填写时的价格,而不是当前价格。

400

这个模型得到的实现可以确保满足 PO 和采购项相关的固定规则,同时,修改部件的价格将不会立即影响引用部件的采购项。涉及面更广的规则可以通过其他方式来满足。例如,系统可以每天为用户列出价格过期的采购项,这样用户就可以决定是更新还是去掉采购项。

(找到变化的内容,判断变化之间的联系是如何的,比如这里 part 和 purchase 没有过多联系。需求部门需要 10 个 mac 电脑,采购部门去采购这 10 个电脑。需求部门不需要关注电脑的价格,只有一个需求预算。采购部门基于预算去判断预算是否合适。)

实际上,只有执行采购的时候,才需要关注 Part 的价格是多少,其余时间并不关注。

即,并非一定要一直保持的固定规则。通过减少采购项对 Part 的依赖,可以避免争用,并且能够更好地反映出业务的现实情况。同时,加强 PO 与采购项之间的关系可以确保遵守这条重要的业务规则。

AGGREGATE 强制了 PO 与采购项之间符合业务实际的所属关系。PO 和采购项的创建及删除很自然地被联系在一起,而 Part 的创建和删除却是独立的。

Factory

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

一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。例如生产一个发动机需要各种复杂的机械装置。

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

(1) 每个创建方法都是原子的,而且要保证被创建对象或 AGGREGATE 的所有固定规则。FACTORY 生成的对象要处于一致的状态。在生成 ENTITY 时,这意味着创建满足所有固定规则的整个 AGGREGATE,但在创建完成后可以向聚合添加可选元素。在创建不变的 VALUE OBJECT 时,这意味着所有属性必须被初始化为正确的最终状态。如果 FACTORY 通过其接口收到了一个创建对象的请求,而它又无法正确地创建出这个对象,那么它应该抛出一个异常,或者采用其他机制,以确保不会返回错误的值。

(2) FACTORY 应该被抽象为所需的类型,而不是所要创建的具体类。[Gamma et al. 1995]中的高级 FACTORY 模式介绍了这一话题。

选择 FACTORY 及其应用位置

Factory 两种 —— AGGREGATE Factory & Entity/Value Object Factory。
整个 AGGREGATE 通常由一个独立的 FACTORY 来创建,FACTORY 负责把对根的引用传递出去,并确保创建出的 AGGREGATE 满足固定规则。如果 AGGREGATE 内部的某个对象需要一个 FACTORY,而这个 FACTORY 又不适合在 AGGREGATE 根上创建,那么应该构建一个独立的 FACTORY。

但是,在有些情况下直接使用构造函数确实是最佳选择。FACTORY 实际上会使那些不具有多态性的简单对象复杂化。

在以下情况下最好使用简单的、公共的构造函数。

 类(class)是一种类型(type)。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态性。
 客户关心的是实现,可能是将其作为选择 STRATEGY 的一种方式。
 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。
 构造并不复杂。
 公共构造函数必须遵守与 FACTORY 相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。

115 页

接口的设计;

  • 每个操作都必须是原子的。我们必须在与 FACTORY 的一次交互中把创建对象所需的所有信息传递给 FACTORY。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。可以抛出一个异常或仅仅返回 null。为了保持一致,可以考虑采用编码标准来处理所有 FACTORY 的失败。
  • Factory 将与其参数发生耦合。如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数(argument)的处理。如果只是简单地将参数插入到要构建的对象中,则依赖度是适中的。如果从参数中选出一部分在构造对象时使用,耦合将更紧密。

最安全的参数是那些来自较低设计层的参数。即使在同一层中,也有一种自然的分层倾向,其中更基本的对象被更高层的对象使用(第 10 章将从不同方面讨论这样的分层,第 16 章也会论述这个问题)。

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

关于 Aggregate 的固定规则,如果 aggregate 基本上用不上,那么可以放在 factory 中(但不要放在和其他领域对象关联的 factory method 中)。

用于重建对象的 FACTORY 与用于创建对象的 FACTORY 很类似,主要有以下两点不同。
(1)用于重建对象的 ENTITY FACTORY 不分配新的跟踪 ID。
(2) 当固定规则未被满足时,重建对象的 FACTORY 采用不同的方式进行处理。当创建新对象时,如果未满足固定规则,FACTORY 应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。

Repository

我们可以通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个 ENTITY 或 VALUE。

无论要用对象执行什么操作,都需要保持一个对它的引用。那么如何获得这个引用呢?一种方法是创建对象,因为创建操作将返回对新对象的引用。第二种方法是遍历关联。我们以一个已知对象作为起点,并向它请求一个关联的对象。

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

第七章-货运示例(好)(todo)

第三部分-通过重构来加深理解

《重构》[Fowler 1999]一书中所列出的重构分类涵盖了大部分常用的代码细节重构。这些重
构主要是为了解决一些可以从代码中观察到的问题。相比之下,领域模型会随着新认识的出现而
不断变化,由于其变化如此多样,以至于根本无法整理出一个完整的目录。

例如,我曾参与过一个运输应用系统的开发,我的初始想法是构建一个包括货轮(ship)和
集装箱的对象模型。货轮将货物从一个地点运送到另一个地点。集装箱则通过装卸操作与货轮建
立关联或解除关联。这确实能够准确描述一部分实际运输活动。但事实证明,它对于运输业务的
软件实现并没有太多帮助。
最终,在与运输专家一起工作了几个月并进行了多次迭代后, 我们得到了一个完全不同的模
型。在外行人看来,它也许没那么浅显易懂,但却能贴切地反映出专家的想法。这个模型的关注
点再次回到了运送货物的业务。
我们依然保留了 ship,但是将其抽象为“船只航次”(vessel voyage) ,即货轮、火车或其他
运输工具的某一调度好的航程。货轮本身不再重要,如遇维修或计划变动可临时改用其他方式,
只要保证原定航次按计划执行即可。运输集装箱则完全从模型中移除了。它现在以一种完全不同
的复杂形式出现在货物装卸应用程序中,而在原来的应用程序中,集装箱变成了操作细节。货物
实际的位臵变化已不重要,重要的是其法律责任的转移。原来一些诸如“提货单”之类不被关注
的对象也出现在模型中。
每当有新的对象建模人员加入这个项目时,他们首先提出的建议是什么?就是添加“货轮”
和“集装箱”这两个缺少的类。他们都很聪明,只不过还没有仔细揣摩运输领域的知识罢了。

这种模型的共同之处在于:它们提供了一种业务专家青睐的简单语言,尽管这种语言可能也是抽象的。

第九章-将隐式概念转变为显式概念

深层建模的第一步就是要设法在模型中表达出领域的基本概念。随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。

要挖掘出大部分的隐含概念,需要开发人员去倾听团队语言、仔细检查设计中的不足之处以及与专家观点相矛盾的地方、研究领域相关文献并且进行大量的实验。

后面略。