DDD记录 领域对象的声明周期

管理对象的生命周期存在的挑战可以分为两类:
1. 在生命周期中维护对象的完整性
2. 避免模型由于管理生命周期的复杂性而陷入困境。

本章将通过3个模式来处理这些问题。首先,聚合通过定义清晰的所有权边界来使模型变得更紧凑,避免出现盘根错节的对象关系网。这个模式对于在生命周期的各个阶段中维护完整性是非常关键的。

接下来,我们将重点转向生命周期的开始部分,用工厂(Factory)来创建和重组复杂的对象喝聚合,并保持对其内部结构的良好封装。最后,仓储(Repository)用于处理生命周期的中间和结束部分,为我们提供查找和提取持久对象的方法,同时把与生命周期管理有关的复杂基础设施封装起来。

虽然工厂和仓储本身并不是从领域中产生的,但它们在领域设计中作用不容忽视。这些构造为我们提供了访问和控制模型对象的方法,完善了模型驱动设计。

建立聚合的模型,并把工厂和仓储加入设计中来,可以使我们系统地对模型对象进行操纵,同时使得这些对象的生命周期成为一个个意义明确的单元。聚合圈出一个范围,在这个范围中无论对象处于生命周期的哪个阶段,都应该保持不变性。工厂和仓储对聚合进行操作,将特定生命周期变迁的复杂性封装起来。

聚合

实际上,要为这种问题找到一种平衡的解决方案,我们必须对领域理解得更加深刻,了解更多的扩展因素,例如某些类的实例发生改变的频率是多少等。我们需要找到这样一个模型,它使得远离高竞争多发点,而不变量约束更加牢固。

首先,我们需要一种抽象机制用于在模型中对引用进行封装。一个聚合是一簇相关联的对象,出于数据变化的目的,我们将这些对象视为一个单元。每个聚合都有一个根和一个边界。边界定义了聚合中包含什么;根是包含在聚合中的单个特定的实体。根是聚合中唯一允许被外部对象引用的元素,但在聚合的边界内,对象之间可以相互引用。跟之外的实体具有本地标识,但是他们仅仅在聚合内部才需要区分其标识,因为根实体上下文以外的外部对象不会看到它们。

比如轿车的例子,我们会查询数据库,找到一部车,然后找它要一个轮胎的暂时引用。因此,这个聚合的根实体是轿车,而且其边界把轮胎也包含在内。另一方面,引擎组上都刻了序列号,有时需要独立于轿车单独对它们进行跟踪,因此在某些应用中,引擎可能会是它自己的聚合根。

不变量是指无论何时数据发生变化都必须满足的一致性规则。

现在,为了将概念上的聚合转换为实现,我们需要在所有的事务中应用一系列的规则。
1. 根实体具有全局标识,并最终负责对不变量的检查。
2. 根实体具有全局标识。边界之内的ENTITY具有本地标识,这些标识仅在聚合内部是唯一的。
3. 聚合边界以外的任何对象出了可以引用根实体,不能持有任何对其内部对象的引用。根实体可以把其内部实体的引用传递给其他对象,但是它们只能临时使用这种引用,而不能持有这种引用。根还可以复制一个值对象的副本传递给另一个对象。它并不关心这个副本会发生什么变化,因为那只是一个值,而且与聚合已经不再有任何关联了。
4. 作为上一条规则的推论,能通过数据库查询直接获得的对象只有聚合跟。所有其他对象必须通过导航关联来访问。
5. 聚合内的对象可以持有其他聚合根的引用。
6. 删除操作必须一次性删除聚合边界内的所有对象(如果有垃圾收集器就很容易了,因为只有根才会被外部引用,删除根就会使其他内部对象全部被收集掉)
7. 当在聚合边界内发生的任何对象修改被提交时,整个聚合的所有不变量必须都被满足。

将实体和值对象聚集到聚合中。每个聚合定义了一个边界。为每个聚合选择一个实体作为其根,并通过根来控制所有对边界内对象的访问。外部对象只能持有根的引用;对内部元素的临时引用只能在单个操作中使用。由于根控制了访问,因此我们无法绕过它去修改内部元素。这种安排使得我们可以保证在任何状态变化中,聚合本身(作为一个整体)的不变量,以及聚合中对象的不变量都可以被满足。

订单和订单条目的例子,有一个不变约束总价格。

聚合强制了PO及其子项之间的所有权关系,这是符合业务实际的。PO及其子项的创建和删除被自然地关联起来,同时part的创建和删除则是独立的。

聚合圈定出一个范围,在这个范围中无论出于生命周期的哪个阶段,都应该满足不变量。接下来的工厂和仓储模式用来对聚合进行操作,将特定生命周期变迁的复杂性封装起来。

工厂

当创建一个对象或整个聚合的逻辑变得非常复杂,或者过多地暴露了内部结构时,工厂提供了封装。

任何好的工厂都有两个基本的要求:
1. 每个创建方法都是原子的,并保证所创建的对象或聚合能满足所有不变量。工厂所构造出来的对象在状态上必须是一致的。对于实体来说,这意味着它创建的是一个完整的、满足所有的不变量的聚合,但有一些可选的元素可能还有待加入。对于具有不变性的值对象来说,这意味着其所有属性都已经初始化为正确的最终状态。有时工厂可能无法根据外部请求把对象正确地构造出来,如果工厂的接口允许这种请求,那么它应该抛出一个异常,或者调用其他几只来确保不会返回错误的对象。
2. 工厂应该将构造结果抽象到所需的类型,而不是它所创建的具体的类型。

工厂及其应用场所的选择

一般而言,我们创建工厂的目的是为了把待创建对象的细节隐藏起来,并在我们希望进行控制的地方使用工厂。这些决定通常是围绕聚合来考虑的。

例如,如果您需要向一个已有的聚合中加入元素,那么可以在聚合根中创建一个工厂方法。这可以把聚合的内部实现向所有外部客户隐藏起来,同时由根负责保证聚合在加入元素之后的完整性。

另一个在对象中实现工厂方法的例子是,这个对象与另一个对象的生成密切相关,但是却并不拥有它生成的对象。当一个对象所提供的数据或规则在另一个对象的创建过程中起到支配作用的时候,我们用它的工厂方法来创建那个对象会很方便,因为如果在其他地方创建的话,我们还得把这个对象的内部信息都提取出来。此外,这种做法还能表达出生产值对象与其产品之间的特殊联系。

例子,Trade Order不属于Brokerage Account所在的聚合的一部分,因为它在生成后将继续与交易执行系统打交道,把它放在Brokerage Account中反而碍事。虽然如此,让Brokerage Account来控制Trade Order的生成看起来还是很自然的。Brokerage Account包含了Trade Order中必须嵌入的信息(从其自身的标识开始),还包含了一些规则来控制哪些交易是允许的。还有一个好处是我们可以把Trade Order的实现隐藏起来。例如,Trade Order可能会被重构为一个层次结构,包括Buy Order和Sell Order等不同的子类。工厂解除了客户与具体类的关联。

就是在Account中创建Order比较容易,因为它可以给工厂传入有用的参数,比如自己的id

只需构造函数的情况

在下面的情况下,我们倾向于使用原始的共有构造函数:
1. 类就是类型。它不是任何层次结构的一部分,也没有通过实现接口来使用多态;
2. 客户关心具体的实现,可能作为选择策略的一种方法;
3. 客户可以使用对象的所有属性,因此提供给客户的构造函数中没有嵌入对象创建逻辑
4. 构造函数并不复杂;
5. 共有构造函数必须遵循和工厂相同的规则:它必须是一个原子操作,并保证创建出来的对象满足所有不变量。

接口的设计

当设计工厂的方法签名时,无论工厂是独立的工厂还是工厂方法,都要记住以下两点:
1. 每个操作都必须是原子的。您必须把所需的所有信息传递给工厂,使之只需一次交互就能创建出完整的产品来。您还必须决定当创建失败时如何处理,即出现某些不变量无法满足时。您可以抛出一个异常,或者直接返回一个null。为了保持一致,您可以把工厂失败的处理方法写入编码规范。
2. 工厂将与其变元的类型产生关联。在选择输入参数时,我们可能会不小心产生隐含的依赖关系。关联的程度取决于我们是怎样处理变元的。如果变元可以简单地插入到产品之中,那么依赖关系还不是很大。如果我们要从参数中挑出一些部分用来构造对象的话,那么关联就会变得更紧。

如何放置不变量的逻辑

工厂有的时候可以放置对象当中的不变量。虽然远离上不变量是在每个操作完成时被检查的,但是对象允许的转换往往不会违反不变量。例如,在实体中可能有一天规则是约束其标识属性的赋值,但是实体的标识属性在创建之后就不会再发生变化了。值对象则完全是不变的。由于这种规则在对象的活跃生命周期中从来不会被用到,因此对象可以不需要携带这些逻辑。在这样的情况下,把不变量存放在工厂就非常合理了,也能使得产品更加简单。

工厂封装了对象在创建和重建时的生命周期变迁。还有另一个变迁,它在技术上的复杂性也会导致领域设计陷入混乱,那就是对象喝存储之间的双向转换。这种转换是另一种领域设计构造——仓储的职责。

你可能感兴趣的:(对象)