深层模型(Deep Model)

深层模型(Deep Model)——领域专家们最关心的问题以及与这些问题最相关的知识的清晰表示。深层模型不停留在领域的表层和粗浅的理解上。
**深层模型能够穿过领域表象,清楚地表达出领域专家们的主要关注点以及最相关的知识。**以上定义并没有涉及抽象。事实上,深层模型通常含有抽象元素,但在切中问题核心的关键位置也同样会出现具体元素。
恰当反映领域的模型通常都具有功能多样、简单易用和解释力强的特性。这种模型的共同之处在于:它们提供了一种业务专家青睐的简单语言,尽管这种语言可能也是抽象的。

突破

重构的投入与回报并非呈线性关系。通常,小的调整会带来小的回报,小的改进也会积少成多。小改进可防止系统退化,成为避免模型变得腐败的第一道防线。但是,有些最重要的理解也会突然出现,给整个项目带来巨大的突破。
可以确定的是,项目团队会积累、消化知识,并将其转化成模型。微小的重构可能每次只涉及一个对象,在这里加上一个关联,在那里转移一项职责。然而,一系列微小的重构会逐渐汇聚成深层模型
一般来说,持续重构让事物逐步变得有序。代码和模型的每一次精化都让开发人员有了更加清晰的认识。这使得理论上的突破成为可能。之后,一系列快速的改变得到了更符合用户需要并更加切合实际的模型。其功能性及说明性急速增强,而复杂性却随之消失。

重构与深层模型

重构的原则是始终小步前进,始终保持系统正常运转。但是要按照发现的深层模型来重构则需要修改大量的支持代码,在重构的过程中,系统几乎无法正常运转。我们能够看出一些力所能及的微小改进,但这些改进无法让我们实现新的领域概念。我们也知道通过一系列小改动可以实现新模型,但是在这个过程中必然会导致程序的一部分功能无法正常工作。如果自动化测试还没有广泛应用于项目,那重构的影响则更难评估。此外,重构是需要花费精力去实现的。

机遇

当突破带来更深层的模型时,通常会令人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。而且突破出现的时机可能很不合时宜。
尽管我们希望进展顺利,但往往事与愿违。过渡到真正的深层模型需要从根本上调整思路,并且对设计做大幅修改。在很多项目中,建模和设计工作最重要的进展都来自于突破。

关注根本

不要试图去制造突破,那只会使项目陷入困境。通常,只有在实现了许多适度的重构后才有可能出现突破。在大部分时间里,我们都在进行微小的改进,而在这种连续的改进中模型深层含义也会逐渐显现。
要为突破做好准备,应专注于知识消化过程,同时也要逐渐建立健壮的UBIQUITOUS LANGUAGE。寻找那些重要的领域概念,并在模型中清晰地表达出来(将隐式概念转变为显式概念)。精化模型,使其更具柔性(柔性设计)。提炼模型(精炼)。利用这些更容易掌握的手段使模型变得更清晰,这通常会带来突破。
不要犹豫着不去做小的改进,这些改进即使脱离不开常规的概念框架,也可以逐渐加深我们对模型理解。不要因为好高骛远而使项目陷入困境。只要随时注意可能出现的机会就够了。

越来越多的新理解

突破使我们走出了困境,但故事并没有就此结束。更深层次的模型为我们带来了意想不到的机会,它使应用程序的功能更加丰富,设计也更加清晰。
我们对领域的理解又向前迈进了一步,并获得了一个更深层次的模型。这个新模型使隐含的概念显现了出来。于是一些业务操作,如定义交易、交易规则、审批流程等就变得轻而易举了,而且实现这些概念的代码也相对来说变得更好理解了。
通常,在经过一次真正的突破并获得了深层模型之后,所获得的新设计变得更加清晰简单,新的UBIQUITOUS LANGUAGE也会增进沟通,于是又促成了下一次建模突破。

将隐式概念转变为显式概念

隐式概念(implicit concept)——一种为了理解模型和设计的意义而必不可少的概念,但它从未被提及。
深层建模听起来很不错,但是我们要如何实现它呢?深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。深层建模的第一步就是要设法在模型中表达出领域的基本概念。随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。
若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。
有时,这种从隐式概念到显式概念的转换可能是一次突破,使我们得到一个深层模型。但更多的时候,突破不会马上到来,而需要我们在模型中显式表达出许多重要概念,并通过一系列重构不断地调整对象职责、改变它们与其他对象的关系、甚至多次修改对象名称,在这之后,突破才会姗姗而来。最后,所有事情都变得清晰了。但是要实现上述过程,必须首先识别出以某种形式存在的隐含概念,无论这些概念有多么原始。

挖掘概念

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

倾听语言

你可能会想起这样的经历:用户总是不停地谈论报告中的某一项。该项可能来自各种对象的参数汇编,甚至还可能来自一次直接的数据库查询。同时,应用程序的另一部分也需要这个数据集来进行显示、报告或其他操作。但是,你却一直认为没有必要为此创建一个对象。也许你一直没有真正理解用户想通过某个特定术语传达的东西,也没有意识到它的重要性。
然后,你突然灵机一动。原来,报告中该项名称给出了一个重要的领域概念。你高兴地与专家谈起了这个新发现。他们可能会松一口气,因为你终于明白了。也可能会觉得很平常,因为他们一直认为这是理所当然的。不管专家们如何反应,你开始在白板上画模型图了(之前你也一直这么做)。用户会帮助你修正新模型连接方面的细节,但你明显感到讨论的质量有所提高。你和用户可以更加准确地理解对方,并且可以更加自然地用模型交互来演示特定场景。领域模型的语言也变得更加强大。然后,你可以重构代码来反映新模型,同时也会发现你的设计变得更加清晰了。
倾听领域专家使用的语言。有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正过你的用词(也许是很委婉的提醒)?当你使用某个特定词语时,他们脸上是否已经不再流露出迷惑的表情?这些都暗示了某个概念也许可以改进模型。
这不同于原来的"名词即对象"概念。听到新单词只是个开头,然后我们还要进行对话、消化知识,这样才能挖掘出清晰实用的概念。如果用户或领域专家使用了设计中没有的词汇,这就是个警告信号。而当开发人员和领域专家都在使用设计中没有的词汇时,那就是一个倍加严重的警告信号了。
或者,应该把这种警告看成一次机会。UBIQUITOUS LANGUAGE是由遍布于对话、文档、模型图甚至代码中的词汇构成的。如果出现了设计中没有的术语,就可以把它添加到UBIQUITOUS LANGUAGE中,这样也就有机会改进模型和设计了。

检查不足之处

你所需要的概念并不总是浮在表面上,也绝不仅仅是通过对话和文档就能让它显现出来。有些概念可能需要你自己去挖掘和创造。要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于解释的地方。每当有新的需求时,似乎都会让这个地方变得更加复杂。
有时,你很难意识到模型中丢失了什么概念。也许你的对象能够实现所有的功能,但是有些职责的实现却很笨拙。而有时,你虽然能够意识到模型中丢失了某些东西,但是却无法找到解决方案。这个时候,你必须积极地让领域专家参与到讨论中来。如果你足够幸运,这些专家可能会愿意一起思考各种想法,并通过模型来进行验证。如果你没那么幸运,你和你的同事就不得不自己思索出不同的想法,让领域专家对这些想法进行判断,并注意观察专家的表情是认同还是反对。

思考矛盾之处

由于经验和需求的不同,不同的领域专家对同样的事情会有不同的看法。即使是同一个人提供的信息,仔细分析后也会发现逻辑上不一致的地方。在挖掘程序需求的时候,我们会不断遇到这种令人烦恼的矛盾,但它们也为深层模型的实现提供了重要线索。有些矛盾只是术语说法上的不一致,有些则是由于误解而产生的。但还有一种情况是专家们会给出相互矛盾的两种说法。
天文学家伽利略曾提出过一个悖论。我们的感觉清楚地表明地球是静止的:人们既不会被吹走也不会被抛出去。然而哥白尼提出了一个很有说服力的观点,即地球是围绕着太阳飞速转动的。将这一矛盾统一起来可能会揭示出大自然运转的某种深奥的规律。
于是,伽利略设计了一个假想实验。如果一个骑手在奔跑的马背上丢下一个球,这个球会掉到哪里?显然,这个球会随着马一起向前移动,直到它落在马蹄旁边的地面上,就像马一直站着没动时一样。根据这个实验,伽利略推导出了惯性参考系思想的早期雏形,它可以解决前面提到的悖论并可引出更为实用的物理运动模型。
我们遇到的矛盾通常不会这么有趣,也不会具有如此深刻的意义。尽管如此,采用同样的思考模式通常可以帮助我们透过问题领域的表面获得更深层的理解。
要解决所有矛盾是不太现实的,甚至是不需要的。然而,即使不去解决矛盾,我们也应该仔细思考对立的两种看法是如何同时应用于同一个外部现实的,这会给我们带来启示。

查阅书籍

在寻找模型概念时,不要忽略一些显而易见的资源。在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。
当然,看书与咨询领域专家并不冲突。即便能够从领域专家那里得到充分的支持,花点时间从文献资料中大致了解领域理论也是值得的。虽然许多业务并不会像会计学或金融行业那样具有极其细致的模型,但大多数领域中都有一些擅于思考的人,他们已组织并抽象出了业务的一些通用的惯例。
开发人员还有另一个选择,就是阅读在此领域中有过开发经验的软件专业人员编写的资料。例如,《分析模式》[Fowler 1997]一书的第6章可能会提供一个完全不同的思考方向——无论这个方向会让开发变得更好还是更糟。阅读书籍并不能提供现成的解决方案,但可以提供一些全新的实验起点,以及在这个领域中探索过的人总结出来的经验。这样可以避免开发人员重复设计已有的概念。

尝试,再尝试

上面的例子并没有显示出不断尝试和出错的次数。在讨论过程中,我可能尝试六七种不同的思路,然后找到一个看起来足够清晰且实用的概念,并在模型中尝试它。后面,随着经验的积累和知识的消化,我们会有更好的想法,最终,这个概念至少会被替换一次。因此,建模人员/设计人员绝对不能固执己见。
并不是所有这些方向性的改变都毫无用处。每次改变都会把开发人员更深刻的理解添加到模型中。每次重构都使设计变得更灵活并且为那些可能需要修改的地方做好准备。
我们其实别无选择。只有不断尝试才能了解什么有效什么无效。企图避免设计上的失误将会导致开发出来的产品质量低劣,因为没有更多的经验可用来借鉴,同时也会比进行一系列快速实验更加费时。

为那些不太明显的概念建模

面向对象范式会引导我们去寻找和创造特定类型的概念。所有事物(即使是像"应计费用"这种非常抽象的概念)及其操作行为是大部分对象模型的主要部分。它们就是面向对象设计入门书籍所讲到的"名词和动词"。但是,其他重要类别的概念也可以在模型中显式地表现出来。
下面我将会描述3个这样的类别(约束、过程、规格(SPECIFICATION)),我在开始接触对象时,对它们的认识并不够清晰。我每学会一个这样的类别,就会让设计变得更加清晰深刻。

显式的约束

约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。
有时,约束很自然地存在于对象或方法中。Bucket(桶)对象必须满足一个固定规则——内容(contents)不能超出它的容量(capacity)。这样一个简单的固定规则可以在每次可改变内容的操作中使用一个逻辑判断来保证。

class Bucket {
    private float capacity;
    private float contents;
    public void pourIn(float addedVolume) {
        if(contents + addedVolume > capacity) {
            contents = capacity;
        } else {
            contents = contents + addedVolume;
        }
    }
}

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

class Bucket {
    private float capacity;
    private float contents;
    public void pourIn(float addedVolume) {
        float volumePresent = contents + addedVolume;
        contents = constrainedToCapacity(volumePresent);
    }

    private float constrainedToCapacity(float volumePalacedIn) {
        if(volumePalacedIn > capacity) {
            return capacity;
        }
        return volumePalacedIn;
    }
}

这两个版本的代码都实施了约束,但是第二个版本与模型的关系更为明显(这也是MODEL-DRIVEN DESIGN的基本需求)。这个规则十分简单,使用最初形式的代码也很容易理解,但如果要是执行的规则比较复杂的话,它们就会像所有隐式概念一样淹没掉被约束的对象或操作。将约束条件提取到其自己的方法中,这样就可以通过方法名来表达约束的含义,从而在设计中显式地表现出这条约束。现在这个约束条件就是一个"有名有姓"的概念了,我们可以用它的名字来讨论它。这种方式也为约束的扩展提供了空间。比这更复杂的规则很容易就会产生比其调用者(在这里就是方法)更长的方法。这样,调用者就可以简单一些,并且只专注于处理自己的任务,而约束条件则可以根据需要进行扩展。
这种独立方法为约束预留了一定的增加空间,但是在很多时候,约束条件是无法用单独的方法来轻松表达的。或者,即使方法自身能够保持其简单性,但它可能会调用一些信息,但对于对象的主要职责而言,这些信息毫无用处。这种规则可能就不适合放到现有对象中。
下面是一些警告信号,表明约束的存在正在扰乱其"宿主对象"(Host Object)的设计。
(1) 计算约束所需的数据从定义上看并不属于这个对象。
(2) 相关规则在多个对象中出现,造成了代码重复或导致不属于同一族的对象之间产生了继承关系。
(3) 很多设计和需求讨论是围绕这些约束进行的,而在代码实现中,它们却隐藏在过程代码中。
如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出但在模型中却不明显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合。(The Object Constraint Language: Precise Modeling with UML [Warmer and Kleppe 1999]一书中提供了关于这个问题的半正式的深入解决方案。)

将过程建模为领域对象

首先要说明的是,我们都不希望过程变成模型的主要部分。对象是用来封装过程的,这样我们只需考虑对象的业务目的或意图就可以了。
在这里,我们讨论的是存在于领域中的过程,我们必须在模型中把这些过程表示出来。否则当这些过程显露出来时,往往会使对象设计变得笨拙。
SERVICE是显式表达这种过程的一种方式,同时它还会将异常复杂的算法封装起来。如果过程的执行有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的STRATEGY。
过程是应该被显式表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当作计算机程序机制的一部分。
约束和过程是两大类模型概念,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
有些类别的概念很实用,但它们可应用的范围要窄很多。为了使本章的讨论更全面,我会探讨一个更特殊但也非常常用的概念——规格(specification)。"规格"提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它们显式地表示出来。
SPECIFICATION是Eric Evans与Martin Fowler[Evans and Fowler 1997]协作开发出来的。这个概念看起来很简单,但是应用和实现中却很微妙。

模式:SPECIFICATION

请添加图片描述
在所有类型的应用程序中,都会有布尔值测试方法,实际上它们只是些小规则。只要它们很简单,就可以用测试方法来处理它们。在Invoice类中,isOverdue()的代码是计算一条规则的算法。例如:

public boolean isOverdue() {
    Date currentDate = new Date();
    return currentDate.after(dueDate);
}

但是并非所有规则都如此简单。在同一个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来建模,如下图所示。在规格中声明拖欠的含义,对任意的Invoice对象进行计算并做出判断。

请添加图片描述

SPECIFICATION将规则保留在领域层。由于规则是一个完备的对象,所以这种设计能够更加清晰地反映模型。利用工厂,可以用来自其他资源(如客户账户或者企业政策数据库)的信息对规格进行配置。之所以使用FACTORY,是为了避免Invoice直接访问这些资源,因为这样会使得Invoice与这些资源发生不正确的关联(Invoice的基本职责是请求付款,而这些资源与这一职责无关)。在这个例子中,我们将创建Delinquent Invoice Specification(拖欠发票规格)来对一些发票进行评估,这个SPECIFICATION用过之后就被丢掉,因此可以将评估日期直接放在SPECIFICATION中,这真是一次不错的简化。我们可以用简单直接的方式为SPECIFICATION提供完成其职责所需的信息。
SPECIFICATION的基本概念非常简单,这能帮助我们思考领域建模问题。但是MODEL-DRIVEN DRSIGN要求我们开发出一个能够把概念表达出来的有效实现。要实现这个目标,必须要更深入地挖掘应用这个模式的方法。领域模式不仅仅是UML图中好的想法,也应该可以为MODEL-DRIVEN DRSIGN中的编程问题提供解决方案。
只要恰当地应用模式,就可以得出一整套如何解决领域建模问题的思路,同时也可以从这种长时间搜寻有效实现的经验中受益。
下面的SPECIFICATION讨论详细介绍了功能和实现方法的多种选择。模式并不像菜谱那么死板。它可以让你以模式的经验为起点来开发自己的解决方案,并为你讨论手头工作提供了语言。在第一次阅读时,你可以快速浏览关键概念。以后碰到具体情况时,可以再回过头来阅读并从细节讨论中获取经验。然后就可以开始设计你自己的解决方案了。

SPECIFICATION的应用和实现

SPECIFICATION最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下3个目的中的一个或多个,我们可能需要指定对象的状态。
(1) 验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备。
(2) 从集合中选择一个对象。
(3) 指定在创建新对象时必须满足某种需求。
这3种用法(验证、选择和根据要求来创建)从概念层面上来讲是相同的。如果没有诸如SPECIFICATION这样的模式,相同的规则可能会表现为不同的形式,甚至有可能是相互矛盾的形式。这样就会丧失概念上的统一性。通过应用SPECIFICATION模式,我们可以使用一致的模型,尽管在实现时可能需要分开处理。

验证

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

请添加图片描述

选择(或查询)

验证是对一个独立的对象进行测试,检查它是否满足某些标准,然后客户可能根据验证的结论来采取行动。另一种常见需求是根据某些标准从对象集合中选择一个子集。SPECIFICATION概念同样可以在此应用,但是实现问题会有所不同。
假设应用程序的需求是列出所有拖欠发票的客户。那么从理论上来说,我们依然可以使用之前定义的Delinquent Invoice Specification,但实际上我们可能不得不去修改它的实现。为了证明二者的概念是相同的,让我们首先假设发票的数量很少,可能已经全部装入内存了。在这种情况下,验证功能的最直接实现方式依然可用。Invoice Repository可以用一个一般化的方法来基于SPECIFICATION选择Invoice:

public Set<Invoice> selectSatisfying(InvoiceSpecification spec) {
    Set<Invoice> results = new HashSet<>();
    Iterator it = invoices.iterator();
    while(it.hasNext()) {
        Invoice candidate = it.next();
        if(spec.isSatisfiedBy(candidate)) {
            results.add(candidate);
        }
    }
    return results;
}

这样,用一行代码即可获得所有拖欠发票的集合:

Set<Invoice> delinquentInvoices = invoiceReposity.selectSatisfying(new DeliquentInvoiceSpecification(currentDate));

上面这行代码建立了操作背后的概念。当然,Invoice对象可能并不在内存中。也有可能会有成千上万个Invoice对象。在典型的业务系统中,数据很可能会存储在关系数据库中。我们在前面的章节中曾经指出,在与其他技术交互使用时,很容易分散我们对模型的注意力。
关系数据库具有强大的查询能力。我们如何才能充分利用这种能力来有效解决这一问题,同时又能保留SPECIFICATION模型呢?MODEL-DRIVEN DESIGN要求模型与实现保持同步,但它同时也让我们可以自由选择能够准确捕捉模型意义的实现方式。SPECIFICATION与REPOSITORY的搭配非常合适,REPOSITORY作为一种构造块机制,提供了对领域对象的查询访问,并且把数据库接口封装起来。

请添加图片描述

现在的设计有一些问题。最重要的问题是,表结构的细节本应该被隔离到一个映射层中(这个映射层把领域对象关联到关系表),现在却泄漏到了DOMAIN LAYER中。这样一来,这些表结构信息发生了隐性的重复,因此导致对Invoice和Customer对象的修改和维护变得很麻烦,因为现在必须在多个地方跟踪它们的映射变化。但是,这个例子只是一个简单的例证,用来说明如何将规则放在一个地方。**一些对象关系映射框架提供了用模型对象和属性来表达这种查询的方式,并在基础设施层中创建实际的SQL语句。**这样就可以两全其美了。
如果无法把SQL语句创建到基础设施中,还可以重写一个专用的查询方法并把它添加到Invoice Repository中,这样就把SQL语句从领域对象中分离出来了。为了避免在REPOSITORY中嵌入规则,必须采用更为通用的方式来表达查询,这种方式不捕捉规则但是可以通过组合或放置在上下文中来表达规则。
SPECIFICATION和REPOSITORY之间的交互有很多种实现方式,不但能够利用开发平台的优势,还可以保证基本职责的实施。
有时,为了改善性能(或者更有可能是为了加强安全性),我们可能把查询实现为服务器上的存储过程。在这种情况下,SPECIFICATION可能只带有存储过程允许的参数。除此之外,这些不同实现之间的模型并没有什么不同。我们可以自由选择实现方式,除非模型中有特别的约束条件。这么做的代价是更加难于编写和维护查询。
上面的讨论基本上没有涉及将SPECIFICATION与数据库结合时所面临的挑战,而只是想简单介绍一下必须要做出的选择。Mee和Hieatt在[Fowler 2002]中讨论了用规格设计REPOSITORY时遇到的一些技术问题。

根据要求来创建(生成)

很多计算机程序都能够生成一些工件,这些工件是需要被指定的。当你在字处理软件文档中插入图片时,文字会环绕在图片周围。你已指定了图片的位置,可能也指定了文字环绕的样式。这样,字处理软件就可以按照你指定的规格来将页面上的文字摆放到正确的位置。
尽管乍看起来并不明显,但是这种SPECIFICATION概念与应用于验证和选择的规格并无二致。都是在为尚未创建的对象指定标准。但是,SPECIFICATION的实现则会大不相同。这种SPECIFICATION与查询不同,它不用来过滤已存在对象;也与验证不同,并不用来测试已有对象。在这里,我们要创建或重新配置满足SPECIFICATION的全新对象或对象集合。
如果不使用SPECIFICATION,可以编写一个生成器,其中包含可创建所需对象的过程或指令集。这种代码隐式地定义了生成器的行为。
反过来,我们也可以使用描述性的SPECIFICATION来定义生成器的接口,这个接口就显式地约束了生成器产生的结果。这种方法具有以下几个优点。
生成器的实现与接口分离。SPECIFICATION声明了输出的需求,但没有定义如何得到输出结果。
接口把规则显式地表示出来,因此开发人员无需理解所有操作细节即可知晓生成器会产生什么结果。而如果生成器是采用过程化的方式定义的,那么要想预测它的行为,唯一的途径就是在不同的情况下运行或去研究每行代码。
接口更为灵活,或者说我们可以增强其灵活性,因为需求由客户给出,生成器唯一的职责就是实现SPECIFICATION中的要求。
最后一点也很重要。这种接口更加便于测试,因为接口显式地定义了生成器的输入,而这同时也可用来验证输出。也就是说,传入生成器接口的用于约束创建过程的同一个SPECIFICATION也可发挥其验证的作用(如果实现方式能够支持这一点的话),以保证被创建的对象是正确的。
根据要求来创建可以是从头创建全新对象,也可以是配置已有对象来满足SPECIFICATION。

参考

《领域驱动设计 软件核心复杂性应对之道》 Eric Evans 著, 赵俐 盛海艳 刘霞 等译, 任发科 审校

  • 15
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值