本节书摘来自异步社区《领域驱动设计:软件核心复杂性应对之道(修订版)》一书中的第1章,第1.1节有效建模的要素,作者【美】埃里克•埃文斯(Eric Evans), 马利伟 , 万龙,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第一部分 运用领域模型
领域驱动设计:软件核心复杂性应对之道(修订版)
![c70c518145e56cbface5c3eb3e7876b3339aafeb](https://i-blog.csdnimg.cn/blog_migrate/54055608e695aa9cc10eec92ba0a555c.png)
上面这张图是18世纪中国描绘的世界地图。图中央最大的部分是中国,其周围散布着其他国家,但这些国家只是草草地表示了一下。这是适用于当时中国社会的世界模型,它意在关注中国自身。然而,这幅地图所呈现的世界观对于处理外交事务并无助益。当然,它对现代中国也毫无用处。地图就是模型,而模型被用来描绘人们所关注的现实或想法的某个方面。模型是一种简化。它是对现实的解释——把与解决问题密切相关的方面抽象出来,而忽略无关的细节。
每个软件程序是为了执行用户的某项活动,或是满足用户的某种需求。这些用户应用软件的问题区域就是软件的领域。一些领域涉及物质世界,例如,机票预订程序的领域中包括飞机乘客在内。有些领域则是无形的,例如,会计程序的金融领域。软件领域一般与计算机关系不大,当然也有例外,例如,源代码控制系统的领域就是软件开发本身。
2
为了创建真正能为用户活动所用的软件,开发团队必须运用一整套与这些活动有关的知识体系。所需知识的广度可能令人望而生畏,庞大而复杂的信息也可能超乎想象。模型正是解决此类信息超载问题的工具。模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
领域模型并非某种特殊的图,而是这种图所要传达的思想。它绝不单单是领域专家头脑中的知识,而是对这类知识严格的组织且有选择的抽象。图可以表示和传达一种模型,同样,精心书写的代码或文字也能达到同样的目的。
领域建模并不是要尽可能建立一个符合“现实”的模型。即使是对具体、真实世界中的事物进行建模,所得到的模型也不过是对事物的一种模拟。它也不单单是为了实现某种目的而构造出来的软件机制。建模更像是制作电影——出于某种目的而概括地反映现实。即使是一部纪录片也不会原封不动地展现真实生活。就如同电影制片人讲述故事或阐明观点时,他们会选择素材,并以一种特殊方式将它们呈现给观众,领域建模人员也会依据模型的作用来选择具体的模型。
模型在领域驱动设计中的作用
在领域驱动的设计中,3个基本用途决定了模型的选择。
(1) 模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品(即一个可运行的程序)。模型与实现之间的这种紧密结合在维护和后续开发期间也会很有用,因为我们可以基于对模型的理解来解释代码。(参见第3章)
3
(2) 模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序。他们可以在无需翻译的情况下与领域专家进行沟通。而且,由于该语言是基于模型的,因此我们可借助自然语言对模型本身进行精化。(参见第2章)
(3) 模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。当开发人员和领域专家在将信息组织为模型时,这一共同的语言(模型)能够促使他们高效地协作。模型与实现之间的紧密结合使来自软件早期版本的经验可以作为反馈应用到建模过程中。(参见第1章)
接下来的3章分别考查上述3种基本用途的意义和价值,以及它们之间的关联方式。遵循这些原则使用模型可以很好地支持具有丰富功能的软件的开发,否则就需要耗费大规模投资进行专门开发。
软件的核心
软件的核心是其为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。当领域很复杂时,这是一项艰巨的任务,要求高水平技术人员的共同努力。开发人员必须钻研领域以获取业务知识。他们必须磨砺其建模技巧,并精通领域设计。
然而,在大多数软件项目中,这些问题并未引起足够的重视。大部分有才能的开发人员对学习与他们的工作领域有关的知识不感兴趣,更不会下力气去扩展自己的领域建模技巧。技术人员喜欢那些能够提高其技能的可量化问题。领域工作很繁杂,而且要求掌握很多复杂的新知识,而这些新知识看似对提高计算机科学家的能力并无裨益。
4
相反,技术人才更愿意从事精细的框架工作,试图用技术来解决领域问题。他们把学习领域知识和领域建模的工作留给别人去做。软件核心的复杂性需要我们直接去面对和解决,如果不这样做,则可能导致工作重点的偏离。
在一次电视访谈节目中,喜剧演员John Cleese讲述了电影《巨蟒和圣杯》(Monty Python and the Holy Grail)在拍摄期间发生的一个小故事。有一幕他们反复拍了很多次,但就是感觉不够滑稽。最后,他停下来,与另一位喜剧演员Michael Palin(该幕中的另一位演员)商量了一下,他们决定稍微改变一下。随后又拍了一次,终于令他们满意了,于是收工。
第二天早上,Cleese观看了剪辑人员为前一天工作所做的粗剪。到了那个令他们颇费周章的场景时,Cleese发现剪辑人员竟然使用了先前拍摄的一个镜头,影片到这里又变得不滑稽了。
他问剪辑人员为什么没有按要求使用最后拍的那个镜头,剪辑人员回答说:“那个镜头不能用,因为有人闯入了镜头。”Cleese连看了两遍,仍未发现有什么不妥。最后,剪辑人员将影片暂停,并指出在屏幕边缘有一只一闪而过的大衣袖子。
影片的剪辑人员专注于准确完成自己的工作。他担心其他看到这部电影的剪辑人员会给他挑错。在这个过程中,镜头的核心作用被忽略了(“The Late Late Show with Craig Kilborn”,CBS,2001年9月)。
幸运的是,该剧的导演很懂喜剧,他最终使用了那个镜头。同样,在一个团队中,反映了对领域深层次理解的模型开发有时也会在混乱中迷失方向,此时,理解领域核心的领导者能够将软件项目带回到正确的轨道上来。
本书将展示领域开发中蕴藏的巨大机会,它能够培养精湛的设计技巧。大多数混乱的软件领域其实是一项充满乐趣的技术挑战。事实上,在许多科学领域中,“复杂性”都是当前最热门的话题之一,因为研究人员都在想办法解决真实世界中的复杂性。软件开发人员在面对尚未规范的复杂领域时,也会有同样的期望。创建一个克服这些复杂性的易懂模型会带来巨大的成就感。
5
开发人员可以采用一些系统性的思考方法来透彻地理解领域并开发出有效的模型。还有一些设计技巧可以使毫无头绪的软件应用变得井井有条。掌握这些技能可以令开发人员的价值倍增,即使是在一个最初不熟悉的领域中也是如此。
6
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。
第1章 消化知识
领域驱动设计:软件核心复杂性应对之道(修订版)
几年前,我着手设计一个用于设计印制电路板(PCB)的专用软件工具。但有一个问题,我对电子硬件一无所知。当然,我也曾拜访过一些PCB设计师,但用不了3分钟,他们就令我晕头转向。如何才能了解足够多的知识,以便开始编写这个软件呢?当然,我并不打算在交付期限到来之前成为电子工程师。
我们试着让PCB设计师说明软件具体应该做些什么,但我们错了。虽然他们是优秀的电路设计师,但软件知识却太有限了,往往只知道如何读取一个ASCII文件、对它排序,然后添加一些注释并将它写回文件中,再生成一个报告。这些知识显然无法帮助他们大幅度提高效率。
最初的几次会面令人气馁,但我们在他们要求的报告中也看到了一丝希望。这些报告中总是涉及net这个词以及与其相关的各种细节。在这个领域中,net实质上是一种导线,它可以连接PCB上任意数量的元件,并向它连接的所有元件传递电子信号。这样,我们就得到了领域模型的第一个元素,如图1-1所示。
![1073093bfb993eadde89aba4cc00756eac431c4d](https://i-blog.csdnimg.cn/blog_migrate/4ba4ac4e0a0817493f75f451bae9049b.png)
7
就这样,我们一边讨论所需的软件功能,一边开始画图。我使用一种非正式的、稍加变化的对象交互图来走查①各种场景,如图1-2所示。
![dbcd6fbc46d66127119a8c4bbbe0913e3b275dcd](https://i-blog.csdnimg.cn/blog_migrate/c796501b21dd30afcc676b4e987e7ed4.png)
PCB专家1:元件不一定就是芯片(chip)。
开发人员(我):那它们是不是只应该叫做“元件”?
专家1:我们将它们称作“元件实例”(component instance)。相同的元件可能有很多。
专家2:他把“net”画成和元件实例一样的框了。
专家1:他没有使用我们的符号。我猜想,他要把每一项都画成方框。
开发人员:很抱歉,是这样的。我想我最好对这个符号稍加解释。
他们不断地纠正我的错误,在这个过程中我开始学习他们的知识。我们共同消除了术语上的不一致和歧义,也消除了他们在技术观点上的分歧,在这个过程中,他们也得到了学习。他们的解释更准确和一致了,然后我们开始共同开发一个模型。
专家1:只说一个信号到达一个ref-des是不够明确的,我们必须知道信号到达了哪个引脚。
开发人员:什么是ref-des?
专家2:它就是一个元件实例。我们用的一个专门工具中用ref-des这个名称。
专家1:总之,net将一个实例的某个引脚与另一个实例的某个引脚相连。
开发人员:一个引脚是不是只属于一个元件实例,而且只与一个net相连?
8
专家1:对,是这样。
专家2:还有,每个net都有一个拓扑结构,也就是电路的布局,它决定了net内部各元件的连接方式。
开发人员:嗯,这样画如何(如图1-3所示)?
![a412d1a30ebd513b492ccfe5dfec957c00888e73](https://i-blog.csdnimg.cn/blog_migrate/729805811d868008c5510e5a504408d7.png)
为了让讨论更集中,接下来的一段时间我们探讨了一个特定的功能:探针仿真(probe simulation)。探针仿真跟踪信号的传播,以便检测在设计中可能出现特定类型问题的位置。
开发人员:现在我已经明白了Net是如何将信号传播给它所连接的所有Pin的,但如何将信号传送得更远呢?这与拓扑结构(topology)有关系吗?
专家2:没有,是元件推送信号前进。
开发人员:我们肯定无法对芯片的内部行为建模,因为这太复杂了。
专家2:我们不必这样做。可以使用一种简化形式。只需列出通过元件可从某些Pin将信号推送到其他引脚即可。
开发人员:类似于这样吗?
(经过反复的尝试和修改,我们终于共同绘制出了一个草图,如图1-4所示。)
![dbd06d54999486102c494c7ee644c2a6a4332c4f](https://i-blog.csdnimg.cn/blog_migrate/be95bd299a314c13b14bb1fc494c6f8a.png)
9
开发人员:但你想从这种计算中知道什么呢?
专家2:我们要查找较长的信号延迟,也就是说,查找超过2或3跳的信号路径。这是一条经验法则。如果路径太长,信号可能无法在时钟周期内到达。
开发人员:超过3跳……这么说我们需要计算路径长度。那么怎样算作一跳呢?
专家2:信号每通过一个Net,就称为1跳。
开发人员:那么我们可以沿着电路来计算跳数,每遇到一个net,跳数就加1,如图1-5所示。
![bde5e5176707157208eff48c2de26231c767da57](https://i-blog.csdnimg.cn/blog_migrate/605a695866764a41b52230acca9d8b58.png)
开发人员:现在我唯一不明白的地方是“推动”是从哪里来的。是否每个元件实例都需要存储该数据?
专家2:一个元件的所有实例的推动行为都是相同的。
开发人员:那么元件的类型决定了推动行为,而每个实例的推动行为都是相同的(如图1-6所示)?
![64740b66c20153fa648d5d13b9151b1b945e4bf1](https://i-blog.csdnimg.cn/blog_migrate/aaf08aa0b10f0af43727bfa762e72a9a.png)
10
专家2:这个图的意思我没完全明白,但我猜想每个元件存储的推动行为就差不多是这样的吧。
开发人员:抱歉,这个地方我可能问得有点过细了。我只是想考虑得全面一些……现在,拓扑结构对它有什么影响吗?
专家1:拓扑结构不影响探针仿真。
开发人员:那么可以暂不考虑它,是吗?等用到这些特性时再回来讨论它。
就这样,我们的讨论一直进行下去(其中遇到的困难比上面显示的多得多)。我们一边进行“头脑风暴”式的讨论,一边对模型进行精化,边提问边回答。随着我对领域理解的加深,以及他们对模型在解决方案中作用的理解的加深,模型不断发展。图1-7显示了那个早期模型的类图。
![961c2d078ac477518f95848eeba29b5ccfb747b7](https://i-blog.csdnimg.cn/blog_migrate/a026702375c21478490bde9ccd798a0b.png)
随后,我们又拿出一部分工作时间进行了几轮这样的讨论,我觉得自己已经理解了足够多的知识,可以试着编写一些代码了。我写了一个非常简单的原型,并用一个自动测试框架来测试它。我避开了所有的基础设施。这个原型没有持久化机制,也没有用户界面(UI)。这样我就可以专注于代码的行为。只不过几天我就能够演示简单的探针仿真了。虽然它使用的是虚拟数据,而且向控制台输出的是原始文本,但确实是使用Java对象对路径长度执行实际的计算。这些Java对象所反映的模型正是我和领域专家们一起开发出来的。
这个具体的原型使得领域专家们更清楚地理解了模型的含义,以及它与最终软件之间的联系。从那时起,我们的模型讨论越来越具有互动性了,因为他们可以看到我如何将新学到的知识融合到模型中,然后反映到软件上。他们也可以从原型得到具体的反馈,从而印证自己的想法。
11
模型中包含与我们要解决的问题有关的PCB领域知识,这些知识远远比我们在这里演示的复杂。模型将很多同义词和语言描写中的微小差别做了统一,并排除了数百条与问题没有直接关系的事实(虽然工程师们都理解这些事实),如元件的实际数字特性。像我这样的软件专业人员看到这张图后,几分钟内就能明白软件是做什么的。这个模型就相当于一个框架,开发人员可以借助它来组织新的信息并更快地学习,从而更准确地判断哪些部分重要,哪些部分不重要,并更好地与PCB工程师进行沟通。
当PCB工程师提出新的功能需求时,我就让他们带我走查对象交互的场景。当模型对象无法清楚地表达某个重要场景时,我们就通过头脑风暴活动创建新的模型对象或者修改原有的模型对象,并消化理解这些模型对象中的知识。在我们精化模型的过程中,代码也随之一步步演进。几个月后,PCB工程师们得到了一个远远超乎他们期望的功能丰富的工具。
1.1 有效建模的要素
以下几方面因素促使上述案例得以成功。
(1) 模型和实现的绑定。最初的原型虽然简陋,但它在模型与实现之间建立了早期链接,而且在所有后续的迭代中我们一直在维护该链接。
(2) 建立了一种基于模型的语言。最初,工程师们不得不向我解释基本的PCB问题,而我也必须向他们解释类图的含义。但随着项目的进展,双方都能够直接使用模型中的术语,并将它们组织为符合模型结构的语句,而且无需翻译即可理解互相要表达的意思。
(3) 开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。
12
(4) 提炼模型。在模型日趋完整的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,其他那些不要的概念就可以丢弃了。
(5) 头脑风暴和实验。语言和草图,再加上头脑风暴活动,将我们的讨论变成“模型实验室”,在这些讨论中可以演示、尝试和判断上百种变化。当团队走查场景时,口头表达本身就可以作为所提议的模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简捷,还是表达得很笨拙。
正是头脑风暴和大量实验的创造力才使我们找到了一个富含知识的模型并对它进行提炼,在这个过程中,基于模型的语言提供了很大帮助,而且贯穿整个实现过程中的反馈闭环也对模型起到了“训练”作用。这种知识消化将团队的知识转化为有价值的模型。