前文回顾
上一篇介绍了该书的第二部分“模型驱动设计的构造块”,将面向对象领域建模中的一些核心的最佳实践提炼为一组基本的构造块。我们了解了模块(MODULE), 建模元素之间的关联关系,以及面向对象设计、业务规则引擎(BUSINESS RULE ENGINE)等建模范式。
这一篇我们继续学习该书的第二部分。
如何管理领域对象的生命周期
如下图所示,每个对象都有生命周期,从创建到删除。特别是有些对象具有较长的生命周期,有部分时间不是活动在内存中。
管理这些对象时面临诸多挑战,稍有不慎就会偏离MODEL DRIVEN DESIGN的轨道。主要的挑战有以下两类。
(1)在整个生命周期中维护完整性。
(2)防止模型陷入管理生命周期复杂性造成的困境当中。
有什么办法来应对这些挑战呢?作者Eric提供了3种方法。 首先,AGGREGATE(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。然后在生命周期的开始阶段,用FACTORY(工厂)来创建或重建复杂对象和AGGREGATE(聚合)。最后在生命周期的中间或者末尾阶段,使用REPOSITORY(仓库)来提供查找和对象持久化。
AGGREGATE(聚合)
上面提到AGGREGATE(聚合)是用来定义所属关系和边界的。它的定义如下:
AGGREGATE就是一组相关对象的集合,我们把它作为数据修改的单元。每个AGGREGATE都有一个根(root)和一个边界(boundary)。边界定义了AGGREGATE的内部都有什么。根则是AGGREGATE所包含的一个特定ENTITY。
聚合主要用来封装模型中的引用,并避免混乱、错综复杂的对象关系网。
对AGGREGATE而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他ENTITY都有本地标识,但这些标识只在AGGREGATE内部才需要加以区别,因为外部对象除了根ENTITY之外看不到其他对象。
比如下图中的汽车和轮胎,我们通常需要将一台汽车和其他汽车区分开,一辆汽车的轮胎也需要各自区分开,因为它们的行驶里程可能不同。而我们通常不会脱离这辆汽车的上下文,去单独关心一个轮胎,也不会有人通过系统查询一个轮胎安装在哪一台汽车上。因此,汽车是AGGREGATE的根ENTITY,而轮胎处于这个AGGREGATE的边界之内。另一方面,发动机上面都刻有序列号,而且有时是独立于汽车被跟踪的,发动机可以是自己的聚合根。
固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。如下图所示, 在每个事务完成时,AGGREGATE内部所应用的固定规则必须得到满足。 而跨AGGREGATE的规则不要求时时刻刻都保持最新状态。可以通过事件处理、批处理等更新机制,在一定的时间内完成更新。
外部对象只能引用聚合根,聚合根可以引用内部对象,并将其传递给外部对象。但外部对象只能临时使用,不能保持这种引用。换句话说,聚合根只是将内部对象的副本传递出去,而并不关心它会发生什么变化。
数据持久化时需要注意:
只有AGGREGATE的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。删除操作必须一次删除AGGREGATE边界之内的所有对象。
示例:采购订单
下面的示例将解释如何划分AGGREGATE,以确保采购订单的完整性。下图展示了一个典型的采购订单(PurchaseOrder,PO)视图,它被分解为采购项(LineItem),有一条固定规则是采购项的总量不能超过PO总额的限制。采购项是对部件(Part)的实例化。
当前实现存在以下3个互相关联的问题:
(1)固定规则的实施。在添加新采购项时,检查PO总额,超过限制则标记为无效。这种保护机制并不充分。
(2)变更管理。PO被删除时,如何处理采购项,以及更改部件(Part)价格所产生的影响不明确。
(3)数据库共享。数据库会出现由于多个用户竞争使用而带来的问题。
我们先采取一个简单的策略,当用户编辑一个对象时,就锁定它,直到提交修改。如下图所示,当George编辑采购项001时,Amanda就无法访问该项。
如下图所示,当George和Amanda同时编辑同一个PO的不同采购项时,问题来了。从这两个用户和他们各自软件的角度来看,他们的操作都没有问题。
但当这两个用户保存了修改之后,数据库中就存储了一个违反领域模型固定规则的PO。 锁定单个采购项并不是一种充分的保护机制。如果一次锁定一个PO,可以防止这样的问题发生。 如果大部分工作分散在多个PO上,那么这可能是个不错的解决方案。但如果是很多人同时集中对一个大PO的不同项进行操作时,这种锁定机制就显得很笨拙了。
另外,如果在新增采购项时有人修改了部件(Part)的价格,也会破坏固定规则。我们除了锁定整个PO之外,也要锁定Part。下图展示了当George、Amanda和Sam在不同PO上工作时发生的情况。结果非常麻烦,3个人都需要等待。
现在我们可以开始改进模型,在模型中加入以下业务知识。
- Part在很多PO中使用(会产生高竞争)。
- 对Part的修改少于对PO的修改。
- 对Price(价格)的修改不一定要传播到现有PO,它取决于修改价格时PO处于什么状态。
所以,如下图所示,price被复制到LineItem中,现在可以确保满足聚合的固定规则了。AGGREGATE强制了PO与采购项之间符合业务实际的从属关系。PO和采购项的创建及删除很自然地被联系在一起,而Part的创建和删除却是独立的。
FACTORY(工厂)
当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结构,我们就需要考虑如何进行封装。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么过多的职责将会导致问题。
但将职责转交给另一个相关方——应用程序中的客户(client)对象——会产生更严重的问题。让客户直接负责创建对象又会使客户端的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
面向对象的语言都提供了创建对象的机制,例如Java中的构造函数,,但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制,这就是FACTORY。如下图所示。
复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。它不是ENTITY、VALUEOBJECT,也不是SERVICE。我们需要往设计中添加一些新元素,但它们不对应于模型中的任何事物,而确实又承担领域层的部分职责。这就是FACTORY,它是一种负责创建其他对象的程序元素。
FACTORY有很多中设计方式,比如FACTORYMETHOD(工厂方法)、ABSTRACTFACTORY(抽象工厂)等等。 任何好的工厂都需满足以下两个基本需求。
(1)每个创建方法都是原子的,而且要保证被创建对象或AGGREGATE的所有固定规则。
(2)FACTORY应该被抽象为所需的类型,而不是所要创建的具体类。
什么情况下不采用FACTORY
在有些情况下直接使用构造函数确实是最佳选择。FACTORY实际上会使那些不具有多态性的简单对象复杂化。 使用构造函数时要注意,不要在构造函数中调用其他类的构造函数,构造函数应该保持绝对简单。复杂的装配,特别是AGGREGATE,需要使用FACTORY。
固定规则的相关逻辑应放置在哪里
FACTORY负责确保它所创建的对象或AGGREGATE满足所有固定规则。但不一定需要将固定规则放在FACTORY之中。因为将应用于一个对象的规则放到该对象之外,需要特别慎重。 FACTORY可以将固定规则的检查工作委派给被创建对象,通常来说这种做法也是最优方案。
如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没有必要携带这个逻辑。在这种情况下,FACTORY是放臵固定规则的合适地方。
ENTITY和VALUE OBJECT的工厂有什么不同
ENTITY 工厂与VALUE OBJECT工厂有两个方面的不同:
-
由于VALUE OBJECT是不可变的,因此,FACTORY所生成的对象就是最终形式。FACTORY操作必须得到被创建对象的完整描述。ENTITY FACTORY则只需具有构造有效AGGREGATE所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
-
另一方面,由于ENTITY是有标识的,ENTITY FACTORY应当知道需要什么样的标识,以及将标识放到何处。当由程序分配标识符,而不是用户提供标识符时,FACTORY是控制它的理想场所。
另外,用于重建对象的FACTORY与用于新创建对象的FACTORY很类似,但有以下两点不同:
- 用于重建对象的ENTITY FACTORY不分配新的跟踪ID。
- 当固定规则未被满足时,重建对象的FACTORY采用不同的方式进行处理。新建的对象不满足固定规则时,可以简单拒绝对象创建。重建时则必须通过某种策略来修复这种不一致的情况,不能忽略这个错误。
系列文章
- [DDD读书笔记] 运用模型①什么是领域模型
- [DDD读书笔记] 运用模型②通用语言
- [DDD读书笔记] 构造块①分离领域层
- [DDD读书笔记] 构造块②实体、值对象和服务
- [DDD读书笔记] 构造块③模块
- [DDD读书笔记] 构造块④聚合与工厂
- [DDD读书笔记] 构造块⑤仓库
- [DDD读书笔记] 构造块⑥实战模拟
- [DDD读书笔记] 重构①突破
- [DDD读书笔记] 重构②SPECIFICATION模式
- [DDD读书笔记] 重构③柔性设计
- [DDD读书笔记] 重构④使用分析模式和设计模式建模
- [DDD读书笔记] 战略设计①模型上下文策略
- [DDD读书笔记] 战略设计②精炼
- [DDD读书笔记] 战略设计③大型结构