阅读-领域驱动设计第六章-领域对象的生命周期

每个对象都有生命周期,如图6-1所示。对象自创建后,可能会经历各种不同的状态,直至最终消亡——要么存档,要么删除。

很多对象是简单的临时对象,仅通过调用构造函数来创建,用来做一些计算,而后由垃圾收集器回收。这类对象没必要搞得那么复杂。但有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的。它们与其他对象具有复杂的相互依赖性。它们会经历一些状态变化,在变化时要遵守一些固定规则。管理这些对象时面临诸多挑战,稍有不慎就会偏离MODEL-DRIVEN DESIGN的轨道。

领域对象的生命周期

主要的挑战有以下两类:
(1) 在整个生命周期中维护完整性。
(2) 防止模型陷入管理生命周期复杂性造成的困境当中。

本章将通过3种模式解决这些问题。

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

接下来,我们将注意力转移到生命周期的开始阶段,使用FACTORY(工厂)来创建和重建复杂对象和AGGREGATE(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。

尽管REPOSITORY和FACTORY本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。这些结构提供了易于掌握的模型对象处理方式,使MODEL-DRIVEN DESIGN更完备。

使用AGGREGATE进行建模,并且在设计中结合使用FACTORY和REPOSITORY,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。AGGREGATE可以划分出一个范围,这个范围内的模型元素在生命周期各个阶段都应该维护其固定规则。FACTORY和 REPOSITORY在AGGREGATE基础上进行操作,将特定生命周期转换的复杂性封装起来。

一、聚合模式AGGREGATE

减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。但大多数业务领域中的对象都具有十分复杂的联系,以至于最终会形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。在某种程度上,这种混乱状态反映了现实世界,因为现实世界中就很少有清晰的边界。但这却是软件设计中的一个重要问题。

假设我们从数据库中删除一个Person对象。这个人的姓名、出生日期和工作描述要一起被删除,但要如何处理地址呢?可能还有其他人住在同一地址。如果删除了地址,那些Person对象将会引用一个被删除的对象。如果保留地址,那么垃圾地址在数据库中会累积起来。虽然自动垃圾收集机制可以清除垃圾地址,但这也只是一种技术上的修复;就算数据库系统存在这种处理机制,一个基本的建模问题依然被忽略了。

在多个客户对相同对象进行并发访问的系统中,这个问题更加突出。当很多用户对系统中的对象进行查询和更新时,必须防止他们同时修改互相依赖的对象。范围错误将导致严重的后果。

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

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

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

人们已经开发出很多模式(scheme)来定义模型中的所属关系。下面这个简单但严格的系统就提炼自这些概念,其包括一组用于实现事务(这些事务用来修改对象及其所有者)的规则①。

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

示例:

汽车修配厂的软件可能会使用汽车模型。如图6-2所示。汽车是一个具有全局标识的ENTITY:我们需要将这部汽车与世界上所有其他汽车区分开(即使是一些非常相似的汽车)。我们可以使用车辆识别号来进行区分,车辆识别号是为每辆新汽车分配的唯一标识符。

我们可能想通过4个轮子的位臵跟踪轮胎的转动历史。我们可能想知道每个轮胎的里程数和磨损度。要想知道哪个轮胎在哪儿,必须将轮胎标识为ENTITY。当脱离这辆车的上下文后,我们很可能就不再关心这些轮胎的标识了。如果更换了轮胎并将旧轮胎送到回收厂,那么软件将不再需要跟踪它们,它们会成为一堆废旧轮胎中的一部分。没有人会关心它们的转动历史。更重要的是,即使轮胎被安在汽车上,也不会有人通过系统查询特定的轮胎,然后看看这个轮胎在哪辆汽车上。人们只会在数据库中查找汽车,然后临时查看一下这部汽车的轮胎情况。

因此,汽车是AGGREGATE的根ENTITY,而轮胎处于这个AGGREGATE的边界之内。另一方面,发动机组上面都刻有序列号,而且有时是独立于汽车被跟踪的。在一些应用程序中,发动机可以是自己的AGGREGATE的根。



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

现在,为了实现这个概念上的AGGREGATE,需要对所有事务应用一组规则。
 根ENTITY具有全局标识,它最终负责检查固定规则。

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

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

 作为上一条规则的推论,只有AGGREGATE的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。

 AGGREGATE内部的对象可以保持对其他AGGREGATE根的引用。

 删除操作必须一次删除AGGREGATE边界之内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。)

 当提交对AGGREGATE边界内部的任何对象的修改时,整个AGGREGATE的所有固定规则都必须被满足。
我们只对最后一条做进一步的解释,其他的相信大家都能理解。
如图所示:



我们应该将 ENTITY和 VALUE OBJECT分门别类地聚集到AGGREGATE中,并定义每个AGGREGATE的边界.在每AGGREGATE中,选择一个ENTITY作为根,并通过根来控制对边界内其他对象所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。

这种设计有利于确保AGGREGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个整体满足固定规则。

有一个能够声明AGGREGATE的技术框架是很有帮助的,这样就可以自动实施锁机制和其他一些功能。如果没有这样的技术框架,团队就必须靠自我约束来使用事先商定的AGGREGATE,并按照这些AGGREGATE来编写代码。

示例采购订单系统


图6-4展示了一个典型的采购订单(Purchase Order,PO)视图,它被分解为采购项(Line Item),一条固定规则是采购项的总量不能超过PO总额的限制。当前实现存在以下3个互相关联的问题。

(1) 固定规则的实施。当添加新采购项时,PO检查总额,如果新增的采购项使总额超出限制,则将PO标记为无效。正如我们将要看到的那样,这种保护机制并不充分。

(2) 变更管理。当PO被删除或存档时,各个采购项也将被一块处理,但模型并没有给出关系应该在何处停止。在不同时间更改部件(Part)价格所产生的影响也不明确。

(3)数据库共享。数据库会出现由于多个用户竞争使用而带来的问题。

多个用户将并发地输入和更新各个PO,因此必须防止他们互相干扰。让我们从一个非常简单的策略开始,当一个用户开始编辑任何一个对象时,锁定该对象,直到用户提交事务。这样,当George编辑采购项001时,Amanda就无法访问该项。Amanda可以编辑其他PO上的任何采购项(包括George正在编辑的PO上的其他采购项).

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

接下来要讨论的两种模式FACTORY和REPOSITORY都是在AGGREGATE上执行操作,它们将特定生命周期转换的复杂性封装起来……

二、模式:FACTORY

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

对象的功能主要体现在其复杂的内部配臵以及关联方面。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。

复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。在有些情况下,对象的创建和装配对应于领域中的重要事件,如“开立银行账户”。但一般情况下,对象的创建和装配在领域中并没有什么意义,它们只不过是实现的一种需要。为了解决这一问题,我们必须在领域设计中增加一种新的构造,它不是ENTITY、VALUE OBJECT,也不是SERVICE。这与前一章的论述相违背,因此把它解释清楚很重要。我们正在向设计中添加一些新元素,但它们不对应于模型中的任何事物,而确实又承担领域层的部分职责。

每种面向对象的语言都提供一种创建对象的机制(例如,Java和C++中的构造函数,Smalltalk中创建实例的类方法)但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是FACTORY,它是一种负责创建其他对象的程序元素。



正如对象的接口应该封装对象的实现一样(从而使客户无需知道对象的工作机理就可以使用对象的功能),FACTORY封装了创建复杂对象或AGGREGATE所需的知识。它提供了反映客户目标的接口,以及被创建对象的抽象视图。

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

FACTORY有很多种设计方式。[Gamma et al. 1995]中详尽论述了几种特定目的的创建模式,包括FACTORY METHOD(工厂方法)、ABSTRACT FACTORY(抽象工厂)和BUILDER(构建器)。该书主要研究了适用于最复杂的对象构造问题的模式。

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

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

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

二、1.选择FACTORY及其应用位置

FACTORY的作用是隐藏创建对象的细节,且我们把FACTORY用在那些需要隐藏细节的地方。这些决定通常与AGGREGATE有关。

例如,如果需要向一个已存在的AGGREGATE添加元素,可以在AGGREGATE的根上创建一个FACTORY METHOD。这样就可以把AGGREGATE的内部实现细节隐藏起来,使任何外部客户看不到这些细节,同时使根负责确保AGGREGATE在添加元素时的完整性。
如图6-13所示 一个FACTORY METHOD封装了AGGREGATE的扩展

另一个示例是在一个对象上使用FACTORY METHOD,这个对象与生成另一个对象密切相关,但它并不拥有所生成的对象。当一个对象的创建主要使用另一个对象的数据(或许还有规则)时,则可以在后者的对象上创建一个FACTORY METHOD,这样就不必将后者的信息提取到其他地方来创建前者。这样做还有利于表达前者与后者之间的关系。

Trade Order不属于Brokerage Account所在的AGGREGATE,因为它从一开始就与交易执行应用程序进行交互,所以把它放在Brokerage Account中只会碍事。尽管如此,让Brokerage Account负责控制Trade Order的创建却是很自然的事情。

Brokerage Account含有会被嵌入到Trade Order中的信息(从自己的标识开始),而且它还包含与交易相关的规则——这些规则控制了哪些交易是允许的。隐藏Trade Order的实现细节还会带来一些其他好处。例如,我们可以将它重构为一个层次结构,分别为Buy Order和Sell Order创建一些子类。FACTORY可以避免客户与具体类之间产生耦合。

FACTORY与被构建对象之间是紧密耦合的,因此FACTORY应该只被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏(无论要隐藏的是具体实现还是构造的复杂性)而又找不到合适的地方来隐藏它们时,必须创建一个专用的FACTORY对象或SERVICE。整个AGGREGATE通常由一个独立的FACTORY来创建,FACTORY负责把对根的引用传递出去,并确保创建出的AGGREGATE满足固定规则。如果AGGREGATE内部的某个对象需要一个FACTORY,而这个FACTORY又不适合在AGGREGATE根上创建,那么应该构建一个独立的FACTORY。但仍应遵守规则——把访问限制在AGGREGATE内部,并确保从AGGREGATE外部只能对被构建对象进行临时引用。


二、2、有些情况下只需使用构造函数

FACTORY的引入提供了巨大的优势,而这种优势往往并未得到充分利用。但是,在有些情况下直接使用构造函数确实是最佳选择。FACTORY实际上会使那些不具有多态性的简单对象复杂化。

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

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

不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是AGGREGATE,需要使用FACTORY。使用FACTORY METHOD的门槛并高。

Java类库提供了一些有趣的例子。所有集合都实现了接口,接口使得客户与具体实现之间不产生耦合。然而,它们都是通过直接调用构造函数创建的。但是,集合类本来是可以使用FACTORY来封装集合的层次结构的。而且,客户也可以使用FACTORY的方法来请求所需的特性,然后由FACTORY来选择适当的类来实例化。这样一来,创建集合的代码就会有更强的表达力,而且新增集合类时不会破坏现有的Java程序。

但在某些场合下使用具体的构造函数更为合适。首先,在很多应用程序中,实现方式的选择对性能的影响是非常敏感的,因此应用程序需要控制选择哪种实现。不管怎样,集合类的数量并不多,因此选择并不复杂。

虽然没有使用FACTORY,但抽象集合类型仍然具有一定价值,原因就在于它们的使用模式。集合通常都是在一个地方创建,而在其他地方使用。这意味着最终使用集合(添加、删除和检索其内容)的客户仍可以与接口进行对话,从而不与实现发生耦合。集合类的选择通常由拥有该集合的对象来决定,或是由该对象的FACTORY来决定。

二、 3 接口的设计

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

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

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

另一个好的参数选择是模型中与被构建对象密切相关的对象,这样不会增加新的依赖。在前面的Purchase Order Item示例中,FACTORY METHOD将Catalog Part作为一个参数,它是Item的一个重要的关联。这在Purchase Order类和Part之间增加了直接依赖。但这3个对象组成了一个关系密切的概念小组。不管怎样,Purchase Order的AGGREGATE已经引用了Part。因此将控制权交给AGGREGATE根,并封装AGGREGATE的内部结构是一个不错的折中选择。

使用抽象类型的参数,而不是它们的具体类。FACTORY与被构建对象的具体类发生耦合,而无需与具体的参数发生耦合。

二、 4、 固定规则的相关逻辑应放置在哪里

FACTORY负责确保它所创建的对象或AGGREGATE满足所有固定规则,然而在把应用于一个对象的规则移到该对象外部之前应三思。FACTORY可以将固定规则的检查工作委派给被创建对象,而且这通常是最佳选择。

在某些情况下,把固定规则的相关逻辑放到FACTORY中是有好处的,这样可以让被创建对象的职责更明晰。对于AGGREGATE规则来说尤其如此(这些规则会约束很多对象)。但固定规则的相关逻辑却特别不适合放到那些与其他领域对象关联的FACTORY METHOD中。

虽然原则上在每个操作结束时都应该应用固定规则,但通常对象所允许的转换可能永远也不会用到这些规则。可能ENTITY标识属性的赋值需要满足一条固定规则。但该标识在创建后可能一直保持不变。VALUE OBJECT则是完全不变的。如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没有必要携带这个逻辑。在这种情况下,FACTORY是放臵固定规则的合适地方,这样可以使FACTORY创建出的对象更简单。

二、 5、 ENTITY FACTORY与VALUE OBJECT FACTORY

ENTITY FACTORY与VALUE OBJECT FACTORY有两个方面的不同。由于VALUE OBJECT是不可变的,因此,FACTORY所生成的对象就是最终形式。因此FACTORY操作必须得到被创建对象的完整
描述。而ENTITY FACTORY则只需具有构造有效AGGREGATE所需的那些属性。对于固定规则不关心的细节,可以之后再添加。

二、6 重建已存储的对象

到目前为止,FACTORY只是发挥了它在对象生命周期开始时的作用。到了某一时刻,大部分对象都要存储在数据库中或通过网络传输,而在当前的数据库技术中,几乎没有哪种技术能够保持对象的内容特征。

大多数传输方法都要将对象转换为平面数据才能传输,这使得对象只能以非常有限的形式出现。因此,检索操作潜在地需要一个复杂的过程将各个部分重新装配成一个可用的对象。

用于重建对象的FACTORY与用于创建对象的FACTORY很类似,主要有以下两点不同:

(1)用于重建对象的ENTITY FACTORY不分配新的跟踪ID。如果重新分配ID,将丢失与先前对象的连续性。因此在重建对象的FACTORY中,标识属性必须是输入参数的一部分。

(2) 当固定规则未被满足时,重建对象的FACTORY采用不同的方式进行处理。当创建新对象时,如果未满足固定规则,FACTORY应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。
比如如果对象已经在系统的某个地方存在(如在数据库中),那么不能忽略这个事实。但是同样也不能任凭规则被破坏。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建新对象更困难。

图6-16和图6-17显示了两种重建


当从数据库中重建对象时,对象映射技术就可以提供部分或全部所需服务,这是非常便利的。当从其他介质重建对象时,如果出现复杂情况,FACTORY是个很好的选择。


总之,必须把创建实例的访问点标识出来,并显式地定义它们的范围。它们可能只是构造函数,但通常需要有一种更抽象或更复杂的实例创建机制。为了满足这种需求,需要在设计中引入新的构造——FACTORY。

FACTORY通常不表示模型的任何部分,但它们是领域设计的一部分,能够使对象更明确地表示出模型。FACTORY封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这种转换由另一种领域设计构造来处理,它就是REPOSITORY。

三、模式:REPOSITORY

从技术的观点来看,检索已存储对象实际上属于创建对象的范畴,因为从数据库中检索出来的数据要被用来组装新的对象。实际上,由于需要经常编写这样的代码,我们对此形成了根深蒂固的观念。但从概念上讲,对象检索发生在ENTITY生命周期的中间。不能只是因为我们将Customer对象保存在数据库中,而后把它检索出来,这个Customer就代表了一个新客户。为了记住这个区别,我把使用已存储的数据创建实例的过程称为重建。

领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。假设开发人员构造了一个SQL查询,并将它传递给基础设施层中的某个查询服务,然后再根据得到的表行数据的结果集提取出所需信息,最后将这些信息传递给构造函数或FACTORY。开发人员执行这一连串操作的时候,早已不再把模型当作重点了。

我们很自然地会把对象看作容器来放臵查询出来的数据,这样整个设计就转向了数据处理风格。虽然具体的技术细节有所不同,但问题仍然存在——客户处理的是技术,而不是模型概念。诸如METADATAMAPPING LAYER[Fowler 2002]这样的基础设施可以提供很大帮助,利用它很容易将查询结果转换为对象,但开发人员考虑的仍然是技术机制,而不是领域。更糟的是,当客户代码直接使用数据库时,开发人员会试图绕过模型的功能(如AGGREGATE,甚至是对象封装),而直接获取和操作他们所需的数据。这将导致越来越
多的领域规则被嵌入到查询代码中,或者干脆丢失了。虽然对象数据库消除了转换问题,但搜索机制还是很机械的,开发人员仍倾向于要什么就去拿什么。

小结

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

根据到目前为止所讨论的设计原则,如果我们找到一种访问方法,它能够明确地将模型作为焦点,从而应用这些原则,那么我们就可以在某种程度上缩小对象访问问题的范围。(初学者可以不必关心临时对象。临时对象(通常是VALUE OBJECT)只存在很短的时间,在客户操作中用到它们时才创建它们,用完就删除了。)我们也不需要对那些很容易通过遍历来找到的持久对象进行查询访问。例如,地址可以通过Person对象获取。

而且最重要的是,除了通过根来遍历查找对象这种方法以外,禁止用其他方法对AGGREGATE内部的任何对象进行访问。

持久化的VALUE OBJECT一般可以通过遍历某个ENTITY来找到,在这里ENTITY就是把对象封装在一起的AGGREGATE的根。事实上,对VALUE的全局搜索访问常常是没有意义的,因为通过属性找到VALUE OBJECT相当于用这些属性创建一个新实例。

持久化的VALUE OBJECT一般可以通过遍历某个ENTITY来找到,在这里ENTITY就是把对象封装在一起的AGGREGATE的根。事实上,对VALUE的全局搜索访问常常是没有意义的,因为通过属性找到VALUE OBJECT相当于用这些属性创建一个新实例。
但也有例外情况。例如,当我在线规划旅行线路时,有时会先保存几个中意的行程,过后再回头从中选择一个来预订。这些行程就是VALUE(如果两个行程由相同的航班构成,那么我不会关心哪个是哪个),但它们已经与我的用户名关联到一起了,而且可以原封不动地将它们检索出来。另一个例子是“枚举”,在枚举中一个类型有一组严格限定的、预定义的可能值。但是,对VALUE OBJECT的全局访问比对ENTITY的全局访问更少见,如果确实需要在数据库中搜索一个已存在的VALUE,那么值得考虑一下,搜索结果可能实际上是一个ENTITY,只是尚未识别它的标识。

小结:

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

有大量的技术可以用来解决数据库访问的技术难题,例如,将SQL封装到QUERY OBJECT中,或利用METADATA MAPPING LAYER(元数据映射层)进行对象和表之间的转换[Fowler 2002]。FACTORY可以帮助重建那些已存储的对象。这些技术和很多其他技术有助于控制数据库访问的复杂度。

  • 我们已经不再考虑领域模型中的概念。代码也不再表达业务,而是对数据库检索技术进行操纵。REPOSITORY是一个简单的概念框架,它可用来封装这些解决方案,并将我们的注意力重新拉回到模型上。

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

客户使用查询方法向REPOSITORY请求对象,这些查询方法根据客户所指定的条件(通常是特定属性的值)来挑选对象。REPOSITORY检索被请求的对象,并封装数据库查询和元数据映射机制。

REPOSITORY可以根据客户所要求的各种条件来挑选对象。它们也可以返回汇总信息,如有多少个实例满足查询条件。REPOSITORY甚至能返回汇总计算,如所有匹配对象的某个数值属性的总和。

REPOSITORY解除了客户的巨大负担,使客户只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。要实现所有这些功能需要大量复杂的技术基础设施,但接口很简单,而且在概念层次上与领域模型紧密联系在一起。

小结:

  • 为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问、添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。

  • 提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。

优点:

REPOSITORY有很多优点,包括:
 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期;
 它们使应用程序和领域设计与持久化技术(多种数据库策略甚至是多个数据源)解耦;
 它们体现了有关对象访问的设计决策;
 可以很容易将它们替换为“哑实现”(dummy implementation),以便在测试中使用(通常使用内存中的集合)。

三、 1 REPOSITORY的查询

所有REPOSITORY都为客户提供了根据某种条件来查询对象的方法,但如何设计这个接口却有很多选择。最容易构建的REPOSITORY用硬编码的方式来实现一些具有特定参数的查询。这些查询可以形式各异,例如,通过标识来检索ENTITY(几乎所有REPOSITORY都提供了这种查询)、通过某个特定属性值或复杂的参数组合来请求一个对象集合、根据值域(如日期范围)来选择对象,甚至可以执行某些属于REPOSITORY一般职责范围内的计算(特别是利用那些底层数据库所支持的操作)。

尽管大多数查询都返回一个对象或对象集合,但返回某些类型的汇总计算也符合REPOSITORY的概念,如对象数目,或模型需要对某个数值属性进行求和统计。

在一些需要执行大量查询的项目上,可以构建一个支持更灵活查询的REPOSITORY框架。如图6-20所示。


即使一个REPOSITORY的设计采取了灵活的查询方式,也应该允许添加专门的硬编码查询。
这些查询作为便捷的方法,可以封装常用查询或不返回对象(如返回的是选中对象的汇总计算)的查询。不支持这些特殊查询方式的框架有可能会扭曲领域设计,或是干脆被开发人员弃之不用。

三、3 REPOSITORY的实现

根据所使用的持久化技术和基础设施不同,REPOSITORY实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。REPOSITORY将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是REPOSITORY实现的最基本的特性
如图6-21所示。


REPOSITORY概念在很多情况下都适用。可能的实现方法有很多,这里只能列出如下一些需要谨记的注意事项。
 对类型进行抽象。REPOSITORY“含有”特定类型的所有实例,但这并不意味着每个类都需要有一个REPOSITORY。类型可以是一个层次结构中的抽象超类.例如TradeOrder可以是BuyOrder或SellOrder)。类型可以是一个接口——接口的实现者并没有层次结构上的关联,也可以是一个具体类。记住,由于数据库技术缺乏这样的多态性质,因此我们将面临很多约束。

 充分利用与客户解耦的优点。可以利用解耦来优化性能,因为这样就可以使用不同的查询技术,或在内存中缓存对象,可以随时自由地切换持久化策略。通过提供一个易于操纵的、内存中的(in-memory)哑实现,还能够方便客户代码和领域对象的测试。

 将事务的控制权留给客户。尽管REPOSITORY会执行数据库的插入和删除操作,但它通常不会提交事务。例如,保存数据后紧接着就提交似乎是很自然的事情,但想必只有客户才有上下文,从而能够正确地初始化和提交工作单元。如果REPOSITORY不插手事务控制,
那么事务管理就会简单得多。

通常,项目团队会在基础设施层中添加框架,用来支持REPOSITORY的实现。REPOSITORY超类除了与较低层的基础设施组件进行协作以外,还可以实现一些基本查询,特别是要实现的灵活查询时。遗憾的是,对于类似Java这样的类型系统,这种方法会使返回的对象只能是Object类型,而让客户将它们转换为REPOSITORY含有的类型。当然,如果在Java中查询所返回的对象是集合时,客户不管怎样都要执行这样的转换。

三、 4、 在框架内工作

在实现REPOSITORY这样的构造之前,需要认真思考所使用的基础设施,特别是架构框架。这些框架可能提供了一些可用来轻松创建REPOSITORY的服务,但也可能会妨碍创建REPOSITORY的工作。我们可能会发现架构框架已经定义了一种用来获取持久化对象的等效模式,也有可能定义了一种与REPOSITORY完全不同的模式。

例如,你的项目可能会使用J2EE。看看这个框架与MODEL-DRIVEN DESIGN的模式之间有哪些概念上近似的地方(记住,实体bean与ENTITY不是一回事),你可能会把实体bean和AGGREGATE根当作一对类似的概念。在J2EE框架中,负责对这些对象进行访问的构造是EJB Home。但如果把EJB Home装饰成REPOSITORY的样子可能会导致其他问题。

一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。寻求领域驱动设计的概念与框架中的概念之间的相似性。这里的假设是除了使用指定框架之外没有别的选择。很多J2EE项目根本不使用实体bean。如果可以自由选择,那么应该选择与你所使用的设计风格相协调的框架或框架中的一些部分。

三、 5、REPOSITORY与FACTORY的关系

FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。从领域驱动设计的角度来看,FACTORY
和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。

REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建(的确,可能会创建一个新实例),但它是同一个概念对象,仍旧处于生命周期的中间。REPOSITORY也可以委托FACTORY来创建一个对象,这种方法(虽然实际很少这样做,但在理论上是可行的)可用于从头开始创建对象,此时就没有必要区分这两种看问题的角度了。


图6-22 REPOSITORY使用FACTORY来重建一个已有对象

这种职责上的明确区分还有助于FACTORY摆脱所有持久化职责。FACTORY的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道在创建完成之后应该把它添加到REPOSITORY中,由REPOSITORY来封装对象在数据库中的存储。如图6-23所示:


如图6-23所示

另一种情况促使人们将FACTORY和REPOSITORY结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。我们最好不要追求这种功能,它不会带来多少方便。当将ENTITY和VALUE OBJECT区分开时,很多看上去有用的功能就不复存在了。需要VALUE OBJECT的客户可以直接请求FACTORY来创建一个。通常,在领域中将新对象和原有对象区分开是很重要的,而将它们组合在一起的框架实际上只会使局面变得混乱。

四、为关系数据库设计对象

在以面向对象技术为主的软件系统中,最常用的非对象组件就是关系数据库。这种现状产生了混合使用范式的常见问题(参见第5章)。但与大部分其他组件相比,数据库与对象模型的关系要紧密得多。数据库不仅仅与对象进行交互,而且它还把构成对象的数据存储为持久化形式。已经有大量的文献对于如何将对象映射到关系表以及如何有效存储和检索它们这样的技术挑战进行了讨论。

有一些相当完善的工具可用来创建和管理它们之间的映射。除了技术上的难点以外,这种不匹配可能对对象模型产生很大的影响。
有3种常见情况:

(1) 数据库是对象的主要存储库;
(2) 数据库是为另一个系统设计的;
(3) 数据库是为这个系统设计的,但它的任务不是用于存储对象;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容