阅读-领域驱动设计第五章-软件中所表示的模型

想在不削弱模型驱动设计能力的前提下对实现做出一些折中,需要重新组织基本元素。我们需要将模型与实现的各个细节一一联系起来。

本章的讨论从如何设计和简化关联开始。对象之间的关联很容易想出来,也很容易画出来,但实现它们却存在很多潜在的麻烦。关联也表明了具体的实现决策在MODEL-DRIVEN DESIGN中的重要性。我们将着重区分用于表示模型的3种模型元素模式:ENTITY、VALUE OBJECT和SERVICE。

从表面上看,定义那些用来捕获领域概念的对象很容易,但要想反映其含义却很困难。这要求我们明确区分各种模型元素的含义,并与一系列设计实践结合起来,从而开发出特定类型的对象。

ENTITY与VALUE OBJECT之间的根本区别

  • 一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是ENTITY与VALUE OBJECT之间的根本区别。明确地选择这两种模式中的一个来定义对象,有利于减少歧义,并帮助我们做出特定的选择,这样才能得到健壮的设计。

领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用SERVICE来表示,而不应把操作的责任强加到ENTITY或VALUE OBJECT上,尽管这样做稍微违背了面向对象的建模传统。SERVICE是应客户端请求来完成某事。在软件的技术层中有很多SERVICE。在领域中也可以使用SERVICE,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项SERVICE。

在有些情况下(例如,为了将对象存储在关系数据库中)我们不得不对对象模型做一些折中改变,虽然这会影响对象模型的纯度。本章将给出一些指导原则,以便在被迫处理这种复杂局面时保持正确的方向。

最后,MODULE的讨论将有助于理解这样一个要点——每个设计决策都应该是在深入理解领域中的某些深层知识之后做出的。高内聚、低耦合这种思想(通常被认为是一种技术指标)可应用于概念本身。在在MODEL-DRIVEN DESIGN中,MODULE是模型的一部分,它们应该反映领域中的概念。

本章将所有这些体现软件模型的构造块组织到一起。这些都是一些传统思想,而且一些书籍中已经介绍过从中产生的建模和设计思想。但将这些思想组织到模型驱动开发的上下文中,可以帮助开发人员创建符合领域驱动设计主要原则的具体组件,从而有助于解决更大的模型和设计问题。此外,掌握这些基本原则可以帮助开发人员在被迫做出折中设计时把握好正确的方向。

一、关联

对象之间的关联使得建模与实现之间的交互更为复杂。

  • 模型中每个可遍历的关联,软件中都要有同样属性的机制。

一个显示了顾客与销售代表之间关联的模型有两个含义。一方面,它把开发人员所认为的两个真实的人之间的关系抽象出来。另一方面,它相当于两个Java对象之间的对象指针,或者相当于数据库查询(或类似实现)的一种封装。

例如,一对多关联可以用一个集合类型的实例变量来实现。但设计无需如此直接。可能没有集合,这时可以使用一个访问方法(accessor method)来查询数据库,找到相应的记录,并用这些记录来实例化对象。这两种设计方法反映了同一个模型。设计必须指定一种具体的遍历机制,这种遍历的行为应该与模型中的关联一致。

至少有3种方法可以使得关联更易于控制。
(1) 规定一个遍历方向。
(2) 添加一个限定符,以便有效地减少多重关联。
(3) 消除不必要的关联。

尽可能地对关系进行约束是非常重要的。双向关联意味着只有将这两个对象放在一起考虑才能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少相互依赖,并简化设计。理解了领域之后就可以自然地确定一个方向。

像很多国家一样,美国有过很多位总统。这是一种双向的、一对多的关系。然而,在提到‚乔治〃华盛顿‛这个名字时,我们很少会问‚他是哪个国家的总统?‛。从实用的角度讲,我们可以将这种关系简化为从国家到总统的单向关联。如图5-1所示。这种精化实际上反映了对领域的深入理解,而且也是一个更实用的设计。它表明一个方向的关联比另一个方向的关联更有意义且更重要。

通常,通过更深入的理解可以得到一个‚限定的‛关系。进一步研究总统的例子就可以知道,一个国家在一段时期内只能有一位总统(内战期间或许有例外)。这个限定条件把多重关系简化为一对一关系,并且在模型中植入了一条明确的规则。如图5-2所示。1790年谁是美国总统?乔治〃华盛顿。



限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联,从而得到一个简单得多的设计。 模型的关系需要深入思考,慢慢演练,得到遍历的方向以及较少不必要的关联,并且可以找一些限定条件去简化关系。

坚持将关联限定为领域所倾向的方向,不仅可以提高这些关联的表达力并简化其实现,而且还可以突出剩下的双向关联的重要性。当双向关联是领域的一个语义特征时,或者当应用程序的功能要求双向关联时,就需要保留它,以便表达出这些需求。当然,最终的简化是清除那些对当前工作或模型对象的基本含义来说不重要的关联。

示例一:Brokerage Account(经纪账户)中的关联


此模型中的Brokerage Account的一个Java实现如下:

java 实现

但是,如果需要从关系数据库取回数据,那么就可以使用另一种实现(它同样也符合模型):




(注意:QueryService是一个实用类,它从数据库中取回数据行(row)并创建对象,这里使用它是为了让示例简单,但这在实际项目中可不一定是个好的设计。)下面,我们通过限定Brokerage Account(经纪账户)与Investment(投资)之间的关联来简化其多重性,从而对模型进行精化。具体的限定是:每支股票只能对应于一笔投资。


这种简化并不适合所有的业务情形(例如,当所有投资都要可追踪时),但不管是什么特殊规则,只要发现了关联的约束,就应该将这些约束添加到模型和实现中。它们可以使模型更精确,使实现更易于维护。


从仔细地简化和约束模型的关联到MODEL-DRIVEN DESIGN,还有一段漫长的探索过程。现在我们转向对象身。仔细区分对象可以使得模型更加清晰,并得到更实用的实现。

二、模式:ENTITY(又称为REFERENCE OBJECT)实体

很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。很多事物是由它们的标识定义的,而不是由任何属性定义的。我们一般会认为,一个人(继续使用非技术示例)有一个标识,这个标识会陪伴他走完一生(甚至死后)。这个人的物理属性会发生变化,最后消失。他的名字可能改变,财务关系也会发生变化,没有哪个属性是一生不变的,但标识却是永久的。我跟我5岁时是同一个人吗? 这种听上去像是纯哲学的问题在探索有效的领域模型时非常重要。稍微变换一下问题的角度:应用程序的用户是否关心现在的我和5岁时的我是不是同一个人?

对象建模有可能把我们的注意力引到对象的属性上,但实体的基本概念是一种贯穿整个生命周期(甚至会经历多种形式)的抽象的连续性。

  • 一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。

  • 主要由标识定义的对象被称作ENTITY①。ENTITY(实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。

  • 为了有效地跟踪这些对象,必须定义它们的标识。类定义、职责、属性和关联必须由其标识来决定,而不依赖于其所具有的属性。即使对于那些不发生根本变化或者生命周期不太复杂的ENTITY,也应该在语义上把它们作为ENTITY来对待,这样可以得到更清晰的模型和更健壮的实现。

重点:

  • 当然,软件系统中的大多数ENTITY‛并不是人,也不是其通常意义上所指的‚实体‛或‚存在‛。ENTITY可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。ENTITY可以是一个人、一座城市、一辆汽车、一张彩票或一次银行交易。

另一方面,在一个模型中,并不是所有对象都是具有有意义标识的ENTITY。但是,由于面向对象语言在每个对象中都构建了一些与标识有关的操作(如Java中的==操作符),这个问题变得有点让人困惑。

这些操作通过比较两个引用在内存中的位置(或通过其他机制)来确定这两个引用是否指向同一个对象。从这个角度讲,每个对象实例都有标识。比方说,当创建一个用于将远程对象缓存到本地的Java 运行时环境或技术框架时,这个领域中的每个对象可能确实都是一个ENTITY。但这种标识机制在其他应用领域中却没什么意义。标识是ENTITY的一个微妙的、有意义的属性,我们是不能把它交给语言的自动特性来处理的。

让我们考虑一下银行应用程序中的交易。同一天、同一个账户的两笔数额相同的存款实际上是两次不同的交易,因此它们是具有各自标识的ENTITY。另一方面,这两笔交易的金额属性可能是某个货币对象的实例。这些值没有标识,因为没有必要区分它们。事实上,两个对象可能有相同的标识,但属性可能不同,在需要的情况下甚至可能不属于同一个类。

当银行客户拿银行结算单与支票记录簿进行交易对账时,这项任务就是匹配具有相同标识的交易,尽管它们是由不同的人在不同的日期记录的(银行清算日期比支票上的日期晚)。支票号码就是用于对账的唯一标识符,无论这个问题是由计算机程序处理还是手工处理。存款和取款没有标识号码,因此可能更复杂,但同样的原则也是适用的——每笔交易都是一个ENTITY,至少出现在两张业务表格中。

小结:

因此:

当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。

要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”。

在现实世界中,并不是每一个事物都必须有一个标识,标识重不重要,完全取决于它是否有用。实际上,现实世界中的同一个事物在领域模型中可能需要表示为ENTITY,也可能不需要表示为ENTITY。

体育场座位预订程序可能会将座位和观众当作ENTITY来处理。在分配座位时,每张票都有一个座位号,座位是ENTITY。其标识符就是座位号,它在体育场中是唯一的。座位可能还有很多其他属性,如位臵、视野是否开阔、价格等,但只有座位号(或者说某一排的一个位臵)才用于识别和区分座位。

另一方面,如果活动采用入场卷的方式,那么观众可以寻找任意的空座位来坐,这样就不需要对座位加以区分。在这种情况下,只有座位总数才是重要的。尽管座位上仍然印有座位号,但软件已经不需要跟踪它们。事实上,这时如果模型仍然将座位号与门票关联起来,那么它就是错误的,因为采用入场卷的活动并没有这样的约束。在这种情况下,座位不是ENTITY,因此不需要标识符。

二、 1. ENTITY建模

当对一个对象进行建模时,我们自然而然会考虑它的属性,而且考虑它的行为也显得非常重要。但ENTITY最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。

不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住ENTITY对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。

此外,应该将行为和属性转移到与核心实体关联的其他对象中。这些对象中,有些可能是ENTITY,有些可能是VALUE OBJECT(这是本章接下来要讨论的模式)。除了标识问题之外,实体往往通过协调其关联对象的操作来完成自己的职责。


在图5-5中,customerID是Customer ENTITY的一个(也是唯一的)标识符,但phone number(电话号码)和address(地址)都经常用来查找或匹配一个Customer(客户)。name(姓名)没有定义一个人的标识,但它通常是确定人的方式之一。在这个示例中,phone和address属性被移到Customer中,但在实际的项目上,这种选择取决于领域中的Customer一般是如何匹配或区分的。

例如,如果一个Customer有很多用于不同目的的phone number,那么phone number就与标识无关,因此应该放在Sales Contact(销售联系人)中。

二、 2. 设计标识操作

每个ENTITY都必须有一种建立标识的操作方式,以便与其他对象区分开,即使这些对象与它具有相同的描述属性。不管系统是如何定义的,都必须确保标识属性在系统中是唯一的,即使是在分布式系统中,或者对象已被归档,也必须确保标识的唯一性。

标识是在模型中定义的。定义标识要求理解领域。有时,某些数据属性或属性组合可以确保它们在系统中具有唯一性,或者在这些属性上加一些简单约束可以使其具有唯一性。这种方法为ENTITY提供了唯一键。例如,日报可以通过名称、城市和出版日期来识别。

当对象属性没办法形成真正唯一键时,另一种经常用到的解决方案是为每个实例附加一个在类中唯一的符号(如一个数字或字符串)。一旦这个ID符号被创建并存储为ENTITY的一个属性,必须将它指定为不可变的。它必须永远不变,即使开发系统无法直接强制这条规则。例如,当对象被扁平化到数据库中或从数据库中重新创建时,ID属性应该保持不变。有时可以利用技术框架来实现此目的,但如果没有这样的框架,就需要通过工程纪律来约束。

关键是要认识到标识问题取决于模型的特定方面。通常,要想找到解决标识问题的方法,必须对领域进行仔细的研究。当自动生成ID时,用户可能永远不需要看到它。ID可能只是在内部需要,例如,在一个可以按人名查找记录的联系人管理应用程序中。这个程序需要用一种简单、明确的方式来区分两个同名联系人,这就可以通过唯一的内部ID来实现。在检索出两个不同的条目后,系统将显示这两
个不同的联系人,但可能不会显示ID。用户可以通过这两个人的公司、地点等属性来区分他们。

最后,在有些情况下用户会对生成的ID感兴趣。当我委托一个包裹运送服务寄包裹时,我会得到一个跟踪号,它是由运送公司的软件生成的,我可以用这个号码来识别和跟踪我的包裹。当我预订机票或酒店时,会得到一个确认号码,它是预订交易的唯一标识符。

在某些情况下,需要确保ID在多个计算机系统之间具有唯一性。例如,如果需要在两家具有不同计算机系统的医院之间交换医疗记录,那么理想情况下每个系统对同一个病人应该使用同一个ID,但如果这两个系统各自生成自己的ID,这就很难实现。这样的系统通常使用由另外一家机构(一般是政府机构)发放的标识符。

在美国,医院通常使用社会保险号码作为病人的标识符。但这样的方法也不是万无一失的,因为并不是每个人都有社会保险号码(特别是儿童和非美国居民),而且很多人会出于个人隐私原因而反对这种做法。

在一些非正式的场合(比方说,音像出租),可以使用电话号码作为标识符。但电话可能是共用的,号码也可能会更改,甚至一个旧的电话号码可能会重新分配给一个不同的人。

由于这些原因,我们一般使用特别指定的标识符(如常飞乘客编号),并使用其他属性(如电话号码和社会保险号码)进行匹配和验证。在任何情况下,当应用程序需要一个外部ID时,都由系统的用户负责提供唯一的ID,而系统必须为用户提供适当的工具来处理异常情况。

在这些技术问题的干扰下,人们很容易忽略基本的概念问题:

两个对象是同一事物时意味着什么?我们很容易为每个对象分配一个ID,或是编写一个用于比较两个实例的操作,但如果这些ID或操作没有对应领域中有意义的区别,那只会使问题更混乱。这就是分配标识的操作通常需要人工输入的原因。例如,支票簿对账软件可以提供一些有可能匹配的账目,但它们是否真的匹配则要由用户最终决定。

三、 模式:VALUE OBJECT 值对象

很多对象没有概念上的标识,它们描述了一个事务的某种特征。值对象。

由于模型中最引人注意的对象往往是ENTITY,而且跟踪每个ENTITY的标识是极为重要的,因此我们很自然地会想到为每个领域对象都分配一个标识。实际上,一些框架确实为每个对象分配了一个唯一的ID。

这样一来,系统就必须处理所有这些ID的跟踪问题,从而导致许多本来可能的性能优化不得不被放弃。此外,人们还需要付出大量的分析工作来定义有意义的标识,还需要开发出一些可靠的跟踪方式,以便在分布式系统或在数据库存储中跟踪对象。同样重要的是,盲目添加无实际意义的标识可能会产生误导。它会使模型变得混乱,并使所有对象看起来千篇一律。

  • 跟踪ENTITY的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。

  • 软件设计要时刻与复杂性做斗争。我们必须区别对待问题,仅在真正需要的地方进行特殊处理。

然而,如果仅仅把这类对象当作没有标识的对象,那么就忽略了它们的工具价值或术语价值。事实上,这些对象有其自己的特征,对模型也有着自己的重要意义。这些是用来描述事物的对象。

  • 用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。VALUE OBJECT被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。

上面这句话,很绕口,我们可以接着看书里提供的一个例子:


颜色是很多现代开发系统的基础库所提供的VALUE OBJECT的一个例子,字符串和数字也是这样的VALUE OBJECT(我们不会关心所使用的是哪一个‚4‛或哪一个‚Q‛)。这些基本的例子非常简单,但VALUE OBJECT并不都这样简单。例如,调色程序可能有一个功能丰富的模型,在这个模型中,可以把功能更强的颜色对象组合起来产生其他颜色。这些颜色可能具有很复杂的算法,通过这些算法的共同计算得到新的VALUE OBJECT。

VALUE OBJECT可以是其他对象的集合。

在房屋设计软件中,可以为每种窗户样式创建一个对象。我们可以将"窗户样式"连同它的高度、宽度以及修改和组合这些属性的规则一起放到‚窗户‛对象中。这些窗户就是由其他VALUE OBJECT组成的复杂VALUE OBJECT。它们进而又被合并到更大的设计元素中,如‚墙‛对象。

VALUE OBJECT甚至可以引用ENTITY。例如,如果我请在线地图服务为我提供一个从旧金山到洛杉矶的驾车风景游路线,它可能会得出一个"路线"对象,此对象通过太平洋海岸公路连接旧金山和洛杉矶。这个“路线”对象是一个VALUE,尽管它所引用的3个对象(两座城市和一条公路)都是ENTITY。

VALUE OBJECT经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。VALUE OBJECT可以用作ENTITY(以及其他VALUE)的属性。我们可以把一个人建模为一个具有标识的ENTITY,但这个人的名字是一个VALUE。

小结:

关于值对象的使用

  1. VALUE OBJECT可以是其他对象的集合。
  2. VALUE OBJECT甚至可以引用ENTITY。
  3. VALUE OBJECT经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。
  4. VALUE OBJECT可以用作ENTITY(以及其他VALUE)的属性。我们可以把一个人建模为一个具有标识的ENTITY,但这个人的名字是一个VALUE。

当我们只关心一个模型元素的属性时,应把它归类为VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT应该是不可变的。不要为它分配任何标识,而且不要把它设计成像ENTITY那么复杂。

VALUE OBJECT所包含的属性应该形成一个概念整体①。例如,street(街道)、city(城市)和postal code(邮政编码)不应是Person(人)对象的单独的属性。它们是整个地址的一部分,这样可以使得Person对象更简单,并使地址成为一个更一致的VALUE OBJECT。如图所示:

三、1.设计VALUE OBJECT

我们并不关心使用的是VALUE OBJECT的哪个实例。由于不受这方面的约束,设计可以获得更大的自由,因此可以简化设计或优化性能。在设计VALUE OBJECT时有多种选择,包括复制、共享或保持VALUE OBJECT不变。

两个人同名并不意味着他们是同一个人,也不意味着他们是可互换的。但表示名字的对象是可以互换的,因为它们只涉及名字的拼写。一个Name对象可以从第一个Person对象复制给第二个Person对象

事实上,这两个Person对象可能不需要自己的名字实例,它们可以共享同一个Name对象(其中每个Person对象都有一个指向同一个名字实例的指针),而无需改变它们的行为或标识。如此一来,当修改其中一个人名字时就会产生问题,这时另一个人的名字也将改变!为了防止这种错误发生,以便安全地共享一个对象,必须确保Name对象是不变的——它不能改变,除非将其整个替换掉。

当一个对象将它的一个属性作为参数或返回值传递给另一个对象时,也会发生同样的问题。一个脱离了其所有者控制的‚流浪‛对象可能会发生任何事情。VALUE的改变可能会破坏所有者的约束条件。这个问题可以通过传递一个不变对象或传递一个副本来解决。

VALUE OBJECT为性能优化提供了更多选择:

这一点可能很重要,因为VALUE OBJECT往往为数众多。房屋设计软件的示例就说明了这一点。如果每个电源插座都是一个单独的VALUE OBJECT,那么在一所房屋的一个设计版本中可能就会有上百个这种VALUE OBJECT。但如果把电源插座看成是可互换的,就只需共享一个电源插座实例,并让所有电源插座都指向这个实例(FLYWEIGHT,[Gamma et al. 1995]中的一个示例)。在大型系统中,这种效果可能会被放大数千倍,而且这样的优化可能决定一个系统是可用的,还是由于数百万个多余对象而变得异常缓慢。这只是无法应用于ENTITY的优化技巧中的一个。

复制和共享哪个更划算取决于实现环境。虽然复制有可能导致系统被大量的对象阻塞,但共享可能会减慢分布式系统的速度。当在两个机器之间传递一个副本时,只需发送一条消息,而且副本到达接收端后是独立存在的。但如果共享一个实例,那么只会传递一个引用,这要求每次交互都要向发送方返回一条消息。

使用共享的情况:

以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:
 节省数据库空间或减少对象数量是一个关键要求时;
 通信开销很低时(如在中央服务器中);
 共享的对象被严格限定为不可变时。

在有些语言和环境中,可以将属性或对象声明为不可变的,但有些却不具备这种能力。这种声明能够体现出设计决策,但它们并不是十分重要。我们在模型中所做的很多区别都无法用当前工具和编程语言在实现中显式地声明出来。例如,我们无法声明ENTITY并自动确保其具有一个标识操作。

但是,编程语言没有直接支持这些概念上的区别并不说明这些区别没有用处。这只是说明我们需要更多的约束机制来确保满足一些重要的规则(这些规则只有在实现中才是隐式的)。命名规则、精心准备的文档和大量讨论都可以强化这些需求。

只要VALUE OBJECT是不可变的,变更管理就会很简单,因为除了整体替换之外没有其他的更改。不变的对象可以自由地共享,像在电源插座的例子中一样。如果垃圾回收是可靠的,那么删除操作就只是将所有指向对象的引用删除。当在设计中将一个VALUE OBJECT指定为不可变时,开发人员就可以完全根据技术需求来决定是使用复制,还是使用共享,因为他们没有后顾之忧——应用程序不依赖于对象的特殊实例。

那么什么时候可以设置值对象可变呢?


定义VALUE OBJECT并将其指定为不可变的是一条一般规则,这样做是为了避免在模型中产生不必要的约束,从而让开发人员可以单纯地从技术上优化性能。如果开发人员能够显式地定义重要约束,那么他们就可以在对设计做出必要调整时,确保不会无意更改重要的行为。这样的设计调整往往特定于具体项目所使用的技术。

下面将有一条示例,来表示设计值对象不可变,能够带来的技术上的优化。

通过VALUE OBJECT来优化数据库。

三、2. 设计包含VALUE OBJECT的关联

前面讨论的与关联有关的大部分内容也适用于ENTITY和VALUE OBJECT。模型中的关联越少越好,越简单越好。

但是,如果说ENTITY之间的双向关联很难维护,那么两个VALUE OBJECT之间的双向关联则完全没有意义。当一个VALUE OBJECT指向另一个VALUE OBJECT时,由于没有标识,说一个对象指向的对象正是那个指向它的对象并没有任何意义的。我们充其量只能说,一个对象指向的对象与那个指向它的对象是等同的,但这可能要求我们必须在某个地方实施这个固定规则。而且,尽管我们可以这样做,并设臵双向指针,但很难想出这种安排有什么用处。因此,我们应尽量完全清除VALUE
OBJECT之间的双向关联。

如果在你的模型中看起来确实需要这种关联,那么首先应重新考虑一下将对象声明为VALUE OBJECT这个决定是否正确。或许它拥有一个标识,而你还没有注意到它。ENTITY和VALUE OBJECT是传统对象模型的主要元素,但一些注重实效的设计人员正逐渐开始使用一种新的元素——SERVICE。

四、模式: SERVICE

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是
SERVICE(服务)。

有些重要的领域操作无法放到ENTITY或VALUE OBJECT中。这当中有些操作从本质上讲是一些活动或动作,而不是事物,但由于我们的建模范式是对象,因此要想办法将它们划归到对象这个范畴里。

有时,一些SERVICE看上去就像是模型对象,它们以对象的形式出现,但除了执行一些操作之外并没有其他意义。这些‚实干家‛(Doer)的名字通常以‚Manager‛之类的名字结尾。它们没有自己的状态,而且除了所承载的操作之外在领域中也没有其他意义。尽管如此,该方法至少为这些特立独行的行为找到了一个容身之所,避免它们扰乱真正的模型对象。

  • 一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为ENTITY或VALUE OBJECT的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。

SERVICE是作为接口提供的一种操作,它在模型中是独立的,它不像ENTITY和VALUE OBJECT那样具有封装的状态。SERVICE是技术框架中的一种常见模式,但它们也可以在领域层中使用。

所谓SERVICE,它强调的是与其他对象的关系。与ENTITY和VALUE OBJECT不同,它只是定义了能够为客户做什么。SERVICE往往是以一个活动来命名,而不是以一个ENTITY来命名,也就是说,它是动词而不是名词。SERVICE也可以有抽象而有意义的定义,只是它使用了一种与对象不同的定义风格。

SERVICE也应该有定义的职责,且这种职责以及履行它的接口也应该作为领域模型的一部分来加以定义。操作名称应来自于UBIQUITOUS LANGUAGE,如果UBIQUITOUS LANGUAGE中没有这个名称,则应将其引入到UBIQUITOUS LANGUAGE中。参数和结果应该是领域对象。

使用SERVICE时应谨慎,它们不应该替代ENTITY和VALUE OBJECT的所有行为。但是,当一个操作实际上是一个重要的领域概念时,SERVICE很自然就会成为MODEL-DRIVEN DESIGN中的一部分。

使用SERVICE时应谨慎,它们不应该替代ENTITY和VALUE OBJECT的所有行为。但是,当一个操作是一个重要的领域概念时,SERVICE很自然就会成为MODEL-DRIVEN DESIGN中的一部分。将模型中的独立操作声明为一个SERVICE,而不是声明为一个不代表任何事情的虚拟对象,可以避免对任何人产生误导。

好的SERVICE有以下3个特征。

  1. 与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
  2. 接口是根据领域模型的其他元素定义的。
  3. 操作是无状态的。
    (PS:这里所说的无状态是指任何客户都可以使用某个SERVICE的任何实例,而不必关心该实例的历史状态。SERVICE执行时将使用可全局访问的信息,甚至会更改这些全局信息(也就是说,它可能具有副作用)。但SERVICE不保持影响其自身行为的状态,这一点与大多数领域对象不同。)
  • 当领域中的某个重要的过程或转换操作不是ENTITY或VALUE OBJECT的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为SERVICE。定义接口时要使用模型语言,并确保操作名称是UBIQUITOUS LANGUAGE中的术语。此外,应该使SERVICE成为无状态的。

四、1. SERVICE与孤立的领域层

这种模式只重视那些在领域中有重要意义的SERVICE,但SERVICE并不只是在领域层中使用。我们要注意区分属于领域层的SERVICE和那些属于其他层的SERVICE,并划分责任,以便将它们明确地区分开。

文献中所讨论的大多数SERVICE是纯技术的SERVICE,它们都属于基础设施层。领域层和应用层的SERVICE与这些基础设施层SERVICE进行协作。

例如,银行可能有一个用于向客户发送电子邮件的应用程序,当客户的账户余额小于一个特定的临界值时,这个程序就向客户发送一封电子邮件。封装了电子邮件系统的接口(也可能是其他的通知方式)就是基础设施层中的SERVICE。

应用层负责通知的设置,而领域层负责确定是否满足临界值,尽管这项任务可能并不需要使用SERVICE,因为它可以作为‚account‛(账户)对象的职责中。这个银行应用程序可能还负责资金转账。如果设计一个SERVICE来处理资金转账相应的借方和贷方,那么这项功能将属于领域层。资金转账在银行领域语言中是一项有意义的操作,而且它涉及基本的业务逻辑。而纯技术的SERVICE应该没有任何业务意义。

很多领域或应用层SERVICE是在ENTITY和VALUE OBJECT的基础上建立起来的,它们的行为类似于将领域的一些潜在功能组织起来以执行某种任务的脚本。ENTITY和VALUE OBJECT往往由于粒度过细而无法提供对领域层功能的便捷访问。我们在这里会遇到领域层与应用层之间很微妙的分界线。例如,如果银行应用程序可以把我们的交易进行转换并导出到一个电子表格文件中,以便进行分析,那么这个导出操作就是应用层SERVICE。‚文件格式‛在银行领域中是没有意义的,它也不涉及业务规则。

另一方面,账户之间的转账功能属于领域层SERVICE,因为它包含重要的业务规则(如处理相应的借方账户和贷方账户),而且‚资金转账‛是一个有意义的银行术语。在这种情况下,SERVICE自己并不会做太多的事情,而只是要求两个Account对象完成大部分工作。但如果将‚转账‛操作强加在Account对象上会很别扭,因为这个操作涉及两个账户和一些全局规则。

我们可能喜欢创建一个Funds Transfer(资金转账)对象来表示两个账户,外加一些与转账有关的规则和历史记录。但在银行间的网络中进行转账时,仍然需要使用SERVICE。此外,在大多数开发系统中,在一个领域对象和外部资源之间直接建立一个接口是很别扭的。我们可以利用一个FACADE(外观)①将这样的外部SERVICE包装起来,这个外观可能以模型作为输入,并返回一个‚Funds Transfer‛对象。但无论中间涉及什么SERVICE,甚至那些超出我们掌控范围的SERVICE,这些SERVICE都是在履行资金转账的领域职责。


看这张图,会发现应用层,会类似于我们controller的操作。

四、2 粒度

上述对SERVICE的讨论强调的是将一个概念建模为SERVICE的表现力,但SERVICE还有其他有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与ENTITY和VALUE OBJECT耦合。

在大型系统中,中等粒度的、无状态的SERVICE更容易被复用,因为它们在简单的接口背后封装了重要的功能。此外,细粒度的对象可能导致分布式系统的消息传递的效率低下。

如前所述,由于应用层负责对领域对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识泄漏到应用层中。这产生的结果是应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码当中,而领域层会丢失这些知识。明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。

这就是控制领域层中的接口的粒度。

这种模式有利于保持接口的简单性,便于客户端控制并提供了多样化的功能。它提供了一种在大型或分布式系统中便于对组件进行打包的中等粒度的功能。而且,有时SERVICE是表示领域概念的最自然的方式。

四、3 、 对SERVICE的访问

与分离特定职责的设计决策相比,提供对SERVICE的访问机制的意义并不是十分重大。一个‚操作‛对象可能足以作为SERVICE接口的实现。我们很容易编写一个简单的SINGLETON对象来实现对SERVICE的访问。从编码惯例可以明显看出,这些对象只是SERVICE接口的提供机制,而不是有意义的领域对象。只有当真正需要实现分布式系统或充分利用框架功能的情况下才应该使用复杂的架构。

五、模式:MODULE(也称为PACKAGE)

MODULE是一个传统的、较成熟的设计元素。虽然使用模块有一些技术上的原因,但主要原因却是‚认知超载。MODULE为人们提供了两种观察模型的方式,
一是可以在MODULE中查看细节,而不会被整个模型淹没,二是观察MODULE之间的关系,而不考虑其内部细节。

领域层中的MODULE应该成为模型中有意义的部分,MODULE从更大的角度描述了领域。

每个人都会使用MODULE,但却很少有人把它们当做模型中的一个成熟的组成部分。代码按照各种各样的类别进行分解,有时是按照技术架构来分割的,有时是按照开发人员的任务分工来分割的。众所周知,MODULE之间应该是低耦合的,而在MODULE的内部则是高内聚的。

  • 只要两个模型元素被划分到不同的MODULE中,它们的关系就不如原来那样直接,这会使我们更难理解它们在设计中的作用。MODULE之间的低耦合可以将这种负面作用减至最小,而且在分析一个MODULE的内容时,只需很少地参考那些与之交互的其他MODULE。

  • 同时,在一个好的模型中,元素之间是要协同工作的,而仔细选择的MODULE可以将那些具有紧密概念关系的模型元素集中到一起。将这些具有相关职责的对象元素聚合到一起,可以把建模和设计工作集中到单一MODULE中,这会极大地降低建模和设计的复杂性,使人们可以从容应对这些工作。

  • 像领域驱动设计中的其他元素一样,MODULE是一种表达机制。MODULE的选择应该取决于被划分到模块中的对象的意义。当你将一些类放到MODULE中时,相当于告诉下一位看到你的设计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那MODULE就是这个故事的各个章节。模块的名称表达了其意义。这些名称应该被添加到UBIQUITOUS LANGUAGE中。你可能会向一位业务专家说‚现在让我们讨论一下‘客户’模块‛,这就为你们接下来的对话设定了上下文。

小结:

  • 选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。

仅仅研究概念关系是不够的,它并不能替代技术措施。这二者是相同问题的不同层次,都是必须要完成的。但是,只有以模型为中心进行思考,才能得到更深层次的解决方案,而不是随便找一个解决方案应付了事。当必须做出一个折中选择时,务必保证概念清晰,即使这意味着MODULE之间会产生更多引用,或者更改MODULE偶尔会产生‚涟漪效应‛。开发人员只要理解了模型所描述的内容,就可以应付这些问题。

五、1 敏捷的MODULE

MODULE需要与模型的其他部分一同演变。这意味着MODULE的重构必须与模型和代码一起进行。但这种重构通常不会发生。更改MODULE可能需要大范围地更新代码。这些更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具(如源代码控制系统)的使用。因此,MODULE结构和名称往往反映了模型的较早形式,而类则不是这样。

在MODULE选择的早期,有些错误是不可避免的,这些错误导致了高耦合,从而使MODULE很难进行重构。而缺乏重构又会导致问题变得更加严重。克服这一问题的唯一方法是接受挑战,仔细地分析问题的要害所在,并据此重新组织MODULE。

五、2 通过基础设施打包时存在的隐患

技术框架对打包决策有着极大的影响,有些技术框架是有帮助的,有些则要坚决抵制。

一个非常有用的框架标准是LAYERED ARCHITECTURE,它将基础设施和用户界面代码放到两组不同的包中,并且从物理上把领域层隔离到它自己的一组包中。

最好把事情变简单。要极度简化技术分层规则,要么这些规则对技术环境特别重要,要么这些规则真正有助于开发。例如,将复杂的数据持久化代码从对象的行为方面提取出来可以使重构变得更简单。

  • 除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中(如果不能放在同一个对象中的话)。

  • 利用打包把领域层从其他代码中分离出来。否则,就尽可能让领域开发人员自由地决定领域对象的打包方式,以便支持他们的模型和设计选择。

如果代码是基于声明式设计(第10章有这方面的讨论)生成的,则是一种例外情况。在这种情况下,开发人员无需阅读代码,因此为了不碍事最好将代码放到一个单独的包中,这样就不会搞乱开发人员实际要处理的设计元素。

领域模型中的每个概念都应该在实现元素中反映出来。ENTITY、VALUE OBJECT、它们之间的关联、领域SERVICE以及用于组织元素的MODULE都是实现与模型直接对应的地方。实现中的对象、指针和检索机制必须直接、清楚地映射到模型元素。如果没有做到这一点,就要重写代码,或者回头修改模型,或者同时修改代码和模型。
不要在领域对象中添加任何与领域对象所表示的概念没有紧密关系的元素。

领域对象的职责是表示模型。当然,其他一些与领域有关的职责也是必须要实现的,而且为了使系统工作,也必须管理其他数据,但它们不属于领域对象。

本章介绍的4种模式为对象模型提供了构造块。但MODEL-DRIVEN DESIGN并不是说必须将每个元素都建模为对象。一些工具还支持其他的模型范式,如规则引擎。

项目需要在它们之间做出契合实际的折中选择。这些其他的工具和技术是MODEL-DRIVEN DESIGN的补充,而不是要取而代之。

六、 建模范式

MODEL-DRIVEN DESIGN要求使用一种与建模范式协调的实现技术。人们曾经尝试了大量的建模范式,但在实践中只有少数几种得到了广泛应用。目前,主流的范式是面向对象设计,而且现在的大部分复杂项目都开始使用对象。

然而,虽然对象建模的概念很简单,但它的丰富功能足以捕获重要的领域知识。而且它从一开始就获得了开发工具的支持,使得模型可以在软件中表达出来。

是目前大部分采用MODEL-DRIVEN DESIGN的项目很明智地使用面向对象技术作为系统核心。它们不会被束缚在只有对象的系统里,因为对象已经成为内业的主流技术,人们目前使用的几乎所有的技术都有与之对应的集成工具。

领域模型不一定是对象模型。例如,使用Prolog语言实现的MODEL-DRIVEN DESIGN,它的模型是由逻辑规则和事实构成的。

模型范式为人们提供了思考领域的方式。这些领域的模型由范式
塑造成型。结果就得到了遵守范式的模型,这样的模型可以用支持对应建模风格的工具来有效地实现。不管在项目中使用哪种主要的模型范式,领域中都会有一些部分更容易用某种其他范式来表达。当领域中只有个别元素适合用其他范式时,开发人员可以接受一些蹩脚的对象,以使整个模型保持一致。

但是,当领域的主要部分明显属于不同的范式时,明智的做法是用适合各个部分的范式对其建模,并使用混合工具集来进行实现。当领域的各个部分之间的互相依赖性较小时,可以把用另一种范式建立的子系统封装起来,例如,只有一个对象需要调用的复杂数学计算。其他时候,不同方面之间的关系更为复杂,例如,对象的交互依赖于某些数学关系的时候。

这就是将业务规则引擎或工作流引擎这样的非对象组件集成到对象系统中的动机。混合使用不同的范式使得开发人员能够用最适当的风格对特殊概念进行建模。此外,大部分系统都必须使用一些非对象的技术基础设施,最常见的就是关系数据库。但是在使用不同的范式后,要想得到一个内聚的模型就比较难了,而且让不同的支持工具共存也较为复杂。当开发人员在软件中无法清楚地辨认出一个内聚的模型时,MODEL-DRIVEN DESIGN就会被抛诸脑后,尽管这种混合设计更需要它。

六、3 在混合范式中坚持使用MODEL-DRIVEN DESIGN

在面向对象的应用程序开发项目中,有时会混合使用一些其他的技术,规则引擎就是一个常见的例子。

一个包含丰富知识的领域模型可能会含有一些显式的规则,然而对象范式却缺少用于表达规则和规则交互的具体语义。尽管可以将规则建模为对象(而且常常可以成功地做到),但对象封装却使得那些针对整个系统的全局规则很难应用。

规则引擎技术非常有吸引力,因为它提供了一种更自然、声明式的规则定义方式,能够有效地将规则范式融合到对象范式中。逻辑范式已经得到了很好的发展并且功能强大,它是对象范式的很好补充,使其可以扬长避短。

  • 但人们并不总是能够从规则引擎的使用中得到预期结果。有些产品并不能很好地工作。有些则缺少一种能够显示出衔接两种实现环境的模型概念相关性的无缝视图。一个常见的结果是应用程序被割裂成两部分:一个是使用了对象的静态数据存储系统,另一个是几乎完全与对象模型失去联系的某种规则处理应用程序。

  • 重要的是在使用规则的同时要继续考虑模型。团队必须找到能够同时适用于两种实现范式的单一模型。虽然这并非易事,但还是可以办到的,条件是规则引擎支持富有表达力的实现方式。如果不这样,数据和规则就会失去联系。与领域模型中的概念规则相比,引擎中的规则更像是一些较小的程序。只有保持规则与对象之间紧密、清晰的关系,才能确保显示出这二者所表达的含义。

如果没有无缝的环境,就要完全靠开发人员提炼出一个由清晰的基本概念组成的模型,以便完全支撑整个设计。

将各个部分紧密结合在一起的最有效工具就是健壮的UBIQUITOUS LANGUAGE,它是构成整个异构模型的基础。坚持在两种环境中使用一致的名称,坚持用UBIQUITOUS LANGUAGE讨论这些名称,将有助于消除两种环境之间的鸿沟。

虽然MODEL-DRIVEN DESIGN不一定是面向对象的,但它确实需要一种富有表达力的模型结构实现,无论是对象、规则还是工作流,都是如此。如果可用工具无法提高表达力,就要重新考虑选择工具。缺乏表达力的实现将削弱各种范式的优势。

当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下4条经验规则。

 不要和实现范式对抗。我们总是可以用别的方式来考虑领域。找到适合于范式的模型概念。

 把通用语言作为依靠的基础。即使工具之间没有严格联系时,语言使用上的高度一致性也能防止各个设计部分分裂。

 不要一味依赖UML。有时固定使用某种工具(如UML绘图工具)将导致人们通过歪曲模型来使它更容易画出来。例如,UML确实有一些特性很适合表达约束,但它并不是在所有情况下都适用。有时使用其他风格的图形(可能适用于其他范式)或者简单的语言描述比牵强附会地适应某种对象视图更好。

 保持怀疑态度。工具是否真正有用武之地?不能因为存在一些规则,就必须使用规则引擎。规则也可以表示为对象,虽然可能不是特别优雅。多个范式会使问题变得非常复杂。

在决定使用混合范式之前,一定要确信主要范式中的各种可能性都已经尝试过了。尽管有些领域概念不是以明显的对象形式表现出来的,但它们通常可以用对象范式来建模。

关系范式是范式混合的一个特例。作为一种最常用的非对象技术,关系数据库与对象模型的关系比其他技术与对象模型的关系更紧密,因为它作为一种数据持久存储机制,存储的就是对象。

你可能感兴趣的:(阅读-领域驱动设计第五章-软件中所表示的模型)