六 领域驱动设计-领域对象的生命周期
每个对象都有生命周期,如图6-1所示。对象自创建后,可能会经历各种不同的状态,直至最终消亡——要么存档,要么删除。当然,很多对象是简单的临时对象,仅通过调用构造函数来创建,用来做一些计算,而后由垃圾收集器回收。这类对象没必要搞得那么复杂。但有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的。它们与其他对象具有复杂的相互依赖性。它们会经历一些状态变化,在变化时要遵守一些固定规则。管理这些对象时面临诸多挑战,稍有不慎就会偏离MODEL-DRIVEN DESIGN的轨道。
主要的挑战有以下两类。
(1) 在整个生命周期中维护完整性。
(2) 防止模型陷入管理生命周期复杂性造成的困境当中。
AGGREGATE(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。我们将注意力转移到生命周期的开始阶段,使用FACTORY(工厂)来创建和重建复杂对象和AGGREGATE(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
尽管REPOSITORY和FACTORY本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。这些结构提供了易于掌握的模型对象处理方式,使MODEL-DRIVEN DESIGN更完备。
使用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的根。
固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定的时间内得以解决。但在每个事务完成时,AGGREGATE内部所应用的固定规则必须得到满足,如图6-3所示。
现在,为了实现这个概念上的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来编写代码。
示例 采购订单的完整性
一个典型的采购订单(Purchase Order,PO)视图,它被分解为采购项(Line Item),一条固定规则是采购项的总量不能超过PO总额的限制。当前实现存在以下3个互相关联的问题。
(1) 固定规则的实施。当添加新采购项时,PO检查总额,如果新增的采购项使总额超出限制,则将PO标记为无效。正如我们将要看到的那样,这种保护机制并不充分。
(2) 变更管理。当PO被删除或存档时,各个采购项也将被一块处理,但模型并没有给出关系应该在何处停止。在不同时间更改部件(Part)价格所产生的影响也不明确。
(3) 数据库共享。数据库会出现由于多个用户竞争使用而带来的问题。
多个用户将并发地输入和更新各个PO,因此必须防止他们互相干扰。让我们从一个非常简单的策略开始,当一个用户开始编辑任何一个对象时,锁定该对象,直到用户提交事务。这样,当George编辑采购项001时,Amanda就无法访问该项。Amanda可以编辑其他PO上的任何采购项(包括George正在编辑的PO上的其他采购项)
每个用户都将从数据库读取对象,并在自己的内存空间中实例化对象,而后在那里查看和编辑对象。只有当开始编辑时,才会请求进行数据库锁定。因此,George和Amanda可以同时工作,只要他们不同时编辑相同的采购项即可。一切正常,直到George和Amanda开始编辑同一个PO上的不同采购项.
从这两个用户和他们各自软件的角度来看,他们的操作都没有问题,因为他们忽略了事务期间数据库其他部分所发生的变化,而且每个用户都没有修改被对方锁定的采购项。当这两个用户保存了修改之后,数据库中就存储了一个违反领域模型固定规则的PO。一条重要的业务规则被破坏了,但并没有人知道
显然,锁定单个行并不是一种充分的保护机制。如果一次锁定一个PO,可以防止这样的问题发生
直到Amanda解决这个问题之前,程序将不允许保存这个事务,Amanda可以通过提高限额或减少一把吉他来解决此问题。这种机制防止了问题,如果大部分工作分布在多个PO上,那么这可能是个不错的解决方案。但如果是很多人同时对一个大PO的不同项进行操作时,这种锁定机制就显得很笨拙了。
即便是很多小PO,也存在其他方法破坏这条固定规则。让我们看看“Part”。如果在Amanda将长号加入订单时,有人更改了长号的价格,这不也会破坏固定规则吗?
我们试着除了锁定整个PO之外,也锁定Part,当George、Amanda和Sam在不同PO上工作时将会发生的情况。
工作变得越来越麻烦,因为在Part上出现了很多争用的情况。这样就会发生图6-10中的结果:3个人都需要等待。
现在我们可以开始改进模型,在模型中加入以下业务知识。
(1) Part在很多PO中使用(会产生高竞争)。
(2) 对Part的修改少于对PO的修改。
(3) 对Price(价格)的修改不一定要传播到现有PO,它取决于修改价格时PO处于什么状态。
当考虑已经交货并存档的PO时,第三点尤为明显。它们显示的当然是填写时的价格,而不是当前价格。
这个模型得到的实现可以确保满足PO和采购项相关的固定规则,同时,修改部件的价格将不会立即影响引用部件的采购项。涉及面更广的规则可以通过其他方式来满足。例如,系统可以每天为用户列出价格过期的采购项,这样用户就可以决定是更新还是去掉采购项。但这并不是必须一直保持的固定规则。通过减少采购项对Part的依赖,可以避免争用,并且能够更好地反映出业务的现实情况。同时,加强PO与采购项之间的关系可以确保遵守这条重要的业务规则。
AGGREGATE强制了PO与采购项之间符合业务实际的所属关系。PO和采购项的创建及删除很自然地被联系在一起,而Part的创建和删除却是独立的。
AGGREGATE划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。接下来要讨论的两种模式FACTORY和REPOSITORY都是在AGGREGATE上执行操作,它们将特定生命周期转换的复杂性封装起来……
个人理解:完全没看懂,好像是建模处理资源竞争的问题。
FACTORY
当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用FACTORY进行封装。
对象的功能主要体现在其复杂的内部配臵以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
汽车发动机是一种复杂的机械装臵,它由数十个零件共同协作来履行发动机的职责——使轴转动。我们可以试着设计一种发动机组,让它自己抓取一组活塞并塞到汽缸中,火花塞也可以自己找到插孔并把自己拧进去。但这样组装的复杂机器可能没有我们常见的发动机那样可靠或高效。相反,我们用其他东西来装配发动机。或许是机械师,或者是工业机器人。无论是机器人还是人,实际上都比二者要装配的发动机复杂。装配零件的工作与使轴旋转的工作完全无关。只是在生产汽车时才需要装配工,我们驾驶时并不需要机器人或机械师。由于汽车的装配和驾驶永远不会同时发生,因此将这两种功能合并到同一个机制中是毫无价值的。同理,装配复杂的复合对象的工作也最好与对象要执行的工作分开。
正如对象的接口应该封装对象的实现一样(从而使客户无需知道对象的工作机理就可以使用对象的功能),FACTORY封装了创建复杂对象或AGGREGATE所需的知识。它提供了反映客户目标的接口,以及被创建对象的抽象视图。
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
FACTORY有很多种设计方式。[Gamma et al.1995]中详尽论述了几种特定目的的创建模式,包括FACTORY METHOD(工厂方法)、ABSTRACT FACTORY(抽象工厂)和BUILDER(构建器)。该书主要研究了适用于最复杂的对象构造问题的模式。本书的重点并不是深入讨论FACTORY的设计问题,而是要表明FACTORY的重要地位——它是领域设计的重要组件。正确使用FACTORY有助于保证MODEL-DRIVEN DESIGN沿正确的轨道前进。
任何好的工厂都需满足以下两个基本需求
(1) 每个创建方法都是原子的,而且要保证被创建对象或AGGREGATE的所有固定规则。FACTORY生成的对象要处于一致的状态。在生成ENTITY时,这意味着创建满足所有固定规则的整个AGGREGATE,但在创建完成后可以向聚合添加可选元素。在创建不变的VALUE OBJECT时,这意味着所有属性必须被初始化为正确的最终状态。如果FACTORY通过其接口收到了一个创建对象的请求,而它又无法正确地创建出这个对象,那么它应该抛出一个异常,或者采用其他机制,以确保不会返回错误的值。
(2) FACTORY应该被抽象为所需的类型,而不是所要创建的具体类。[Gamma et al.1995]中的高级FACTORY模式介绍了这一话题
个人理解:这个我懂了,对象的创建如果非常复杂(要组装或者初始化很多参数,规则一定)就可以使用工厂模式,技术开发人员应该非常了解工厂模式,业务不需要知道啥是工厂模式。
总结:没看懂,这部分更加注重程序对象和数据库的设计,还有就是模型的生命周期,好好了解下设计模式和数据库设计范式应该就够了。