阅读笔记-领域驱动设计第九章--将隐式概念转变为显式概念

深层建模听起来很不错,但是我们要如何实现它呢?

深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。

深层建模的第一步就是要设法在模型中表达出领域的基本概念。随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。

  • 若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。

有时,这种从隐式概念到显式概念的转换可能是一次突破,使我们得到一个深层模型。但更多的时候,突破不会马上到来,而需要我们在模型中显式表达出许多重要概念,并通过一系列重构不断地调整对象职责、改变它们与其他对象的关系、甚至多次修改对象名称,在这之后,突破才会姗姗而来。最后,所有事情都变得清晰了。但是要实现上述过程,必须首先识别出以某种形式存在的隐含概念,无论这些概念有多么原始。

一、概念挖掘

开发人员必须能够敏锐地捕捉到隐含概念的蛛丝马迹,但有时他们必须主动寻找线索。要挖掘出大部分的隐含概念,需要开发人员去倾听团队语言、仔细检查设计中的不足之处以及与专家观点相矛盾的地方、研究领域相关文献并且进行大量的实验。

一、1.倾听语言

你可能会想起这样的经历:用户总是不停地谈论报告中的某一项。该项可能来自各种对象的参数汇编,甚至还可能来自一次直接的数据库查询。同时,应用程序的另一部分也需要这个数据集来进行显示、报告或其他操作。但是,你却一直认为没有必要为此创建一个对象。

也许你一直没有真正理解用户想通过某个特定术语传达的东西,也没有意识到它的重要性。然后,你突然灵机一动。原来,报告中该项名称给出了一个重要的领域概念。不管专家们如何反应,你开始在白板上画模型图了(之前你也一直这么做)。用户会帮助你修正新模型连接方面的细节,但你明显感到讨论的质量有所提高。你和用户可以更加准确地理解对方,并且可以更加自然地用模型交互来演示特定场景。领域模型的语言也变得更加强大。然后,你可以重构代码来反映新模型,同时也会发现你的设计变得更加清晰了。

  • 倾听领域专家使用的语言。有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正过你的用词(也许是很委婉的提醒)?当你使用某个特定词语时,他们脸上是否已经不再流露出迷惑的表情?这些都暗示了某个概念也许可以改进模型。

这不同于原来的“名词即对象”概念。听到新单词只是个开头,然后我们还要进行对话、消化知识,这样才能挖掘出清晰实用的概念。如果用户或领域专家使用了设计中没有的词汇,这就是个警告信号。而当开发人员和领域专家都在使用设计中没有的词汇时,那就是一个倍加严重的警告信号了。

或者,应该把这种警告看成一次机会。UBIQUITOUS LANGUAGE是由遍布于对话、文档、模型图甚至代码中的词汇构成的。如果出现了设计中没有的术语,就可以把它添加到通用语言中,这样也就有机会改进模型和设计了.

接下来看一个示例:听出运输模型中缺失的一个概念

团队已经开发出了可用来预订货物的有效应用程序。现在他们开始开发“作业支持”应用程序,此程序可帮助工作人员管理工作单,这些工作单用于安排起始地和目的地的货物装卸以及在不同货轮之间转运时需要的货物装卸。

预定应用程序使用一个路线引擎来安排货物行程。运输过程的每段行程都作为一行数据存储在数据库表中,其中指定了装载该货物的船名航次(某一货轮的某一航次)ID、装货地点以及卸货地 点,如图9-1所示

让我们来听听开发人员和运输专家之间的对话吧(对话已被高度简化)。

这位开发人员回去与负责路线处理的人员进行讨论。他们仔细研究了这会给模型和设计带来什么影响和变化,在必要的时候也去请教了运输专家。最后,他们得到了图9-3所示的模型:
图 9-3

接下来,开发人员对代码进行了重构,以使它能反映出新的模型。在一周内,他们很快对代码作出了一系列的修改,每次修改都进行两到三次重构。但是他们还没有对预订应用程序中的航海日程报告进行简化,而简化工作将会在接下来的一周开始进行。

这位开发人员一直都在仔细倾听运输专家的见解,并注意到“航海日程”概念的重要性。事实上,所有的数据都已收集,在航海日程报告中也已隐含了操作行为,但是,把显式的Itinerary对象作为模型的一部分给他们带来了新的机会。

通过重构得到显式的Itinerary对象的益处是:
(1) 更明确地定义Routing Service接口;
(2) 将Routing Service与预订数据库表解耦——Routing Service无需关心存储逻辑;
(3) 明确了预订应用程序和作业支持应用程序之间的关系(即共享Itinerary对象);
(4) 减少重复,因为Itinerary可同时为预订报表和作业支持应用程序提供装货/卸货时间;
(5) 从预订报表中删除领域逻辑并将其移至独立的领域层;
(6) 扩充了UBIQUITOUS LANGUAGE,使得开发人员和领域专家之间或者开发人员内部能够更准确地讨论模型和设计。

一、2 、 检查不足之处

有时,你很难意识到模型中丢失了什么概念。也许你的对象能够实现所有的功能,但是有些职责的实现却很笨拙。而有时,你虽然能够意识到模型中丢失了某些东西,但是却无法找到解决方案。

这个时候,你必须积极地让领域专家参与到讨论中来。如果你足够幸运,这些专家可能会愿意一起思考各种想法,并通过模型来进行验证。如果你没那么幸运,你和你的同事就不得不自己思索出不同的想法,让领域专家对这些想法进行判断,并注意观察专家的表情是认同还是反对。

案例说明:摸索利息计算模型


图9-4 笨拙的模型

Asset(资产),Interest Calculator(利息计算器)、
会计期(accounting period)、Payment History
(付款历史)、Fee Calculator(费用计算机器)、
下面的故事以一家假想的金融公司为背景,该公司经营商业贷款和其他一些生息资产。公司开发了一个用于跟踪这些投资及收益的应用程序,通过一项一项地添加功能来使它不断地发展。每天晚上,公司都会运行一个批处理脚本,用于计算当天所生成的利息和费用,并把它们相应地记录到公司的财务软件中。

晚间批处理脚本会遍历每笔Asset(资产),并让其执行 ,按
照当天的日期来计算利息。然后,该脚本会接收返回值(收益金额),并将它和指定分类账的名称一起发送给一个SERVICE(这个SERVICE提供了记账程序的公共接口)。再由记账软件将收入金额过账到指定的分类账中。这个脚本还会对每笔Asset当日的手续费作类似的处理,并记录到另一个不同的分类账中。

负责这个程序的一位开发人员一直在费力地应对日益复杂的利息计算。她开始怀疑应该能找到一个更适合完成此项任务的模型。于是,她向她熟识的领域专家寻求帮助,希望专家可以协助她深入研究这个问题。
图 9-5



由于Calculator类并没有直接与设计中的其他部分相关联,所以这其实是一个非常简单的重构。这位开发人员只需花几个小时就能够通过重写单元测试来验证新的语言,第二天新的设计就

可以用了。最终,她得到了下面的模型。
图9-8 重构后的深层模型

Accrual schedule(应计计划) Daily Compound Interest(复利率)
Monthly Fee(月费) Income Accrual(利息收益)payment(付款方式)
在重构后的应用程序中,夜间批处理脚本会通知每个Asset执行
calculateAccrualsThroughDate()。其返回值是Accrual的集合,而其中的每笔金额都会过账到指定的分类账中。

新模型具有几个优点,包括:

(1) 术语“应计费用(accrual)”使UBIQUITOUS LANGUAGE更丰富;
(2) 将应计费用从付款中分离出来;
(3) 将领域知识(如过账到哪个分类账)从脚本中移出来,并放到领域层中;
(4) 将费用与利息统一,既能够符合业务逻辑,又可消除重复代码;
(5) 新形式的费用和利息可以通过Accrual Schedule直接添加到模型中。

一、 4. 查阅书籍

在寻找模型概念时,不要忽略一些显而易见的资源。在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层 的认识。

示例:借助参考书来设计利息计算模型

让我们设想一下前面讨论的投资跟踪应用程序的另一个场景。与前面一样,这个故事的开头也是开发人员意识到设计变得越来越笨拙,特别是Interest Calculator。但是在这个场景中,领域专家主要负责其他工作,他对帮助软件开发项目并不十分感兴趣。在这里,开发人员不能指望专家与其一起进行头脑风暴,帮助她探寻隐藏于表象之下的遗漏概念。

于是,她去了书店。随意翻阅了几本书之后,她找到了一本自己比较喜欢的会计学入门书籍, 并把它粗略浏览了一遍。她发现书中有一整套明确定义的概念体系。其中一段文字给了她特别大的启发:

开发人员再也不用自己去重新编造一个会计学出来了。在与其他开发人员进行了一些讨论之后,她设计出了一个模型,如图9-9所示。

她还没有认识到收入是由Asset产生的,所以模型中依然含有Calculator。分类账的概念还是包含在应用程序中,而不是在它本应归属的领域层中。但是,她确实将付款从应计收入中分离出来了(这曾是最大的问题)并且她将“应计费用”这个词引入到模型和UBIQUITOUS LANGUAGE中。在之后的迭代过程中,模型还会得到进一步的精化。

当然,看书与咨询领域专家并不冲突。即便能够从领域专家那里得到充分的支持,花点时间从文献资料中大致了解领域理论也是值得的。虽然许多业务并不会像会计学或金融行业那样具有极其细致的模型,但大多数领域中都有一些擅于思考的人,他们已组织并抽象出了业务的一些通用的惯例。

开发人员还有另一个选择,就是阅读在此领域中有过开发经验的软件专业人员编写的资料。

例如,《分析模式》[Fowler 1997]一书的第6章可能会为她提供一个完全不同的思考方向——无论这个方向会让开发变得更好还是更糟。阅读书籍并不能提供现成的解决方案,但可以为她提供一些全新的实验起点,以及在这个领域中探索过的人总结出来的经验。这样可以避免开发人员重复设计已有的概念。

一、 5 尝试再尝试

上面的例子并没有显示出不断尝试和出错的次数。在讨论过程中,我可能尝试六七种不同的思路,然后找到一个看起来足够清晰且实用的概念,并在模型中尝试它。后面,随着经验的积累和知识的消化,我们会有更好的想法,最终,这个概念至少会被替换一次。因此,建模人员/设计人员绝对不能固执己见。

并不是所有这些方向性的改变都毫无用处。每次改变都会把开发人员更深刻的理解添加到模型中。每次重构都使设计变得更灵活并且为那些可能需要修改的地方做好准备。 我们其实别无选择。只有不断尝试才能了解什么有效什么无效。企图避免设计上的失误将会导致开发出来的产品质量低劣,因为没有更多的经验可用来借鉴,同时也会比进行一系列快速实验更加费时

二、如何为那些不太明显的概念建模

面向对象范式会引导我们去寻找和创造特定类型的概念。所有事物(即使是像“应计费用”这种非常抽象的概念)及其操作行为是大部分对象模型的主要部分。

它们就是面向对象设计入门书籍所讲到的“名词和动词”。但是,其他重要类别的概念也可以在模型中显式地表现出来。
下面我将会描述3个这样的类别,我在开始接触对象时,对它们的认识并不够清晰。我每学会一个这样的类别,就会让设计变得更加清晰深刻。

二、 1.显式的约束

约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。

有时,约束很自然地存在于对象或方法中。Bucket(桶)对象必须满足一个固定规则——内容(contents)不能超出它的容量(capacity)。

这样一个简单的固定规则可以在每次可改变内容的操作中使用一个逻辑判断来保证。

这里的逻辑非常简单,规则也很明显。但是不难想象,在更复杂的类中这个约束可能会丢失。让我们把这个约束提取到一个单独的方法中,并用清晰直观的名称来表达它的意义。


这两个版本的代码都实施了约束,但是第二个版本与模型的关系更为明显(这也是MODEL-DRIVEN DESIGN的基本需求)。这个规则十分简单,使用最初形式的代码也很容易理解,但如果要是执行的规则比较复杂的话,它们就会像所有隐式概念一样淹没掉被约束的对象或操作。

将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这条约束。现在这个约束条件就是 一个“有名有姓”的概念了,我们可以用它
的名字来讨论它。这种方式也为约束的扩展提供了空间。比这更复杂的规则很容易就会产生比其调用者(在这里就是pourIn方法)更长的方法。这样,调用者就可以简单一些,并且只专注于处理自己的任务,而约束条件则可以根据需要进行扩展。

这种独立方法为约束预留了一定的增加空间,但是在很多时候,约束条件是无法用单独的方法来轻松表达的。或者,即使方法自身能够保持其简单性,但它可能会调用一些信息,但对于对象的主要职责而言,这些信息毫无用处。这种规则可能就不适合放到现有对象中。

下面是一些警告信号,表明约束的存在正在扰乱其“宿主对象”(Host Object)的设计。

(1) 计算约束所需的数据从定义上看并不属于这个对象。
(2) 相关规则在多个对象中出现,造成了代码重复或导致不属于同一族的对象之间产生了继 承关系。
(3) 很多设计和需求讨论是围绕这些约束进行的,而在代码实现中,它们却隐藏在过程代码中。

如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出但在模型中却不明 显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合。

示例:复核:超订策略

在第1章中,我们讨论了一个常见的运输业务惯例:预订超出运输能力10%的货物。(货运公司的经验表明,这种程度的超定可以抵消因客户临时取消订单而空出来的舱位,这样货轮基本能够满载起航。)

通过加入一个新类来反映Voyage和Cargo关联中的约束,该约束不管是在图表中还是在代码中都能显式地体现出来,如图9-11所示

Policy(策略)

二、2 将过程建模为领域对象

首先要说明的是,我们都不希望过程变成模型的主要部分。对象是用来封装过程的,这样我们只需考虑对象的业务目的或意图就可以了。

在这里,我们讨论的是存在于领域中的过程,我们必须在模型中把这些过程表示出来。否则当这些过程显露出来时,往往会使对象设计变得笨拙。

本章的第一个例子描述了用来安排货运路线的运输系统。安排路线的过程具有业务意义。 SERVICE是显式表达这种过程的一种方式,同时它还会将异常复杂的算法封装起来。

如果过程的执行有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的STRATEGY。类似策略模式

过程是应该被显式表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当作计算机程序机制的一部分?

  • 约束和过程是两大类模型概念,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。

有些类别的概念很实用,但它们可应用的范围要窄很多。为了使本章的讨论更全面,我会探 讨一个更特殊但也非常常用的概念——规格(specification)。“规格”提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它们显式地表示出来。

SPECIFICATION是我与Martin Fowler[Evans and Fowler 1997]协作开发出来的。这个概念看起来很简单,但是应用和实现中起来却很微妙,因此在本节中会有大量的细节描述。在第10章中还会继续讨论SPECIFICATION,并对这种模式进行扩展。在阅读完接下来对该模式的初步解释后,你可以跳过9.2.4节,等到你真正想要应用这种模式时再回来阅读也不迟。

二、3 、 模式:SPECIFICATION


但是并非所有规则都如此简单。在同一个Invoice(发票)类中,还有另外一个规则anInvoice.isDelinquent(),它一开始也是用来检查Invoice是否过期的,但仅仅是开始部分。

根据客户账户状态的不同,可能会有宽限期政策。一些拖欠票据正准备再一次发出催款通知,而另一些则准备发给收账公司。此外,还要考虑客户的付款历史纪录、公司在不同产品线上的政策等。Invoice作为付款请求是明白无误的,但它很快就会消失在大量杂乱的规则计算代码中。Invoice还会发展出对领域类和子系统的各种依赖关系,而这些领域类和子系统与Invoice的基本含义无关。

到了这一步,为了简化Invoice类,开发人员通常会将规则计算代码重构到应用层中(在这里就是账单收集应用程序)。现在规则已经从领域层中分离出来,留下了一个纯粹的数据对象,它将不再表达本来应该在业务模型中表示的规则。这些规则需要保留在领域层中,但是把它们放到被其约束的对象(在这里是Invoice)里又不合适。此外,计算规则的方法中到处都是条件代码,这也使得规则变得复杂难懂。

那些使用逻辑编程范式的开发人员会用一种不同的方式来处理这种情况。这种规则被称为谓词。谓词是指计算结果为“真”或“假”的函数,并且可以使用操作符(如AND和OR)把它们连接起来以表达更复杂的规则。通过谓词,我们可以显式地声明规则并在Invoice中使用这些规则。但前提是必须使用逻辑范式。

认识到这一点后,人们已经开始尝试以对象的形式来实现逻辑规则。在这些尝试中,有些很成熟,有些则很幼稚。有些很激进,有些则很谨慎。有些被证明很有价值,有些则被当作失败的试验丢到一边。虽然项目允许进行几次这样的尝试,但是,有一件事情是很清楚的:无论这个想法多么吸引人,完全用对象来实现逻辑可是个大工程。(毕竟,逻辑编程本身就是一套建模和设计范式。)

  • 业务规则通常不适合作为ENTITY或VALUE OBJECT的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。

  • 逻辑编程提供了一种概念,即“谓词”这种可分离、可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念过于通用,在表达设计意图方面,它的针对性不如专门的设计那么好。

幸运的是,我们并不真正需要完全实现逻辑编程即可从中受益。大部分规则可以归类为几种特定的情况。我们可以借用谓词概念来创建可计算出布尔值的特殊对象。那些难于控制的测
试方法可以巧妙地扩展出自己的对象。它们都是些小的真值测试,可以提取到单独的VALUE OBJECT中。而这个新对象则可以用来计算另一个对象,看看谓词对那个对象的计算是为“真”



换言之,这个新对象就是一个规格。SPECIFICATION(规格)中声明的是限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。SPECIFICATION有多种用途,其中一种体现了最基本的概念,这种用途是:SPECIFICATION可以测试任何对象以检验它们是否满足指定的标准。

  • 为特殊目的创建谓词形式的显式的VALUE OBJECT。SPECIFICATION就是一个谓词,可用来确定对象是否满足某些标准。

许多SPECIFICATION都是具有特殊用途的简单测试,就像在拖欠票据示例中的规格一样。当规则很复杂时,可以扩展这种概念,对简单的规格进行组合,就像用逻辑运算符把多个谓词组合起来一样。(这种技术将在下一章中讨论。)基本模式保持不变,并且提供了一种从简单模型过渡到复杂模型的途径。

拖欠票据的例子可以使用SPECIFICATION来建模,如图9-13所示。在规格中声明拖欠的含义,对任意的Invoice对象进行计算并做出判断。

SPECIFICATION中,这真是一次不错的简化。我们可以用简单直接的方式为SPECIFICATION提供完成其职责所需的信息。

SPECIFICATION的基本概念非常简单,这能帮助我们思考领域建模问题。但是MODEL-DRIVEN DRSIGN要求我们开发出一个能够把概念表达出来的有效实现。要实现这个目标,必须要更深入地挖掘应用这个模式的方法。

只要恰当地应用模式,就可以得出一整套如何解决领域建模问题的思路,同时也可以从这种长时间搜寻有效实现的经验中受益。下面的SPECIFICATION讨论详细介绍了功能和实现方法的多种选择。模式并不像菜谱那么死板。它可以让你以模式的经验为起点来开发自己的解决方案,并为你讨论手头工作提供了语言。

在第一次阅读时,你可以快速浏览关键概念。以后碰到具体情况时,可以再回过头来阅读并从细节讨论中获取经验。然后就可以开始设计你自己的解决方案了。

二、4 SPECIFICATION的应用和实现

SPECIFICATION最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下3个目的中的一个或多个,我们可能需要指定对象的状态。

(1) 验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备。
(2) 从集合中选择一个对象(如上述例子中的查询过期发票)。
(3) 指定在创建新对象时必须满足某种需求。

这3种用法(验证、选择和根据要求来创建)从概念层面上来讲是相同的。如果没有诸如SPECIFICATION这样的模式,相同的规则可能会表现为不同的形式,甚至有可能是相互矛盾的形式。

这样就会丧失概念上的统一性。通过应用SPECIFICATION模式,我们可以使用一致的模型,尽管在实现时可能需要分开处理。

验证

规格的最简单用法是验证,这种用法也最能直观地展示出它的概念,如图9-14所示



现在,假设当销售人员看到一个欠账客户的信息时,系统需要显示一个红旗标识。我们只需要在客户类中编写一个方法即可,类似于下面这段代码:

选择(或查询)

验证是对一个独立的对象进行测试,检查它是否满足某些标准,然后客户可能根据验证的结论来采取行动。另一种常见需求是根据某些标准从对象集合中选择一个子集。SPECIFICATION概念同样可以在此应用,但是实现问题会有所不同。

假设应用程序的需求是列出所有拖欠发票的客户。那么从理论上说,我们依然可以使用之前定义的Delinquent Invoice Specification,但实际上我们可能不得不去修改它的实现。为了证明二者的概念是相同的,让我们首先假设发票的数量很少,可能已经全部装入内存了。在这种情况下,验证功能的最直接实现方式依然可用。Invoice Repository可以用一个一般化的方法来基于SPECIFICATION选择Invoice:

上面这行代码建立了操作背后的概念。当然,Invoice对象可能并不在内存中。也有可能会有成千上万个Invoice对象。在典型的业务系统中,数据很可能会存储在关系数据库中。我们在前面的章节中曾经指出,在与其他技术交互使用时,很容易分散我们对模型的注意力。

关系数据库具有强大的查询能力。我们如何才能充分利用这种能力来有效解决这一问题,同时又能保留SPECIFICATION模型呢?MODEL-DRIVEN DESIGN要求模型与实现保持同步,但它同时也
让我们可以自由选择能够准确捕捉模型意义的实现方式。幸运的是,SQL是用于编写SPECIFICATION的一种很自然的方式。

下面是个简单的例子,其中查询被封装在验证规则所在的类中。我们在Invoice Specification中添加了一个方法,该方法在Delinquent Invoice Specification子类中得以实现:

SPECIFICATION与REPOSITORY的搭配非常合适,REPOSITORY作为一种构造块机制,提供了对领域对象的查询访问,并且把数据库接口封装起来

现在的设计有一些问题。最重要的问题是,表结构的细节本应该被隔离到一个映射层中(这个映射层把领域对象关联到关系表),现在却泄漏到了DOMAIN LAYER中。这样一来,这些表结构信息发生了隐性的重复,因此导致对Invoice和Customer对象的修改和维护变得很麻烦,因为现在必须在多个地方跟踪它们的映射变化。但是,这个例子只是一个简单的例证,用来说明如何将规则放在一个地方。一些对象关系映射框架提供了用模型对象和属性来表达这种查询的方式,并在基础设施层中创建实际的SQL语句。这样就可以两全其美了。
如果无法把SQL语句创建到基础设施中,还可以重写一个专用的查询方法并把它添加到Invoice Repository中,这样就把SQL语句从领域对象中分离出来了。为了避免在REPOSITORY中嵌入规则,必须采用更为通用的方式来表达查询,这种方式不捕捉规则但是可以通过组合或放置在上下文中来表达规则。

这段代码将SQL臵于REPOSITORY中,而应该使用哪个查询则由SPECIFICATION来控制。SPECIFICATION中并没有定义完整的规则,但规则的核心已位于其中——指明了什么条件构成了拖欠(即超过宽限期)。

现在,REPOSITORY中包含的查询非常具有针对性,可能只适用于这种情况。虽然这是可以接受的,但是根据拖欠发票在过期发票中所占数量的不同,我们可以选择一种更通用REPOSITORY解决方案,使得性能仍然很好,同时又使SPECIFICATION的使用更易理解。



因为我们取出了更多Invoice并在内存中对其进行筛选,上面的代码会有性能方面的影响。这种以降低性能来实现更好的职责分离的代价是否可以接受完全取决于环境因素。SPECIFICATION和 REPOSITORY之间的交互有很多种实现方式,不但能够利用开发平台的优势,还可以保证基本职责的实施。

有时,为了改善性能(或者更有可能是为了加强安全性),我们可能把查询实现为服务器上 的存储过程。在这种情况下SPECIFICATION可能只带有存储过程允许的参数。除此之外,这些不 同实现之间的模型并没有什么不同。我们可以自由选择实现方式,除非模型中有特别的约束条件。这么做的代价是更加难于编写和维护查询。

上面的讨论基本上没有涉及将SPECIFICATION与数据库结合时所面临的挑战,我并不想在这里说明所有可能需要考虑的问题,而只是想简单介绍一下必须要做出的选择。

根据要求来创建(生成)

很多计算机程序都能够生成一些工件,这些工件是需要被指定的。当你在字处理软件文档中插入图片时,文字会环绕在图片周围。你已指定了图片的位臵,可能也指定了文字环绕的样式。这样,字处理软件就可以按照你指定的规格来将页面上的文字摆放到正确的位臵。

尽管乍看起来并不明显,但是这种SPECIFICATION概念与应用于验证和选择的规格并无二致。都是在为尚未创建的对象指定标准。但是,SPECIFICATION的实现则会大不相同。这种SPECIFICATION
与查询不同,它不用来过滤已存在对象;也与验证不同,并不用来测试已有对象。在这里,我们要创建或重新配臵满足SPECIFICATION的全新对象或对象集合。

如果不使用SPECIFICATION,可以编写一个生成器,其中包含可创建所需对象的过程或指令集。这种代码隐式地定义了生成器的行为。
反过来,我们也可以使用描述性的SPECIFICATION来定义生成器的接口,这个接口就显式地约束了生成器产生的结果。这种方法具有以下几个优点。
 生成器的实现与接口分离。SPECIFICATION声明了输出的需求,但没有定义如何得到输出结果。

 接口把规则显式地表示出来,因此开发人员无需理解所有操作细节即可知晓生成器会产生什么结果。而如果生成器是采用过程化的方式定义的,那么要想预测它的行为,唯一的途径就是在不同的情况下运行或去研究每行代码。

 接口更为灵活,或者说我们可以增强其灵活性,因为需求由客户给出,生成器唯一的职责就是实现SPECIFICATION中的要求。

 最后一点也很重要。这种接口更加便于测试,因为接口显式地定义了生成器的输入,而这同时也可用来验证输出。也就是说,传入生成器接口的用于约束创建过程的同一个SPECIFICATION也可发挥其验证的作用(如果实现方式能够支持这一点的话),以保证被创建的对象是正确的。

根据要求来创建可以是从头创建全新对象,也可以是配置已有对象来满足SPECIFICATION。

示例: 化学品仓库打包程序

假设有一个仓库,里面用类似于货车车厢的大型容器存放各种化学品。有些化学品是惰性的,可以随意摆放。有些则是易挥发的,必须放于特制的通风容器中。还有一些是易爆品,必须保存于特制的防爆容器中。还有一些规则是关于如何在容器中混装化学品的。

我们的目标是编写出一个软件,用于寻找一种安全而高效地在容器中放置化学品的方式,如图9-16所示:

我们可以首先从编写一个过程——取出一个化学品并将其放臵在一个容器中——开始,但是让我们从验证问题开始着手吧。这种方式让我们必须显式描述规则,同时也提供了一种测试最终实现的方式。

每种化学品都有一个容器SPECIFICATION。

现在,如果将这些规格编写成Container Specification,就可以提出一种把化学品混装在容器中的配臵方法,并测试它是否满足这些约束条件。

客户并没有要求我们编写这样一个软件。让业务人员知道这个程序当然很好,但客户的要求
是设计一个打包程序。而现在我们得到的是打包的测试程序。这些对领域的理解和基于

SPECIFICATION的模型使我们有能力为服务定义一个清晰而简单的接口,这个服务可接受Drum和Container集合并将它们按照规则进行打包。

现在,为履行Packer服务的职责,我们的任务就是设计一个优化的约束求解方案。这一任务
已经与程序中的其他部分分离开来,因此其他部分的实现机制不会对这个部分的设计产生影响。

(详见第10章和第15章。)然而,控制打包的规则并没有从领域对象中提取出来。

仓库打包程序的可工作的原型

drum
为了让仓库打包软件有效工作而编写优化逻辑,这是一项艰巨的工作。一个小组的开发人员
和业务专家已经分头开始工作了,但是编码工作尚未进行。同时,另一个小组正在开发一个应用
程序,该程序允许用户从数据库中获取库存并提交给Packer处理,最后分析打包结果。这个小组
是面向预期的Packer进行设计的。但是他们能做的只是模拟一个用户界面,编写一些数据库集成
代码。他们无法为用户显示一个具有实际行为的界面,因此无法获得良好的反馈。同样,Packer
小组也在闭门造车。

通过仓库打包程序示例中创建的领域对象和SERVICE接口,开发应用程序的小组认识到他们可以构建一个非常简单的Packer实现代码,这有助于开发工作获得进展,同时可以与其他小组协同工作并建立起反馈循环,但这只有在端到端的系统中才可以完全发挥作用。


image.png

上述代码有很多不足之处。它可能会将砂打包到特制容器中,这就导致在打包危险化学品时,特制容器已经没有多余的空间了。显然,它没有对空间的利用进行优化。但是很多优化方面的问题无论怎样都无法得到完美的解决。而这段实现代码确实遵循了到目前为止已声明过的所有规则。

有了这个原型,应用程序的开发人员就可以全速开展工作了,包括进行所有与外部系统的集 成。在领域专家对原型进行研究并确认自己的想法后,Packer开发小组也能够得到专家的反馈,从而帮助他们自己理清需求和优先级。Packer小组决定接管这个原型并对其进行调整,以便测试他们的想法。

同时,他们还使接口与最新设计保持同步,以推动应用程序和一些领域对象的重构,从而尽早解决集成问题。

一旦完成复杂的Packer程序,集成就是轻而易举的事情了,因为它有一个描述得很清楚的接口,应用程序在与原型交互的时候也是根据相同的接口和ASSERTION编写的。

这里的例子演示了如何通过更巧妙的模型使“最简单却可能非常最有效的事物”成为可能。我们可以用几十行简单易懂的代码编写出复杂组件的功能原型。如果不用MODEL-DRIVEN DESIGN,系统会更难理解和升级(因为Packer与设计的其他部分更紧密地耦合在一起),在这种情况下,开发原型可能会更加耗时。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容