书籍简介
埃里克·埃文斯(Eric Evans)所著的《领域驱动设计:软件核心复杂性应对之道》这本书,自从2003年出版以来,在世界范围内影响极大。作为DDD的开山之作,纵使已经过去18年,其中的思想至今仍然非常有意义,是我们学习DDD的必经之路。
此书分为4个部分:
第一部分“运用领域模型”提出领域驱动开发的基本目标,这些目标是后面几部分中所讨论的实践的驱动因素。由于软件开发方法有很多,因此第一部分还定义了一些术语,并给出了用领域模型来驱动沟通和设计的总体含义。
第二部分“模型驱动设计的构造块”将面向对象领域建模中的一些核心的最佳实践提炼为一组基本的构造块。这一部分主要是消除模型与实际运行的软件之间的鸿沟。
第三部分“通过重构来加深理解”讨论如何将构造块装配为实用的模型,从而实现其价值。这一部分没有直接讨论深奥的设计原则,而是着重强调一个发现过程。有价值的模型不是立即就会出现的,为了对领域一步一步深入理解,需要反复改进这个模型设计。
第四部分“战略设计”讨论在复杂系统、大型组织以及与外部系统和遗留系统的交互中出现的复杂情况。这一部分探讨了作为一个整体应用于系统的3条原则:上下文、提炼和大型结构。
下面我们带着问题,一起来读读这本书吧。
本书解决什么问题
在Martin Fowler写的序言中,他就直接点明:使软件开发复杂化的根本原因是问题领域本身的复杂性。Eric在前言部分也阐明:
很多因素可能会导致项目偏离轨道,如官僚主义、目标不清、资源缺乏等。但真正决定软件复杂性的是设计方法。
然而很多应用程序最主要的复杂性并不在技术上,而是来自领域本身、用户的活动或业务。当这种领域复杂性在设计中没有得到解决时,基础技术的构思再好也无济于事。
所以本书的希望解决的问题,就是如何用良好的设计方法,来解决由于领域复杂性带来的问题。
本书中的讨论基于以下两个前提:
(1)在大多数软件项目中,主要的焦点应该是领域和领域逻辑;
(2)复杂的领域设计应该基于模型。
作者Eric在前提中就给出了问题的答案,那就是基于模型的设计方法。
什么是领域模型
维基百科给出的解释是:模型是指用一个较为简单的东西来代表另一个东西。而Eric的定义是:
模型是一种简化。它是对现实的解释——把与解决问题密切相关的方面抽象出来,而忽略无关的细节。
简而言之就是抽出重要的,忽略无关的。Eric还对在领域驱动设计中,模型的作用作了如下说明:
(1)模型和设计的核心互相影响。
(2)模型是团队所有成员使用的通用语言的中枢。
(3)模型是浓缩的知识。
模型和代码实现之间有着非常紧密的联系,同时它也是开发人员和领域专家之间沟通所使用的通用语言,是整个团队共同认可的浓缩的领域知识。
如何对待建模工作
既然模型有着如此重要的作用,作为软件开发人员,应该如何去运用模型呢?
Eric指出软件开发人员喜欢提高自己的技能,特别是可以量化的部分,比如掌握一门编程语言,学习一个开发框架,学习一种数据库等等。而对工作领域相关的知识不太关心,例如技术人员自身所处的制造业,餐饮业,金融业等等领域,认为学习领域知识,对提高计算机人才的能力没有用处,更加对领域建模不感兴趣。
我们需要明白软件的核心是什么?并不是各种计算机技术的堆叠,而是像Eric所说的:
软件的核心是其为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。
开发人员不能把领域知识的学习和领域建模交给其他人,而试图用单纯的技术来解决领域问题。解决软件核心的复杂性问题,需要开发人员直接去面对和解决。Eric同时还苦口婆心的劝说道,掌握建模的技巧可以让软件自身变得井井有条,可以让开发人员的价值倍增!
如何建立领域模型
书中给出了一个形象的比喻:
建模更像是制作电影——出于某种目的而概括地反映现实。
如果是拍电影,谁来做编导呢?换句话说谁来复杂创作?在传统的瀑布方法中,一般存在3种角色,业务专家与分析员和程序员。业务专家与分析员进行讨论,分析员消化理解这些知识后,对其进行抽象并将结果传递给程序员,再由程序员编写软件代码。这里业务专家负责描述现实,分析员负责概括,程序员负责制作,编导似乎是分析员。Eric指出:
由于这种方法完全没有反馈,因此总是失败。分析员全权负责创建模型,但他们创建的模型只是基于业务专家的意见。他们既没有向程序员学习的机会,也得不到早期软件版本的经验。知识只是朝一个方向流动,而且不会累积。
如果程序员只是按照分析员的描述来构建软件,而不去理解其背后的原因,不去学习领域知识,这样开发出来的软件在当时也许可以用,但随着变更不断增加,必将陷入泥潭。单纯由技术人员来建模,缺少领域专家的支持也是不行的。编导应该由整个团队一起负责。
在团队所有成员一起消化理解模型的过程中,他们之间的交互也会发生变化。
团队成员需要在互相学习的过程中建模。开发人员要学习业务原理,领域专家要学习软件项目所需的严谨性。
Eric还引述了Kerievsky的话:
高效率的团队需要有意识地积累知识,并持续学习。
在Eric看来,领域模型和相应的设计就是团队积累的知识。为了更好的承载这些积累的知识,他提出了通用语言模式(UBIQUITOUS LANGUAGE):
领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型语言的语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。
通用语言指的不是中文、英语、日语这些自然语言,而是描述领域模型的词汇,包括类和主要操作的名称。如果不使用通用语音,领域专家和开发人员使用各自的术语,那么团队成员之间的沟通就需要翻译。翻译工作导致各类促进深入理解模型的知识和想法无法结合到一起。这种间接沟通的后果是什么呢?
由于软件的各个部分不能够浑然一体,因此这就导致无法开发出可靠的软件。
为了让模型成为领域专家和开发人员之间互相交流的语言,我们需要在各个开发活动中使用这种语音。开发人员使用这种语言来描述系统中的组件和功能。领域专家使用这种语言来描述需求,开发计划和特性。
将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
Eric还给出了一个小技巧,就是要大声地讨论模型。讨论时要使用模型元素来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的表达落实到图和代码中。虽然暂时还没有给出详细的建模步骤,但是我们知道了需要用通用语言,在互相交流学习中建模。
使用通用语言的样例
书中给出了两个场景作为比较。
场景1:最小化的领域抽象
用户:那么,当更改清关(customs clearance)地点时,需要重新制定整个路线计划啰。
开发人员:是的。我们将从货运表(shipment table)中删除所有与该货物id相关联的行,然后将出发地、目的地和新的清关地点传递给RoutingService,它会重新填充货运表。Cargo中必须设立一个布尔值,用于指示货运表中是否有数据。
用户:删除行?好,就按你说的做。但是,如果先前根本没有指定清关地点,也需要这么做吗?
开发人员:是的,无论何时更改了出发地、目的地或清关地点(或是第一次输入),都将检查是否已经有货运数据,如果有,则删除它们,然后由RoutingService重新生成数据。
用户:当然,如果原有的清关数据碰巧是正确的,我们就不需要这样做了。
开发人员:哦,没问题。但让RoutingService每次重新加载或卸载数据会更容易些。
用户:是的,但为新航线制定所有支持计划的工作量很大,因此,除非非改不可,我们一般不想更改航线。
开发人员:哦,好的,当第一次输入清关地点时,我们需要查询表格,找到以前的清关地点,然后与新的清关地点进行比较,从而判断是否需要重做。
用户:这个处理不必考虑出发地和目的地,因为航线在此总要变更。
开发人员:好的,我明白了。
场景2:用领域模型进行讨论
用户:那么,当更改清关地点时,需要重新制定整个路线计划啰。
开发人员:是的。当更改RouteSpecification(路线说明)的任意属性时,都将删除原有的Itinerary(航线),并要求RoutingService(路线服务)基于新的RouteSpecification生成一个新的Itinerary。
用户:如果先前根本没有指定清关地点,也需要这么做吗?
开发人员:是的,无论何时更改了RouteSpec的任何属性,都将重新生成Itinerary。这也包括第一次输入某些属性。
用户:当然,如果原有的清关数据碰巧是正确的,我们就不需要这样做了
开发人员:哦,没问题。但让RoutingService每次重新生成一个Itinerary会更容易些。
用户:是的,但为新航线制定所有支持计划的工作量很大,因此,除非非改不可,我们一般不想更改路线。
开发人员:哦。那么需要在RouteSpecification添加一些功能。这样,当更改RouteSpecification中的属性时,查看Itinerary是否仍满足Specification。如果不满足,则需要由RoutingService重新生成Itinerary。
用户:这一点不必考虑出发地和目的地,因为Itinerary在此总是要变更的。
开发人员:好的,但每次只做比较就简单多了。只有当不满足RouteSpecification时,才重新生成Itinerary。
不难看出,场景2中使用了基于领域模型的术语RouteSpecification(路线说明)和Itinerary(航线),是得描述更为精准。
模型和文档有什么关系
模型应当以什么样形式表现出来呢?很多人会想到UML统一建模语言。UML可以用图形很好地展现对象之间的关系,帮助我们讨论领域模型。但是Eric提醒我们:
当人们必须通过UML图表示整个模型或设计时,麻烦也随之而来。很多对象模型图在某些方面过于细致,同时在某些方面又有很多遗漏。
UML图无法传达模型的两个最重要的方面,一个方面是模型所表示的概念的意义,另一方面是对象应该做哪些事情。
因此我们需要使用其他自然语言作为补充,来表达模型的确切含义。UML只是一种沟通和解释的手段,它并不是模型本身。不能强制用图来表示全部模型或设计,因为这样会削弱图的清晰表达的能力。所以模型可以表现为UML加自然语言构成的文档。
但是在敏捷开发大行其道的今天,文档的处境很尴尬。很多极限编程的拥护者甚至提倡没有文档,代码即文档。其实仔细看看敏捷宣言,原话是工作的软件高于详尽的文档,并没有完全否定文档的意义。那么文档究竟应该处于一个什么样的位置呢?Eric告诉我们,文档应作为代码和口头交流的补充。
文档不应再重复表示代码已经明确表达出的内容。代码已经含有各个细节,它本身就是一种精确的程序行为说明。其他文档应该着重说明含义,以便使人们能够深入理解大尺度结构,并将注意力集中在核心元素上。当编程语言无法直接明了地实现概念时,文档可以澄清设计意图。
设计文档的最大价值在于解释模型的概念,帮助在代码的细节中指引方向。
很多人倡导代码即文档的主要原因是:代码不会骗人,而文档会。很多时候文档没有随着代码的演变而得到更新,如果使用了这种过期的文档,还可能会对项目带来伤害。所以文档应当鲜活并保持最新。但是如果要持续更新所有的文档,开发人员有没有意愿先不说,还会带来不少工作量。如何去平衡这个问题?Eric建议最大限度地减少文档,确保文档只是作为代码和口头交流的补充,可以避免文档与项目脱节。
模型和代码有什么关系
Martin Fowler在序言中提到:
在领域建模过程中不应将概念与实现割裂开来。高效的领域建模人员不仅应该能够在白板上与会计师进行讨论,而且还应该能与程序员一道编写Java代码。
模型与代码实现应该是紧密相连的,创建模型的人和编写代码的人也不应该割裂开。然而领域驱动设计以外的很多设计方法却提倡使用分析模型,一种与程序设计毫不相干,只作为对业务领域的分析结果的模型。通常由分析员创建,作为需求传递给开发人员。分析模型和最终的程序设计只能保持了松散的对应关系。由于前期创建分析模型是没有考虑程序设计,一旦编码开始,细节问题就会层出不穷,慢慢地纯粹的分析模型就会被抛到一边。这种开发模式至今仍然司空见惯。Eric指出:
如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在实际项目中,当设计改变时也无法维护这种关系。若分析与和设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享。
如何来解决这个问题呢?Eric适时提出了模型驱动设计模式(MODEL DRIVEN DESIGN),试图寻找一种单一模型,即能满足分析模型的需求又能满足程序设计的需求。事实证明绑定模型和程序设计是切实可行的,但是需要注意:
这种绑定不能够因为技术考虑而削弱分析的功能,我们也不能接受那些只反映了领域概念却舍弃了软件设计原则的拙劣设计。
这是模型驱动设计模式(MODEL DRIVEN DESIGN)的两个基本要素,即模型要支持有效的实现,并抽象出关键的领域知识。
在开发语言方面,Java这类面向对象的语言天然适合用来表达模型。而且只有用代码表现模型概念时,面向对象设计的突破之处才能真正彰显出来。面向对象设计是目前大多数项目所使用的建模范式,也是此书中使用的主要方法。
书中提到一个曾经困扰了作者Eric多年的问题,负责创建模型的分析员和负责程序设计的开发人员是不是应该区分开?他曾经在一个项目中负责开发领域模型,用于指导程序设计,然而项目管理层却禁止他编写代码或者与程序员讨论细节问题,他们认为负责分析的人就专心分析建模,编写代码是在浪费时间。Eric分析领域知识,提炼出了一个不错的领域模型,但是最终却没派上用场。因为模型的部分意图在传递过程中丢失,而且代码实现对模型的影响得不到反馈。于是Eric提出了亲身实践的建模人员的模式(HANDS-ON MODELER),这个模式也被翻译成建模人员参与程序开发。
如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。