3. 领域对象的生命周期
每个对象都有生命周期,如下图所示。对象自创建后,可能会经历各种不同的状态,直至最终消亡——要么存档,要么删除。当然很多对象是简单的临时对象,仅通过调用构造函数来创建,用来做一些计算,然后由垃圾收集器回收。这类对象没必要搞得那么复杂。但有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的。它们与其他对象具有复杂的相互依赖性。它们会经历一些状态变化,在变化时要遵守一些固定规则。管理这些对象时面临诸多挑战,稍有不慎就会偏离 Model-Driven Design 的轨道。
主要的挑战有以下两类。
(1)在整个生命周期中维护完整性。
(2)防止模型陷入管理生命周期复杂性造成的困境当中。
下面通过三种模式解决这些问题。首先是 Aggregate(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。
接下来,我们讲注意力转移到生命周期的开始阶段,使用 Factory(工厂)来创建和重建复杂对象和 Aggregate(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用 Repository(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
尽管 Repository 和 Factory 本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。这些结构提供了易于掌握的模型对象处理方式,使 Model- Driven Design 更完备。
使用 Aggregate 进行建模,并且在设计中结合使用 Factory 和 Repository ,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。Aggregate 可以划分出一个范围,这个范围内的模型元素在生命周期各个阶段都应该维护其固定规则。Factory 和 Repository 在 Aggregate 基础上进行操作,将特定生命周期的复杂封装起来。
3.1 模式:Aggregate
减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。但大多数业务领域中的对象都具有十分复杂的联系,以至于最终形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。在某种程度上,这种混乱状态反映了现实世界,因为现实世界中就很少有清晰的边界。但这却是软件设计中的一个重要问题。
假设我们从数据库中删除一个 Person 对象。这个人的姓名、出生日期和工作描述要一起被删除,但要如何处理地址呢?可能还有其他人住在同一地址。如果删除了地址,那些 Person 对象将会引用一个被删除的对象。如果保留地址,那么垃圾地址在数据库中会积累起来。虽然自动垃圾收集机制可以清除垃圾地址,但这也是一种技术上的修复;就算数据库系统存在这种处理机制,一个基本的建模问题依然被忽略。
即便是在考虑孤立的事务时,典型对象模型中的关系网也使我们难以断定一个修改会产生哪些潜在的影响。仅仅因为存在依赖就更新系统的每个对象,这样做是不现实的。
在多个客户对相同对象进行并发访问的系统中,这个问题更加突出。当很多用户对系统中的对象进行查询和更新时,必须防止他们同时修改互相依赖的对象。范围错误将导致严重的后果。
在具有复杂关联的模型中,要保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然而,过于谨慎的锁定机制会导致多个用户之间毫无意义地相互干扰,从而使系统不可用。
换句话说,我们如何知道一个由其他对象组成的对象从哪里开始,又到何处结束呢?在任何具有持久化数据存储的系统中,对数据进行修改的事务必须要有范围,而且要保持数据一致性的方式(也就是说,保持数据遵守固定规则)。数据库支持各种锁机制,而且可以编写一些测试来验证。但这些特殊的解决方案分散了人们对模型的注意力,很快人们就会回到 “走一步,看一步” 的老路上来。
实际上,要找到一种兼顾各种问题的解决方案,要求对领域有深刻的理解,例如,要了解特定类实例之间的更改频率这样的深层次因素。我们需要找到一个使对象间冲突较少而固定规则联系更紧密的模型。
尽管从表面上看这个问题是数据库事务方面的一个技术难题,但它的根源却在模型,归根结底是由于模型中缺乏明确定义的边界。从模型得到的解决方案将使模型更易于理解,并且使设计更易于沟通。当模型被修改时,它将引导我们对实现做出修改。
人们已经开发出很多模式(scheme)来定义模型中的所属关系。
首先,我们需要用一个抽象来封装模型中的引用。Aggregate 就是一组相关对象的集合,我们把它当作数据修改的单元。每个 Aggregate 都有一个根(root)和一个边界(boundary)。边界定义了 Aggregate 的内部都有什么。根则是 Aggreg ate 所包含的一个特定 Entity。对 Aggregate 而言,外部对象只可以引用根,而边界内部的对象之间则可以相互引用。除根以外的其他 Entity 都有本地标识,但这些标识只在 Aggregate 内部才需要加以区别,因为外部对象除了根 Entity 之外看不到其他对象。
固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及 Aggregate 成员之间的内部关系。而任何跨越 Aggregate 的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定时间内得以解决。但在每个事务完成时,Aggregate 内部所应用的固定规则必须得到满足,
现在,为了实现这个概念上的 Aggregate ,需要对所有事务应用一组规则。
(1)根 Entity 具有全局标识,它最终负责检查固定规则。
(2)根 Entity 具有全局标识。边界内的 Entity 具有本地标识,这些标识只在 Aggregate 内部才是唯一的。
(3)Aggregate 外部的对象不能引用除根 Entity 之外的任何内部对象。根 Entity 可以把内部 Entity 的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个 Value Object 的副本传递给另一个对象,而不关心它发生了什么变化,因为它只是一个 Value,不再与 Aggregate 有任何关联。
(4)作为上一条规则的推论,只有 Aggregate 的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。
(5)Aggregate 内部的对象可以保持对其他 Aggregate 根的引用。
(6)删除操作必须一次删除 Aggregate 边界内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收)
(7)当提交对 Aggregate 边界内部的任何对象的修改时,整个 Aggregate 的所有固定规则必须被满足。
我们应该将 Entity 和 Value Object 分门别类地聚集到 Aggregate 中,并定义每个 Aggregate 的边界。在每个 Aggregate 中,选择一个 Entity 作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保 Aggregate 中的对象满足所有固定规则,也可以确保在任何状态变化时 Aggregate 作为一个整体满足固定规则。
有一个能够声明 Aggregate 的技术框架是很有帮助的,这样就可以自动实施锁机制和其他一些功能。如果没有这样的技术框架,团队就必须靠自我约束来使用时限事先商定的 Aggregate,并按照这些 Aggregate 来编写代码。
Aggregate 划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。接下来要讨论的两种模式 Factory 和 Repository 都是在 Aggregate 上执行操作,它们将特定生命周期转换的复杂性封装起来。
3.2 模式:Factory
当创建一个对象或创建整个 Aggregate 时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用 Factory 进行封装。
对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
但将职责转交给另一相关方——应用程序中的客户(client)对象——会产生更严重的问题。客户知道需要完成什么工作,并依靠领域对象来执行必要的计算。如果指望客户来装配它需要的领域对象,那么它必须了解一些对象的内部结构。为了确保所有应用于领域对象各部分关系的固定规则得到满足。客户必须知道对象的一些规则。甚至调用构造函数也会使客户与所要构建的对象的具体类产生耦合。结果是,对领域对象实现所做的任何修改都要求客户做出相应修改,这使得重构变得更加困难。
当客户创建对象时,它会牵涉不必要的复杂性,并将其职责搞得模糊不清。这违背了领域对象及创建的 Aggregate 的封装要求。更严重的是,如果客户是应用层的一部分,那么职责就会从领域层泄漏到应用层。应用层与实现细节之间的这种耦合使得领域层抽象的大部分优势荡然无存,而且导致后续更改的代价变得更加高昂。
对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或 Aggregate 的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。在这些情况下,对象的创建和装配对应于领域中的重要事件。如 “开立银行账号”。但一般情况下,对象的创建和装配在领域中并没有什么意义,它们只不过是实现的一种需要。为了解决这一问题,我们必须在领域设计中增加一种新的构造,它不是 Entity、Value Object ,也不是 Service。这与前面的论述违背,因此把它解释清楚很重要。我们正在向设计中添加一些元素,但它们不对应于模型中的任何事物,而确实又承担了领域层的部分职责。
每种面向对象的语言都提供了一种创建对象的机制(例如,构造函数),但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是 Factory,它是一种负责创建其他对象的程序元素。如图:
正如对象的接口应该封装对象的实现一样(从而使客户无需知道对象的工作机理就可以使用对象的功能),Factory 封装了创建对象或 Aggregate 所需的知识。它提供了反映客户目标的接口,以及被创建对象的抽象视图。
因此:
应该将创建复杂对象的实例和 Aggregate 的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建 Aggregate 时要把它作为一个整体,并确保它满足固定规则。
Factory 有很多设计方式。包括 Factory Method(工厂方法)、Abstract Factory(抽象工厂)和 Builder(构建器)。
任何好的工厂都需要满足以下两个基本需求。
(1)每个创建方法都是原子的,而且要保证被创建对象或 Aggregate 的所有固定规则。Factory 生成的对象要处于一致的状态。在生成 Entity 时,这意味着创建满足所有固定规则的整个 Aggregate,但在创建完成后可以向聚合添加可选元素。在创建不变的 Value Object 时,这意味着所有属性必须被初始化为正确的最终状态。如果 Factory 通过其接口接收到了一个创建对象的请求,而它又无法正确地创建出这个对象,那么它应该抛出一个异常,或者采用其他机制,以确保不会返回错误的值。
(2)Factory 应该被抽象为所需的类型,而不是所要创建的具体类。
3.2.1 选择 Factory 及其应用位置
一般来说,Factory 的作用是隐藏创建对象的细节,而且我们把 Factory 用在那些需要隐藏细节的地方。这些决定通常与 Aggregate 有关。
例如,如果需要向一个已经存在的 Aggregate 添加元素,可以在 Aggregate 的根上创建一个 Factory Method 。这样就可以把 Aggregate 的内部实现细节隐藏起来,使任何外部客户看不到这些细节,同时使根负责确保 Aggregate 在添加元素时的完整性。
另一个示例是在一个对象上使用 Factory Method,这个对象与生成另一个对象密切相关,但它并不拥有所生成的对象。当一个对象的创建主要使用另一个对象的数据(或许还有规则)时,则可以在后者的对象上创建一个 Factory Method,这样就不必将后者的信息提取到其他地方来创建前者。这样做还有利于表达前者与后者之间的关系。
Factory 与被构建对象之间是紧密耦合的,因此 Factory 应该只被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏(无论要隐藏的具体实现还是构造的复杂性)而又找不到适合的地方来隐藏它们时,必须创建一个专用的 Factory 对象或 Service。整个 Aggregate 通常由一个独立的 Factory 来创建,Factory 负责把对根的引用传递出去,并确保创建出的 Aggregate 满足固定规则。如果 Aggregate 内部的某个对象需要一个 Factory ,而这个 Factory 又不适合在 Aggregate 根上创建,那么应该构建一个独立的 Factory。但仍应遵守规则——把访问限制在 Aggregate 内部,并确保从 Aggregate 外部只能对被构建对象进行临时引用。
3.2.2 有些情况下只需使用构造函数
Factory 的引入提供了巨大的优势,而这种优势往往并未得到充分利用。但是,在有些情况下直接使用构造函数确实是最佳选择。Factory 实际上会使那些不具有多态性的简单对象复杂化。
在以下情况下最好使用简单的、公共的构造函数。
(1)类(class)是一种类型(type)。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态。
(2)客户关心的是实现,可能是将其作为选择 Strategy 的一种方式。
(3)客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。
(4)构造并不复杂。
(5)公共构造函数必须遵守与 Factory 相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。
不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是 Aggregate ,需要使用 Factory。使用 Factory Method 的门槛并不高。
3.2.3 接口的设计
当设计 Factory 的方法签名时,无论是独立的 Factory 还是 Factory Method,都要记住以下两点:
(1)每个操作都必须是原子的。我们必须在与 Factory 的一次交互中把创建对象所需的所有信息传递给 Factory 。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。可以抛出一个异常或仅仅返回 null 。为了保持一致,可以考虑采用编码标准来处理所有 Factory 的失败。
(2)Factory 将与其参数发生耦合。如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数(argument)的处理。如果只是简单地将参数插入到要构建的对象中,则依赖程度是适中的。如果从参数中选出一部分在构造对象时使用,耦合将更紧密。
最安全的参数是那些来自较低设计层的参数。即使在同一层中,也有一种自然的分层倾向,其中更基本的对象被更高层的对象使用。
另一个好的参数选择是模型中与被构建对象密切相关的对象,这样不会增加新的依赖。
3.2.4 固定规则的相关逻辑应放在哪里
Factory 负责确保它所创建的对象或 Aggregate 满足所有固定规则,然而把应用于一个对象的规则移到该对象外部之前应三思。Factory 可以将固定规则的检查工作委派给被创建的对象,而且通常是最佳选择。
但 Factory 与被创建对象之间存在一种特殊关系。Factory 已经知道被创建对象的内部结构,而且创建 Factory 的目的与被创建对象的实现有着密切的联系。在某些情况下,把固定规则的相关逻辑放到 Factory 中是有好处的,这样可以让被创建对象的职责更明晰。对于 Aggregate 规则来说尤其如此(这些规则会约束很多对象)。但固定规则的相关逻辑却不适合放到那些与其他领域对象关联的 Factory Method 中。
虽然原则上在每个操作结束时都应该应用固定规则,但通常对象所允许的转换可能永远也不会用到这些规则。可能 Entity 标识属性的赋值需要满足一条固定规则。但该标识在创建后可能一直保持不变。Value Object 则是完全不变的。如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没必要携带这个逻辑。在这种情况下,Factory 是放置固定规则的合适地方,这样可以使 Factory 创建出的对象更简单。
3.2.5 Entity Factory 与 Value Object Factory
Entity Factory 与 Value Object Factory 有两个方面的不同。由于 Value Object 是不可变的,因此,Factory 所生成的对象就是最终形式。因此 Factory 操作必须得到被创建对象的完整描述。而 Entity Factory 则只需具有构造有效 Aggregate 所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
3.2.6 重建已存储的对象
到目前为止,Factory 只是发挥了它在对象生命周期开始时的作用。到了某一时刻,大部分对象都要存储在数据库中或通过网络传输,而在当前的数据库技术中,几乎没有哪种技术能够保持对象的内容特征。大多数传输方法都要将对象转换为平面数据才能传输,这使得对象只能以非常有限的形式出现。因此,检索操作潜在地需要一个复杂的过程将各个部分中信装配成一个可用的对象。
用于重建对象的 Factory 与用于创建对象的 Factory 很类似,主要有以下两点不同。
(1)用于重建对象的 Entity Factory 不分配新的跟踪ID。如果重新分配ID,将丢失与先前对象的连续性。因此,在重建对象的 Factory 中,标识属性必须是输入参数的一部分。
(2)当固定规则未被满足时,重建对象的 Factory 采用不同的方式进行处理。当创建新对象时,如果未满足固定规则,Factory 应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。如果对象已经在系统的某个地方存在(如在数据库中),那么不能忽略这个事实。但是,同样也不能任凭规则被破坏。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建对象更困难。
总之,必须把创建实例的访问点标识出来,并显示地定义它们的范围。它们可能只是构造函数,但通常需要一种更抽象或更复杂的实例创建机制。为了满足这种需求,需要在设计中引入新的构造——Factory。Factory 通常不表示模型的任何部分,但它们是领域设计的一部分,能够使对象更明确地表示出模型。
Factory 封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这种转换由另一种领域设计构造来处理,它就是 Repository。
3.3 模式:Repository
我们可以通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个 Entity 或 Value。
数据库搜索是全局可访问的,它使我们可以直接访问任何对象。由此,所有对象不需要相互联结起来,整个对象关系网就能够保持在可控的范围内。是提供遍历还是依靠搜索,这成为一个设计决策,需要在搜索的解耦与关联的内聚之间做出权衡。Customer 对象应该保持该客户所有已订的 Order 吗?应该通过 Customer ID 字段在数据库中查找 Order 吗?恰当地结合搜索与关联将会得到易于理解的设计。
遗憾的是,开发人员一般不会过多地考虑这种精细的设计,因为他们满脑子都是需要用到的机制,以便很有技巧地利用它们来实现对象的存储,取回和最终删除。
从概念上讲,对象检索发生在 Entity 生命周期的中间。不能只是因为我们将 Customer 对象保存在数据库中,而后把它们检索出来,这个 Customer 就代表一个新的客户。为了记住这个区别,把使用已存储的数据创建实例的过程称为重建。
领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。假设开发人员构造了一个 SQL 查询,并将这些信息传递给构造函数或 Factory 。开发人员执行这一连串操作的时候,早已不再把模型当作重点了。我们很自然地会把对象当作容器来放置查询出来的数据,这样整个设计就转向了数据处理风格。虽然具体的技术细节有所不同,但问题仍然存在——客户处理的是技术,而不是模型概念。诸如 Metadata Mapping Layer 这样的基础设施可以提供很大帮助,利用它很容易将查询结果转换为对象,但开发人员试图会绕过模型的功能(如 Aggregate,甚至是对象封装),而直接获取和操作他们所需的数据。这将导致越来越多的领域规则嵌入到查询代码中,或者干脆丢失了。虽然对象数据库清楚了转换问题,但搜索机制还是很机械的,开发人员仍倾向于要什么就去拿什么。
客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库提取他们所需的数据,或是直接提取具体的对象,而不是通过 Aggregate 的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而 Entity 和 Value Object 则变成单纯的数据容器。采用大多数处理数据库的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。
在所有持久化的对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些 Aggregate 根的时候,就需要使用这种访问方式。它们通常是 Entity,有时候是具有复杂内部结构的 Value Object,还可能是枚举 Value。而其他对象则不宜使用这样访问方式,因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和 Aggregate。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动设计。
有大量的技术可以用来解决数据库访问的技术难题,例如,将 SQL 封装到 Query Object 中,或利用 Metadata Mapping Layer 进行对象和表之间的转换。Factory 可以帮助重建那些已存储的对象。这些技术和很多其他技术有助于控制数据库访问的复杂度。
有得必有失,我们应该注意失去了什么。我们已经不再考虑领域模型中的概念。代码也不再表达业务,而是对数据库检索技术进行操纵。Repository 是一个简单的概念框架,它可用来封装这些解决方案,并将我们的注意力重新拉回到模型上。
Repository 将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合,只是具有更复杂的查询功能。在添加或删除相应类型的对象时,Repository 的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对 Aggregate 根的整个生命周期的全程访问。
客户使用查询方法向 Repository 请求对象,这些查询方法根据客户所指定的条件(通常是特定属性的值)来挑选对象。Repository 检索被请求的对象,并封装数据库查询和元数据映射机制。Repository 可以根据客户所要求的各种条件来挑选对象。它们也可以返回汇总信息,如有多少个实例满足查询条件。Repository 甚至能返回汇总计算,如所有匹配对象的某个数值属性的总和。
Repository 解除了客户的巨大负担,使客户只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。要实现所有这些功能需要大量复杂的技术基础设施,但接口很简单,而且在概念层次上与领域模型紧密联系起来。
因此:
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象中在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的 Aggregate 根提供 Repository 。让客户聚焦于模型,而将所有对象的存储和访问操作交给 Repository 来完成。
Repository 有很多优点,包括:
(1)它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期;
(2)它们使应用程序和领域设计与持久化技术(多种数据库策略甚至是多个数据源)解耦;
(3)它们体现了有关对象访问的设计决策;
(4)可以很容易将它们替换为“哑实现”,以便在测试中使用(通常使用内存中的集合)。
3.3.1 Repository 查询
所有 Repository 都为客户提供了根据某种条件来查询对象的方法,但如何设计这个接口却有很多选择。
最容易构建的 Repository 用硬编码的方式来实现一些具有特定参数的查询。这些查询可以形式各异,例如,通过标识来检索 Entity(几乎所有 Repository 都提供了这种查询)、通过某个特定属性值或复杂的参数组合来请求一个对象集合、根据值域(如日期范围)来选择对象,甚至可以执行某些属于 Repository 一般职责范围内的计算(特别是利用那些底层数据库所支持的操作)。
在任何基础设施上,都可以构建硬编码式的查询,也不需要很大的投入,因为即使它们不做这些事,有些客户也必须要做。
在一些需要执行大量查询的项目上,可以构建一个支持更灵活查询的 Repository 框架,如图。这要求开发人员熟悉必要的技术,而且一个支持性的基础设施会提供巨大的帮助。
基于 Specification(规格)的查询是将 Repository 通用化的好办法。客户可以使用规格来描述(也就是指定)它需要什么,而不必关心如何获取结果。在这个过程中,可以创建一个对象来实际执行筛选操作。
即使一个 Repository 的设计采取了灵活的查询方式,也应该允许添加专门的硬编码查询。这些查询作为便捷的方法,可以封装常用查询或不返回对象(如返回的是选中对象的我汇总计算)的查询。不支持这些特殊查询方式的框架可能会扭曲领域设计,或是干脆被开发人员弃之不用。
持久化技术的封装可以使得客户变得十分简单,并且使客户与 Repository 的实现之间完全解耦。但像一般的封装一样,开发人员必须知道在封装背后都发生了什么事情。在使用 Repository 时,不同的使用方式或工作方式可能会对性能产生极大的影响。
3.3.2 Repository 的实现
根据所使用的持久化技术和基础设施不同,Repository 的实现也将有很大的变化。理想的实现是向客户隐藏所有内部的工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。Repository 将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是 Repository 实现的最基本特性。
Repository 概念在很多情况下都适用。可能的实现方法很多,下面是一些需要谨记的注意事项:
(1)对类型进行抽象。Repository “含有”特定类型的所有实例,但这并不意味着每个类都需要有一个 Repository。类可以是一个层次结构中的抽象超类。类型可以是一个接口——接口的实现者并没有层次结构上的关联,也可以是一个具体的类。由于数据库技术缺乏这样的多态性质,因此我们将面临很多约束。
(2)充分利用与客户解耦的优点。我们可以很容易地更改 Repository 的实现,但如果客户直接调用底层机制,我们就很难修改其实现。也可以利用解耦来优化性能,因为这样就可以使用不同的查询技术,或在内存中缓存对象,可以随时*地切换持久化策略。
(3)将事务的控制权留给客户。尽管 Repository 会执行数据库的插入和删除操作,但它通常不会提交事务。例如,保存数据后紧接着就提交似乎是自然的事情,但想必只有客户才有上下文,从而能够正确地初始化和提交工作单元。如果 Repository 不插手事务控制,那么事务管理就会简单得多。
通常,项目团队会在基础设施中添加框架,用来支持 Repository 的实现。Repository 超类除了与较低层的基础设施组件进行协作以外,还可以实现一些基本查询,特别是要实现的灵活查询时。
3.3.3 Repository 与 Factory 的关系
Factory 负责处理对象生命周期的开始,而 Repository 帮助管理生命周期的中间和结束。当对象驻留在内存中或存储在对象数据库时,这是很好理解的。但通常至少有一部分对象存储在关系数据库、文件或其他非面向对象的系统中。在这些情况下,检索出来的数据必须被重建为对象形式。
由于在这种情况下 Repository 基于数据来创建对象,因此很多人认为 Repository 就是 Factory,而从技术角度来看的确如此。但是我们最好还是从模型的角度来看待这一问题,前面讲过,重建一个已存储的对象并不是创建一个新的概念对象。从领域驱动设计的角度来看,Factory 和 Repository 具有完全不同的职责。Factory 负责制造新的对象,而 Repository 负责查找新的对象。Repository 应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建,但它是同一个概念对象,仍旧处于生命周期的中间。
Repository 也可以委托 Factory 来创建一个对象,这种方法(虽然实际很少这样做,但在理论上是可行的)可用于从头开始创建对象。
这种职责上的明确区分还有助于 Factory 摆脱所有持久化职责。Factory 的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道创建完成之后应该把它添加到 Repository 中,由 Repository 来封装对象在数据库中的存储。
另一种情况促使人们将 Factory 和 Repository 结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。我们最好不要追求这种功能,它不会带来多少方便。当将 Entity 和 Value Object 区分开时,很多看上去有用的功能就不复存在了。需要 Value Object 的客户可以直接请求 Factory 来创建一个。通常,在领域中将新对象和原有对象区分开是很重要的,而将它们组合在一起的框架实际上只会使局面变得混乱。
3.4 为关系型数据库设计对象
在以面向对象为主的软件系统中,最常用的非对象组件就是关系数据库。这种现状产生了混合使用范式的常见问题。(待续)