模型被用来描述人们所关注的现实或想法的某个方面。模型是一种简化。它是对现实的解释 —— 把与解决问题密切相关的方面抽象出来,而忽略无关的细节。
每个软件程序是为了执行用户的某项活动,或是满足客户的某种需求。这些用户应用软件的问题区域就是软件的领域。
一些领域涉及物质世界,例如,机票预定程序的领域中包括飞机乘客。有些领域则是无形的,例如,会计程序的金融领域。
为了创建真正能为用户所用的软件,开发团队必须运用一整套与这些活动有关的知识体系。所需知识的广度可能令人望而生畏,庞大复杂的信息也可能超乎想象。模型正是解决此类信息超载问题的工具。模型这种知识形式对于知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
领域模型并非某种特殊的图,而是这种所要传达的思想。它绝不单单是领域专家头脑中的知识,而是对这类知识严格的组织且有选择的抽象。
领域模型并不是要尽可能建立一个符合“现实”的模型。即使是对具体、真实世界中的事物进行建模,所得到的模型也不过是对事物的一种模拟。出于某种目的概括地反应现实。
模型在领域驱动设计中的作用
在领域模型的设计中,3个基本用途决定了模型的选择。
(1)模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品(即一个可运行的程序)。
(2)模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用语言来讨论程序。可以在无需翻译的情况下与领域专家进行沟通。
(3)模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念、以及将概念联系起来,模型记录了我们看待领域的方式。
软件的核心
软件的核心是为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。当领域很复杂时,这是一项艰巨的任务。开发人员必须钻研领域以获取业务知识。必须磨砺其建模技巧,并精通领域设计。
1.消化知识
一.有效建模的要素
(1)模型和实现的绑定。最初的原型虽然简陋,但它在模型和实现之间建立了早期链接,而且在所有后续的迭代中我们一直维护该链接。
(2)建立一种基于模型的语言。
(3)开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。
(4)提炼模型。
(5)头脑风暴和实验。语言和草图,再加上头脑风暴活动,将我们的讨论变成 “模型实验室” ,在这些讨论中可以演示、尝试和判断上百种变化。当团队走查场景时,口头表达本身就可以作为所提议的模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简捷,还是表达得很笨拙。
二.知识消化
高效的领域建模人员是知识的消化者。他们在大量信息中探寻有用的部分。他们不断尝试各种信息组织方式,努力寻找对大量信息有意义的简单视图。很多模型在尝试后被放弃或改造。只有找到一组适用于所有细节的抽象概念后,工作才算完成。这一精华严谨地表示了所发现的最为相关的知识。
知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员和领域专家组成的团队来共同协作。他们共同收集信息,并通过消化而将它组织为有用的形式。
模型聚焦于需求分析。它与编程和设计紧密交互。模型对理解领域必须是切实可行的。
三.持续学习
高效率的团队需要有意识地积累知识,并持续学习。对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧,也包括认真学习他们正在从事的特定领域的知识。
四.知识丰富的设计
业务活动和规则如同所涉及的实体一样,都是领域的核心,任何领域都有各种类别概念。知识消化所产生的模型能够反映出对知识的深层理解。
当我们的建模不再局限于寻找实体和值对象时,我们才能充分吸取知识,因为业务规则之间可能会存在不一致。领域专家需要反复研究所有规则、解决规则之间的矛盾以及常识来弥补规则的不足等一系列工作。
五.深层模型
有用的模型很少停留在表面。随着对领域和应用程序需求的理解逐步加深,我们往往会丢弃那些最初看起来很重要的表面元素,或者切换它们的角度。
2.交流与语言的使用
领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型预言的语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。
这种基于模型的交流并不局限于 UML 图。为了有效地使用模型,需要充分利用各种交流手段。基于模型的交流提高了书面文档的效用,也提高了敏捷过程中再度强调的非正式图表和交谈的效用。它还通过代码本身及对应的测试促进了交流。
在项目中,语言的使用很微妙,但却至关重要。
一.模式:Ubiquitous Language
要想创建一种灵活、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言不断的试验——然而,软件项目上很少出现这样的试验。
所有翻译的开销,连带着误解的风险,成本很高。项目需要一种公共语言,这种语言要比所有语言的最小公分母健壮得多。通过团队的一致努力,领域模型可以成为这种公共语言的核心,同时将团队沟通与软件实现紧密联系到一起。该语言将存在于团队工作中的方方面面。
Ubiquitous Language(通用语言)的词汇包括类和主要操作的名称。语言中的术语,有些用来讨论模型中意境明确的规则,还有一些规则来自施加于模型上的高级组织原则。
为了解释和给出更广泛的上下文,领域专家的语言会超出 Ubiquitous Language 的范围。但在模型对应的范围内,他们应该使用 Ubiquitous Language,并在发现不合适、不完整或错误之处后要引起注意。通过大量使用基于模型的语言,并且不达顺畅不罢休,我们可以逐步得到一个完整的、易于理解的模型,它由简单元素组成,并通过组合这些简单元素表达复杂的概念。
要认识到,Ubiquitous Language 的更改就是对模型的修改。
有了 Ubiquitous Language ,模型就不仅仅是一个设计工作了。它成为开发人员和领域专家共同完成的每项工作中不可或缺的部分。语言以动态形式传递知识。使用这种语言进行讨论能够呈现图和代码背后的真实含义。
Ubiquitous Language 是那些以非代码形式呈现的设计的主要载体,这些包括把整个系统组织在一起的大尺度结构,定义了不同系统和模型之间关系的限界上下文,以及在模型和设计中使用的其他模式。
二.“大声地” 建模
改善模型的最佳方法之一就是通过对话来研究,试着说出可能的模型变化中的各种结构。这样不完善的地方很容易被听出来。
使用单词和短语是极为重要的——其将我们的语言能力用于建模工作,这就如同素描对于表现视觉和空间推理一样重要。我们既要利用系统性分析和设计方面的分析能力,也要利用对代码的神秘“感觉”。这些思考方式互为补充,要充分利用它们来找到有用的模型和设计。在所有这些方式中,语言上的试验通常是最容易被忽视的。
讨论系统时要结合模型,使用模型元素及其交互来描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。
三.一个团队,一种语言
开发人员会使用领域专家无法理解的技术术语。用户也会用开发人员无法理解的、超出应用程序范畴的专用术语。这些都是对语言的扩展。但在这些语言扩展中,同一领域的相同词汇不应该反映不同的模型。
四 文档和图
一边画图一边用语言来丰富它们的意义,或者讨论时进行解释。
简单、非正式的UML图能够维系整个讨论。绘制一幅包含当前问题最关键的 3~5 个对象的图,这样每个人都可以集中注意力。所有人就对象关系达成一致,更重要的是,他们将使用相同的对象名称。当人们尝试不同的想法时,图也随之改变,草图在某种程度上可以反应讨论的变化,这是讨论中真正重要的部分。
当通过UML图表示整个模型或设计时,会因为过于细致导致 “只见树木,不见森林”。可以把细节(例如约束和断言)在图中用文本加进来。
图是一种沟通和解释手段,它们可以促进头脑风暴。简洁的小图能够很好地实现这些目标,而涵盖整个对象模型的综合性大图反而失去了沟通和解释能力,因为它们将读者淹没在大量细节之中,加之这些图也缺乏目的性。鉴于此,我们应避免使用包罗万象的对象模型图,甚至不能使用包含所有细节的UML数据存储库。相反,应使用简化的图,图中只包含对象模型的重要概念——这些部分对于理解设计至关重要。
设计的重要细节应该在代码中体现出来。良好的实现应该是透明的,清楚地展示其背后的模型。互为补充的图和文档能够引导人们将注意力放在核心要点上。
务必要记住模型不是图。图的目的是帮助表达和解释模型。代码可以充当设计细节的存储库。
1. 书面文档设计
口头交流可以解释代码的含义,因为可作为代码精确性和细节的补充。但书面文档是必不可少的,任何规模的团队都需要它来提供稳定和共享的交流。
两条用于评估文档的总体原则:
(1)文档应作为代码和口头交流的补充
文档不应再重复表示代码已经明确表达出的内容。代码已经含有各个细节,它本身就是一种精确的程序行为说明。
其他文档应该着重说明含义,以便使人们能够深入理解大尺度结构,并将注意力集中在核心元素上。
(2)文档应当鲜活并保持最新
通过将文档减至最少,并且主要用它来补充代码和口头交流,就可以避免文档与项目脱节。
2.完全依赖可执行代码的情况
良好的代码具有很强的表达能力,但它所传递的信息不能确保是准确的。一段代码所产生的实际行为是不会改变的。测试中的断言是严格的,但变量和代码组织方式所表达出来的意思未必严格。好的编程风格会尽力使这种联系直接化,但其仍然主要靠开发人员的自律。
尽管代码可能产生误导,但它仍然比其他文档更基础。要想利用当前的标准技术使代码所传达的消息与它的行为和意图保持一致,需要纪律和思考设计的特定方式(后面会详细介绍)。
五 解释性模型
在实现、设计和团队交流中需要使用同一个模型作为基础。如果各有各的模型,将会造成危害。
对设计起到推动作用的模型是领域的一个视图,但为了学习领域,还可以引入其他视图,这些视图只作用传递一般领域知识的教学工具。出于此目的,人们可以使用与软件设计无关的其他种类模型的图片或文字。
使用其他模型的一个特殊原因是范围。驱动软件开发过程的技术模型必须经过严格的精简,以便用最小化的模型来实现其功能。而解释性模型则可以包含那些提供上下文的领域方面——这些上下文用于澄清范围更窄的模型。
解释性模型提供了一定的自由度,可以专门为某个特殊主体定制一些表达力更强的风格。领域专家在一个领域中所使用的视觉隐喻通常呈现了更清晰的解释,这可以教给开发人员领域知识,同时使领域专家们的意见更一致。
解释性模型不必是对象模型,而且最好不是。
3.绑定模型和实现
领域驱动设计要求模型不仅能够指导早期的分析工作,还应该成为设计的基础。
3.1 模式: Model-Driver-Design
严格按照基础模型来编写代码,能够使代码更好地表达设计含义,并且使模型与实际的系统相契合。
那些压根就没有领域模型的项目,仅仅通过编写代码来实现一个又一个的功能,无法利用知识消化和沟通所带来的好处。如果涉及复杂的领域就会使项目举步维艰。
模型和程序设计之间的联系可能在很多情况下被破坏,但是二者的这种分离往往是有意而为之的。很多设计方法都是提倡使用完全脱离于程序设计的分析模型,并且通常这二者是由不同的人员开发的。之所以称其为分析模型,是因为它是对业务领域进行分析的结果,它在组织业务领域中的概念时,完全不去考虑自己在软件系统中将会起到的作用。分析模型仅仅是理解工具,人们认为把它与程序实现联系在一起无异于搅浑一池清水。随后的程序设计与分析模型之间可能仅仅保持一种松散的对应关系。在创建分析模型时并没有考虑程序设计的问题,因此分析模型很有可能无法满足程序设计的需求。
这种分析中会有一些知识消化的过程,但是在编码开始后,如果开发人员不得不重新对设计进行抽象,那么大部分的领域知识就会被丢弃。如此一来,就不能保证在新的程序设计中还能保留或重现分析人员所获得的并且嵌入在模型中的领域知识。到了这一步,要维护程序设计和松散连接的模型之间的对应关系就很不合算了。
无论是什么原因,软件的设计如果缺乏概念,那么软件充其量不过是一种机械化的产品——只实现有用的功能却无法解释操作的原因。
如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解,在实际项目中,当设计改变时也无法维护这种关系。若分析与设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。
分析工作一定要抓住领域内的基本概念,并且用易于理解和易于表达的方式描述出来。
Model- Driver-Design(模型驱动设计)不再将分析模型和程序设计分离开,而是寻求一种能够满足这两方面需求的单一模型。不考虑纯粹的技术问题,程序设计中的每个对象都反映了模型中所描述的相应概念。这就要求我们以更高的标准来选择模型,因为它必须同时满足两种完全不同的目标。
有很多方法可以对领域进行抽象,也有很多种设计可以解决应用程序的问题。因此,绑定模型和程序设计是切实可行的。但这种绑定不能够因为技术考虑而削弱分析的功能,也不能接受那些只反应了领域概念却舍弃了软件设计原则的拙劣设计。模型和设计的绑定需要的是在分析和程序设计阶段都能发挥良好作用的模型。如果模型对于程序的实现来说显得不太实用时,我们必须重新设计它。而如果模型无法忠实地描述领域的概念,也必须重新设计它。这样,建模和程序设计就结合为一个统一的迭代开发过程。
软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的 Ubiquitous Language(通用语言)。
从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能是模型的改变。而其影响势必要波及接下来相应的项目活动。
完全依赖模型的实现通常需要支持建模范式的软件开发工具和语言,比如面向对象的编程。
3.2 建模范式和工具支持
为了使 Model- Driver-Design 发挥作用,一定要在可控范围内严格保证模型与设计之间的一致性。要实现这种严格的一致性,必须要运用由软件工具支持的建模范式,它可以在程序中直接创建模型的概念。
面向对象编程之所以功能强大,是因为它基于建模范式,并且为建模构造提供了实现方法。
在 Model- Driver-Design 中,建模范式是逻辑的,而模型则是一组逻辑规则以及这些规则所操作的事实。
建模范式:
1.第一范式(1NF):原子性,列不能再分了,每一列只包含一个属性,所有属性都是同一类型,但集合、数组、记录等非原子数据项不是,即当一个实体中的一个属性有多个值时,必须拆分成不同的属性。这是所有关系数据库最基本的要求;
2.第二范式(2NF):唯一性,一个表只描述一件事,表中所有的列都必须依赖主键,没有一列可以与主键无关,即一个表只描述一件事;
3.第三范式(3NF):每一列都与主键直接相关,依赖主键属性不能传递属性。3NF在2NF的基础上,消除了非主属性对代码传递函数的依赖。也就是说,如果非主属性对代码的传递函数存在依赖,则不满足3NF的要求。
3.3 为什么模型对用户至关重要
从理论上讲,可以向用户展示任何一种系统视图,而不管底层如何实现。但实际上系统上下层结构的不匹配轻则导致误解,重则产生BUG。
Model- Driver-Design 要求只使用一个模型(在任何一个上下文中都是如此)。大部分的设计建议和例子都只针对将分析模型和设计模型分离的问题,但是这里的问题涉及了另外一对不同的模型:用户模型和设计/实现模型。
如果程序设计基于一个能够反映出用户和领域专家所关心的基本问题的模型,那么与其他设计方式相比,这种设计可以将其主旨更明确地展示给用户。让用户了解模型,将使他们有更多机会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。
3.4 模式:Hands-On Modeler
人们总是把软件开发比喻成制造业。这个比喻的一个推论是:经验丰富的工程师做设计工作,而技能水平较低的劳动力负责组装产品。这种做法使许多项目陷入困境,原因很简单——软件开发就是设计。虽然开发团队中的每个成员都有自己的职责,但是将分析、建模、设计和编程工作过度分离会对 Model- Driver-Design 产生不良影响。
如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而会削弱它的效果。同样,如果建模人员不参与到程序实现的过程中,那么对程序实现的约束就没有切身的感受,即使有,也会很快忘记。 Model- Driver-Design 的两个基本要素(即模型要支持有效的实现并抽象出关键的领域知识)已经失去了一个,最终模型将变得不再实用。最后一点,如果分工阻断了设计人员与开发人员之间的协作,使他们无法转达实现 Model- Driver-Design 的种种细节,那么经验丰富的设计人员则不能将自己的知识和技术传递给开发人员。
Hands-On Modeler(亲身实践的建模者)并不意味着团队成员不能有自己的专业角色。但是如果把 Model- Driver-Design 中密切相关的建模和实现这两个过程分离开,则会产生问题。
整体设计的有效性有几个非常敏感的影响因素——那就是细粒度的设计和实现决策的质量和一致性。在 Model- Driver-Design 中,代码是模型的表达,改变某段代码就改变了相应的模型。程序员就是建模人员,无论他们是否喜欢。所以在开始项目时,应该让程序员完成出色的建模工作。
因此:
任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型讨论并且与领域专家保持联系。参与不同工作的人必须有意识地通过 Ubiquitous Language 与接触代码的人及时交换关于模型的想法。
将建模和编程过程完全分离是行不通的,然而大型项目依然需要技术负责人来协调高层次的设计和建模,并帮助做出最困难或最关键的决策。后面将介绍这种决策。
Model- Driver-Design 利用模型来为应用程序解决问题。项目组通过知识消化将大量杂乱无章的信息提炼成实用的模型。而 Model- Driver-Design将模型和程序实现过程紧密结合。 Ubiquitous Language 则成为开发人员、领域专家和软件产品之间传递信息的渠道。
最终的软件产品能够在完全理解核心领域的基础上提供丰富的功能。
Model- Driver-Design 离不开详尽的决策设计,后面将介绍这些决策。