领域驱动设计:软件核心复杂性应对之道(修订版)

978-7-115-37675-6
作者: 【美】埃里克•埃文斯(Eric Evans)
译者: 赵俐盛海艳刘霞
编辑: 杨海玲

图书目录:

详情

本书是领域驱动设计方面的经典之作。全书围绕着设计和开发实践,结合若干真实的项目案例,向读者阐述如何在真实的软件开发中应用领域驱动设计。书中给出了领域驱动设计的系统化方法,并将人们普遍接受的一些最佳实践综合到一起,融入了作者的见解和经验,展现了一些可扩展的设计最佳实践、已验证过的技术以及便于应对复杂领域的软件项目开发的基本原则。

图书摘要

版权信息

书名:领域驱动设计:软件核心复杂性应对之道(修订版)

ISBN:978-7-115-37675-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Eric Evans

  译    赵 俐  盛海艳  刘 霞 等

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


本书是领域驱动设计方面的经典之作,修订版更是对之前出版的中文版进行了全面的修订和完善。

全书围绕着设计和开发实践,结合若干真实的项目案例,向读者阐述如何在真实的软件开发中应用领域驱动设计。书中给出了领域驱动设计的系统化方法,并将人们普遍接受的一些最佳实践综合到一起,融入了作者的见解和经验,展现了一些可扩展的设计最佳实践、已验证过的技术以及便于应对复杂领域的软件项目开发的基本原则。

本书适合各层次的面向对象软件开发人员、系统分析员阅读。


Authorized translation from the English language edition, entitled Domain-Driven Design: Tackling Complexity in the Heart of Software, 9780321125217 by Eric Evans, published by Pearson Education, Inc., publishing as Addison-Wesley, Copyright © 2004 by Eric Evans.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD. and POSTS & TELECOM PRESS Copyright © 2016.

本书中文简体字版由 Pearson Education Asia Ltd. 授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有 Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。


我最早听说Eric Evans的《领域驱动设计》是在2007年,那时我所在的项目组出于知识储备的考虑购进了一批软件设计书和相关资料。其中一篇英文的短篇技术文档与我们当时的项目非常相关,于是我们就仔细研读了一番。这篇仅有几万字的文档多次提到了Eric Evans的《领域驱动设计》,并引用了他的很多精辟观点。由于当时领域驱动设计远远没有现在这样普及,因此这些观点使我耳目一新,也给我留下了深刻的印象。随后我又经常在一些文献中看到Eric Evans的名字,更多地了解了他的领域驱动设计思想,没想到时隔几年后竟然有机会把这位大师的作品翻译出来奉献给各位读者,也算是机缘巧合了。

相信大家对这本书都不陌生,它已经成为软件设计书中的经典。在网上搜索一下,读者对它好评如潮,我再多说一句赞美的话都是多余的。而我能想到的也唯有“经典”二字,它堪称经典中的经典。

我们对“领域”这个概念都很熟悉,但有多少人真正重视过它呢?软件开发人员几乎总是专注于技术,把技术作为自己能力的展示和成功的度量。而直到Eric Evans出版了他的这部巨著之后,人们才真正开始关注领域,关注核心领域,关注领域驱动的设计,关注模型驱动的开发。相信在读完本书后,你会对软件设计有全新的认识。

我曾经和一些好友探讨过以下一些问题。项目怎样开发才能确保成功?什么样的软件才能为用户提供真正的价值?什么样的团队才算是优秀的团队?现在,在仔细研读完本书后,这些问题都找到了答案。

本书广泛适用于各种领域的软件开发项目。在每个项目的生命周期中,都会有一些重大关头或转折点。如何制定决策,如何把握项目的方向,如何处理和面对各种机会和挑战,将对项目产生决定性的影响。让我们一起跟随大师的脚步,分享他通过大量项目获得的真知灼见和开发心得吧。

最后,衷心感谢人民邮电出版社各位编辑在翻译工作中给予的帮助和宝贵意见,感谢热心读者魏海枫,他在百忙之中抽出时间对本书译稿做了修订工作,发现并修正了很多问题。由于译者水平有限,在翻译过程中难免还会留有一些错误,恳请读者批评指正。


有很多因素会使软件开发复杂化,但最根本的原因是问题领域本身错综复杂。如果你要为一家人员复杂的企业提高自动化程度,那么你开发的软件将无法回避这种复杂性,你所能做的只有控制这种复杂性。

控制复杂性的关键是有一个好的领域模型,这个模型不应该仅仅停留在领域的表面,而是要透过表象抓住领域的实质结构,从而为软件开发人员提供他们所需的支持。好的领域模型价值连城,但要想开发出好的模型也并非易事。精通此道的人并不多,而且这方面的知识也很难传授。

Eric Evans就是为数不多的能够创建出优秀领域模型的人。我是在与他合作时发现他的这种才能的——发现一个客户竟然比我技术更精湛,这种感觉有些奇妙。我们的合作虽然短暂,但却充满乐趣。从那之后我们一直保持联系,我也有幸见证了本书整个“孕育”过程。

本书绝对值得期待。

本书终于实现了一个宏伟抱负,即描述并建立了领域建模艺术的词汇库。它提供了一个参考框架,人们可以用它来解释相关活动,并用它来传授这门难学的技艺。本书在写作过程中,也带给我很多新想法,如果哪位概念建模方面的老手没有从阅读本书中获得大量的新思想,那我反而该惊诧莫名了。

Eric还对我们多年以来学过的知识进行了归纳总结。首先,在领域建模过程中不应将概念与实现割裂开来。高效的领域建模人员不仅应该能够在白板上与会计师进行讨论,而且还应该能与程序员一道编写Java代码。之所以要具备这些能力,一部分原因是如果不考虑实现问题就无法构建出有用的概念模型。但概念与实现密不可分的最主要原因在于,领域模型的最大价值是它提供了一种通用语言,这种语言是将领域专家和技术人员联系在一起的纽带。

我们将从本书中学到的另一个经验是领域模型并不是按照“先建模,后实现”这个次序来工作的。像很多人一样,我也反对“先设计,再构建”这种固定的思维模式。Eric的经验告诉我们,真正强大的领域模型是随着时间演进的,即使是最有经验的建模人员也往往发现他们是在系统的初始版本完成之后才有了最好的想法。

我衷心希望本书成为一本有影响力的著作,并希望本书能够将如何利用领域模型这一宝贵工具的知识传授给更多的人,从而为这个高深莫测的领域梳理出一个结构,并使它更有内聚力。领域模型对软件开发的控制有着巨大影响,不管软件开发是用什么语言或环境实现的。

最后,也是很重要的一点,我最敬佩Eric的一点是他敢于在本书中谈论自己的一些失败经历。很多作者都喜欢摆出一副无所不能的架势,有时着实让人不屑。但Eric清楚地表明他像我们大多数人一样,既品尝过成功的美酒,也体验过失败的沮丧。重要的是他能够从成功和失败中学习,而对我们来说更重要的是他能够将所有经验传授给我们。

Martin Fowler

2003年4月


至少20年前,一些顶尖的软件设计人员就已经认识到领域建模和设计的重要性,但令人惊讶的是,这么长时间以来几乎没有人写出点儿什么,告诉大家应该做哪些工作或如何去做。尽管这些工作还没有被清楚地表述出来,但一种新的思潮已经形成,它像一股暗流一样在对象社区中涌动,我把这种思潮称为领域驱动设计(domain-driven design)。

过去10年中,我在几个业务和技术领域开发了一些复杂的系统。我在设计和开发过程中尝试了一些最佳实践,它们都是面向对象开发高手用过的领先技术。有些项目非常成功,但有几个项目却失败了。成功的项目有一个共同的特征,那就是都有一个丰富的领域模型,这个模型在迭代设计的过程中不断演变,而且成为项目不可分割的一部分。

本书为作出设计决策提供了一个框架,并且为讨论领域设计提供了一个技术词汇库。本书将人们普遍接受的一些最佳实践综合到一起,并融入了我自己的见解和经验。面对复杂领域的软件开发团队可以利用这个框架来系统性地应用领域驱动设计。

谈到领域设计实践对开发结果的巨大影响时,我的记忆中立即就会跳出三个项目,它们就是鲜活的例子。虽然这三个项目都交付了有用的软件,但只有一个项目实现了宏伟的目标——交付了能够满足组织后续需求、可以不断演进的复杂软件。

我要说的第一个项目完成得很迅速,它提供了一个简单实用的Web交易系统。开发人员主要凭直觉开发,但这并没有妨碍他们,因为简单软件的编写并不需要过多地注意设计。由于最初的这次成功,人们对未来开发的期望值变得极高。我就是在这个时候被邀请开发它的第二个版本的。当我仔细研究这个项目时,发现他们没有使用领域模型,甚至在项目中没有一种公共语言,而且项目完全没有一种结构化的设计。项目领导者对我的评价并不赞同,于是我拒绝了这项工作。一年后,这个项目团队陷入困境,无法交付第二个版本。尽管他们在技术的使用方面也值得商榷,但真正挫败他们的是业务逻辑。他们的第一个版本过早地变得僵化,成为一个维护代价十分高昂的遗留系统。

要想克服这种复杂性,需要非常严格地使用领域逻辑设计方法。在我职业生涯的早期,我幸运地完成了一个非常重视领域设计的项目,这就是我要说的第二个项目。这个项目的领域复杂性与上面提到的那个项目相仿,它最初也小获成功,为贸易机构提供了一个简单的应用程序。但在最初交付之后紧跟着又进行了连续的加速开发。每次迭代都为上一个版本在功能的集成和完善上增加了非常好的新选项。开发团队能够按照贸易商的要求提供灵活性和扩展性。这种良性发展直接归功于深刻的领域模型,它得到了反复精化,并在代码中得以体现。当团队对该领域有了新的理解后,领域模型也随之深化。开发人员之间、开发人员与领域专家之间的沟通质量都得到改善,而且设计不但没有加重维护负担,反而变得易于修改和扩展。

遗憾的是,仅靠重视模型并不会使项目达到这样的良性循环。我要说的第三个项目就是这种情况,它开始制订的目标很高,打算基于一个领域模型建立一个全球企业系统,但在经过了几年的屡战屡败之后,不得不降格以求,最终“泯然众人矣”。团队拥有很好的工具,对业务也有较好的理解,也非常认真地进行了建模。但团队却错误地将开发人员的角色独立出来,导致建模与实现脱节,因此设计无法反映不断深化的分析。总之,详细的业务对象设计不能保证它们能够严丝合缝地被整合到复杂的应用程序中。反复的迭代并没有使代码得以改进,因为开发人员的技术水平参差不齐,他们没有认识到他们使用了非正式的风格和技术体系来创建基于模型的对象(这些对象也充当了实用的、可运行的软件)。几个月过去了,开发工作由于巨大的复杂性而陷入困境,而团队对项目也失去了一致的认识。经过几年的努力,项目确实创建了一个适当的、有用的软件,但团队已经放弃了当初的宏伟抱负,也不再重视模型。

很多因素可能会导致项目偏离轨道,如官僚主义、目标不清、资源缺乏等。但真正决定软件复杂性的是设计方法。当复杂性失去控制时,开发人员就无法很好地理解软件,因此无法轻易、安全地更改和扩展它。而好的设计则可以为开发复杂特性创造更多机会。

一些设计因素是技术上的。软件的网络、数据库和其他技术方面的设计耗费了人们大量的精力。很多书籍都介绍过如何解决这些问题。大批开发人员很注意培养自己的技能,并紧跟每一次技术进步。

然而很多应用程序最主要的复杂性并不在技术上,而是来自领域本身、用户的活动或业务。当这种领域复杂性在设计中没有得到解决时,基础技术的构思再好也无济于事。成功的设计必须系统地考虑软件的这个核心方面。

本书有两个前提:

(1)在大多数软件项目中,主要的焦点应该是领域和领域逻辑;

(2)复杂的领域设计应该基于模型。

领域驱动设计是一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发。为了实现这个目标,本书给出了一整套完整的设计实践、技术和原则。

设计书就是讲设计,过程书只是讲过程。它们之间很少互相参考。设计和过程本身就是两个足够复杂的主题。本书是一本设计书,但我相信设计与过程这二者是密不可分的。设计思想必须被成功实现,否则它们就只是纸上谈兵。

当人们学习设计技术时,各种可能性令他们兴奋不已,然而真实项目的错综复杂又会为他们泼上一盆冷水。他们无法用所使用的技术来贯彻新的设计思想,或者不知道何时应该为了节省时间而放弃某个设计方面,何时又应该坚持不懈直至找到一个干净利落的解决方案。开发人员可以抽象地讨论设计原则的应用,而且他们也确实在进行着这样的讨论,但更自然的做法应该是讨论如何完成实际工作。因此,虽然本书是一本有关设计的书,但我会在必要的时候穿越这条人为设置的边界,进入过程的领域。这有助于将设计原则放到一个适当的语境下进行讨论。

虽然本书并不局限于某一种特定的方法,但主要还是面向“敏捷开发过程”这一新体系。特别地,本书假定项目必须遵循两个开发实践,要想应用书中所讲的方法,必须先了解这两个实践。

(1)迭代开发。人们倡导和实践迭代开发已经有几十年时间了,而且它是敏捷开发方法的基础。在敏捷开发和极限编程(XP)的文献中有很多关于迭代开发的精彩讨论,其中包括Surviving Object-Oriented Projects [Cockburn 1998] Extreme Programming Explained [Beck 1999]。

(2)开发人员与领域专家具有密切的关系。领域驱动设计的实质就是消化吸收大量知识,最后产生一个反映深层次领域知识并聚焦于关键概念的模型。这是领域专家与开发人员的协作过程,领域专家精通领域知识,而开发人员知道如何构建软件。由于开发过程是迭代式的,因此这种协作必须贯穿整个项目的生命周期。

极限编程的概念是由Kent Beck、Ward Cunningham和其他人共同提出的[Beck 2000],它是敏捷过程最重要的部分,也是我使用得最多的一种编程方法。为了使讨论更加具体,整本书都将使用XP作为基础讨论设计和过程的交互。本书论述的原则很容易应用于其他敏捷过程。

近年来,反对“精细开发方法学”(elaborate development methodology)的呼声渐起,人们认为无用的静态文档以及死板的预先规划和设计加重了项目的负担。相反,敏捷过程(如XP)强调的是应对变更和不确定性的能力。

极限编程承认设计决策的重要性,但强烈反对预先设计。相反,它将相当大的精力投入到促进沟通和提高项目快速变更能力的工作中。具有这种反应能力之后,开发人员就可以在项目的任何阶段只利用“最简单而管用的方案”,然后不断进行重构,一步一步做出小的设计改进,最终得到满足客户真正需要的设计。

这种极端的简约主义是解救那些过度追求设计的执迷者的良方。那些几乎没有价值的繁琐文档只会为项目带来麻烦。项目受到“分析瘫痪症”的困扰,团队成员十分担心会出现不完美的设计,这导致他们根本没法取得进展。这种状况必须得到改变。

遗憾的是,这些有关过程的思想可能会被误解。每个人对“最简单”都有不同的定义。持续重构其实是一系列小规模的重新设计,没有严格设计原则的开发人员将会创建出难以理解或修改的代码,这恰好与敏捷的精神相悖。而且,虽然对意外需求的担心常常导致过度设计,但试图避免过度设计又可能走向另一个极端——不敢做任何深入的设计思考。

实际上,XP最适合那些对设计的感觉很敏锐的开发人员。XP过程假定人们可以通过重构来改进设计,而且可以经常、快速地完成重构。但重构本身的难易程度取决于先前的设计选择。XP过程试图改善团队沟通,但模型和设计的选择有可能使沟通更明确,也有可能会使沟通不畅。

本书将设计和开发实践结合起来讨论,并阐述领域驱动设计与敏捷开发过程是如何互相增强的。在敏捷开发过程中使用成熟的领域建模方法可以加速开发。过程与领域开发之间的相互关系使得这种方法比任何“纯粹”真空式的设计更加实用。

本书分为4个部分。

第一部分 “运用领域模型”提出领域驱动开发的基本目标,这些目标是后面几部分中所讨论的实践的驱动因素。由于软件开发方法有很多,因此第一部分还定义了一些术语,并给出了用领域模型来驱动沟通和设计的总体含义。

第二部分 “模型驱动设计的构造块”将面向对象领域建模中的一些核心的最佳实践提炼为一组基本的构造块。这一部分主要是消除模型与实际运行的软件之间的鸿沟。团队一致使用这些标准模式就可以使设计井然有序,并且使团队成员更容易理解彼此的工作。使用标准模式还可以为公共语言贡献术语,使得所有团队成员可以使用这些术语来讨论模型和设计决策。

但这一部分的主旨是讨论一些能够保持模型和实现之间互相协调并提高效率的设计决策。要想达到这种协调,需要密切注意个别元素的一些细节。这种小规模的仔细设计为开发人员提供了一个稳固的基础,在此基础上就可以应用第三部分和第四部分讨论的建模方法了。

第三部分 “通过重构来加深理解”讨论如何将构造块装配为实用的模型,从而实现其价值。这一部分没有直接讨论深奥的设计原则,而是着重强调一个发现过程。有价值的模型不是立即就会出现的,它们需要对领域的深入理解。这种理解是一步一步得到的,首先需要深入研究模型,然后基于最初的(可能是不成熟的)模型实现一个初始设计,再反复改进这个设计。每次团队对领域有了新的理解之后,都需要对模型进行改进,使模型反映出更丰富的知识,而且必须对代码进行重构,以便反映出更深刻的模型,并使应用程序可以充分利用模型的潜力。这种一层一层“剥洋葱”的方法有时会创造一种突破的机会,使我们得到更深刻的模型,同时快速进行一些更深入的设计修改。

探索本身是永无止境的,但这并不意味着它是随机的。第三部分深入阐述一些指引我们保持正确方向的建模原则,并提供了一些指导我们进行探索的方法。

第四部分 “战略设计”讨论在复杂系统、大型组织以及与外部系统和遗留系统的交互中出现的复杂情况。这一部分探讨了作为一个整体应用于系统的3条原则:上下文、提炼和大型结构。战略设计决策通常由团队制定,或者由多个团队共同制定。战略设计可以保证在大型系统或应用程序(它们应用于不断延伸的企业级网络)上以较大规模去实现第一部分提出的目标。

本书通篇讨论使用的例子并不是一些过于简单的“玩具式”问题,而是全部选自实际项目。

本书的大部分内容实际上是作为一系列的“模式”编写的。但读者无需顾忌这一方法也应该能够理解本书,对模式的风格和格式感兴趣的读者可以参考附录。

补充材料可以参考http://domaindrivendesign.org,该网站提供了示例代码和社区讨论内容。

本书主要是为面向对象软件开发人员编写的。软件项目团队的大部分成员都能够从本书的某些部分获益。本书最适合那些正在项目上尝试这些实践的人员,以及那些已经在这样的项目上积累了丰富经验的人员。

要想从本书受益,掌握一些面向对象建模知识是非常必要的,如UML图和Java代码,因此一定要具备基本读懂这些语言的能力,但不必精通细节。了解极限编程的知识有助于从这个角度来理解开发过程的讨论,但不具备这一背景知识也能读懂这些内容。

一些中级软件开发人员可能已经了解面向对象设计的一些知识,也许读过一两本软件设计的书,那么本书将填补这些读者的知识空缺,向他们展示如何在实际的软件项目上应用对象建模技术。本书将帮助这些开发人员学会用高级建模和设计技巧来解决实际问题。

高级软件开发人员或专家可能会对书中用于处理领域的综合框架感兴趣。这种系统性的设计方法将帮助技术负责人指导他们的团队保持正确的方向。此外,本书从头至尾所使用的明确术语将有助于高级开发人员与他们的同行沟通。

本书采用记叙体,读者可以从头至尾阅读,也可以从任意一章的开头开始阅读。具有不同背景知识的读者可能会有不同的阅读方式,但我推荐所有读者从第一部分的引言和第1章开始阅读。除此之外,本书的核心是第2、3、9和14章。已经掌握一定知识的读者可以采取跳跃式阅读的方式,通过阅读标题和粗体字内容即可掌握要点。一些高级读者则可以跳过前两部分,重点阅读第三部分和第四部分。

除了这些主要读者以外,分析员和相关的技术项目经理也可以从阅读本书中获益。分析员在掌握了领域与设计之间的联系之后,能够在敏捷项目中作出更卓越的贡献,也可以利用一些战略设计原则来更有重点地组织工作。

项目经理感兴趣的重点是提高团队的工作效率,并致力于设计出对业务专家和用户有用的软件。由于战略设计决策与团队组织和工作风格紧密相关,因此这些设计决策必然需要项目领导者的参与,而且对项目的路线有着重要的影响。

尽管开发人员个人能够从理解领域驱动设计中学到有价值的设计技术和观点,但最大的好处却来自团队共同应用领域驱动设计方法,并且将领域模型作为项目沟通的核心。这样,团队成员就有了一种公共语言,可以用来进行更充分的沟通,并确保围绕软件来进行沟通。他们将创建出一个与模型步调一致的清晰的实现,从而为应用程序的开发提供帮助。所有人都了解不同团队的设计工作之间的互相联系,而且他们会一致将注意力集中在那些对组织最有价值、最与众不同的特性的开发上。

领域驱动设计是一项艰巨的技术挑战,但它也会带来丰厚的回报,当大多数软件项目开始僵化而成为遗留系统时,它却为你敞开了机会的大门。

这种表述指这是本书参考文献中提到的图书。——编者注


本书的创作历时4年多,其间经历了诸多工作形式的变化,在这个过程中很多人为我提供了帮助和支持。

感谢那些阅读本书书稿并提出意见的人。没有这些人的反馈意见,本书将不可能出版。其中有几个团队和一些人员对本书的评阅给予了特别的关注。由Russ Rufer和Tracy Bialek领导的硅谷模式小组(Silicon Valley Patterns Group)花费了几周时间详细审阅了本书完整的第一稿。由Ralph Johnson领导的伊利诺伊大学的阅读小组也花费了几周时间审阅了本书的第二稿。这些小组长期、精彩的讨论对本书产生了深远的影响。Kyle Brown和Martin Fowler提供了细致入微的反馈意见和宝贵的建议,也给了我无价的精神支持(在我们坐在一起钓鱼的时候)。Ward Cunningham的意见帮助我弥补了一些重大的缺陷。Alistair Cockburn在早期给了我很多鼓励,并和Hilary Evans一起帮助我完成了整个出版过程。David Siegel和Eugene Wallingford帮助我避免了很多技术上的错误。Vibhu Mohindra和Vladimir Gitlevich不厌其烦地检查了所有代码示例。

Rob Mee看了我对一些素材所做的早期研究,并在我尝试表达这种设计风格的时候与我进行了头脑风暴活动,帮我产生了很多新的想法。他后来又与我一起仔细探讨了后面的书稿。

本书在写作过程中经历了一次重大转折,这完全归功于Josh Kerievsky。他劝说我在写作本书时借鉴“亚历山大”模式 ,后来本书正是按这种方式组织的。在1999年PLoP会议临近时的忙碌时刻,Josh还帮我收集第二部分的材料,首次将它们组织为更严密的形式。这些材料成了一粒种子,本书大部分后续内容都是围绕这些内容创作的。

还要感谢Awad Faddoul,我有数百个小时坐在他的咖啡厅中写作。咖啡厅宁静优雅,窗外的湖面上总有片片风帆,我正是这样才坚持写下去。

此外还要感谢Martine Jousset、Richard Paselk和Ross Venables,他们拍摄了一些非常精美的照片,用来演示一些关键概念(参见本书后面的图片说明)。

在构思本书之前,我必须先要形成我自己对软件开发的看法和理解。这个过程得到了一些杰出人员的无私帮助,他们是我的良师益友。David Siegel、Eric Gold和Iseult White各自从不同方面帮助我形成了对软件设计的思考方式。同时,Bruce Gordon、Richard Freyberg和Judith Segal博士也从不同角度帮助我找到了项目的成功之路。

我自己的观念就是从那时的思想体系中自然而然发展形成的。有些内容我在正文中清楚地列了出来,并且在可能的地方标明了出处。还有些可能是十分基础的知识,我甚至自己都没有意识到它们对我产生了影响。

我的硕士论文导师Bala Subramanium博士是我在数学建模方面的引路人,当时我们用数学建模来进行化学反应动力学方面的研究。虽说建模本身没什么稀奇,但那时的工作是引导我创作本书的一部分原因。

在更早之前,我的母亲Carol和父亲Gary对我思维模式的形成产生了很大影响。还有几位特别值得一提的教师激发了我的兴趣,帮助我打下坚实的基础,在此感谢Dale Currier(我的高中数学老师)、Mary Brown(我的高中英文写作老师)和Josephine McGlamery(我上6年级时的自然科学老师)。

最后,感谢我的朋友和家人,以及Fernando De Leon,感谢他们一直以来给我的鼓励。

克里斯托弗•亚历山大(Christopher Alexander),1936年10月4日出生于奥地利的维也纳,是一名建筑师,以其设计理论和丰富的建筑设计作品而闻名于世。亚历山大认为,建筑的使用者比建筑师更清楚他们需要什么,他创造并以实践验证了“模式语言”,建筑模式语言赋予所有人设计并建造建筑的能力。亚历山大的代表作是《建筑模式语言》,该书对计算机科学领域中的“设计模式”运动产生了巨大的影响。亚历山大创立的增量、有机和连贯的设计理念也影响了“极限编程”运动。——编者注


上面这张图是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


几年前,我着手设计一个用于设计印制电路板(PCB)的专用软件工具。但有一个问题,我对电子硬件一无所知。当然,我也曾拜访过一些PCB设计师,但用不了3分钟,他们就令我晕头转向。如何才能了解足够多的知识,以便开始编写这个软件呢?当然,我并不打算在交付期限到来之前成为电子工程师。

我们试着让PCB设计师说明软件具体应该做些什么,但我们错了。虽然他们是优秀的电路设计师,但软件知识却太有限了,往往只知道如何读取一个ASCII文件、对它排序,然后添加一些注释并将它写回文件中,再生成一个报告。这些知识显然无法帮助他们大幅度提高效率。

最初的几次会面令人气馁,但我们在他们要求的报告中也看到了一丝希望。这些报告中总是涉及net这个词以及与其相关的各种细节。在这个领域中,net实质上是一种导线,它可以连接PCB上任意数量的元件,并向它连接的所有元件传递电子信号。这样,我们就得到了领域模型的第一个元素,如图1-1所示。

图1-1

7

就这样,我们一边讨论所需的软件功能,一边开始画图。我使用一种非正式的、稍加变化的对象交互图来走查各种场景,如图1-2所示。

图1-2

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所示)?

图1-3

为了让讨论更集中,接下来的一段时间我们探讨了一个特定的功能:探针仿真(probe simulation)。探针仿真跟踪信号的传播,以便检测在设计中可能出现特定类型问题的位置。

开发人员:现在我已经明白了Net是如何将信号传播给它所连接的所有Pin的,但如何将信号传送得更远呢?这与拓扑结构(topology)有关系吗?

专家2:没有,是元件推送信号前进。

开发人员:我们肯定无法对芯片的内部行为建模,因为这太复杂了。

专家2:我们不必这样做。可以使用一种简化形式。只需列出通过元件可从某些Pin将信号推送到其他引脚即可。

开发人员:类似于这样吗?

(经过反复的尝试和修改,我们终于共同绘制出了一个草图,如图1-4所示。)

图1-4

9

开发人员:但你想从这种计算中知道什么呢?

专家2:我们要查找较长的信号延迟,也就是说,查找超过2或3跳的信号路径。这是一条经验法则。如果路径太长,信号可能无法在时钟周期内到达。

开发人员:超过3跳……这么说我们需要计算路径长度。那么怎样算作一跳呢?

专家2:信号每通过一个Net,就称为1跳。

开发人员:那么我们可以沿着电路来计算跳数,每遇到一个net,跳数就加1,如图1-5所示。

图1-5

开发人员:现在我唯一不明白的地方是“推动”是从哪里来的。是否每个元件实例都需要存储该数据?

专家2:一个元件的所有实例的推动行为都是相同的。

开发人员:那么元件的类型决定了推动行为,而每个实例的推动行为都是相同的(如图1-6所示)?

图1-6

10

专家2:这个图的意思我没完全明白,但我猜想每个元件存储的推动行为就差不多是这样的吧。

开发人员:抱歉,这个地方我可能问得有点过细了。我只是想考虑得全面一些……现在,拓扑结构对它有什么影响吗?

专家1:拓扑结构不影响探针仿真。

开发人员:那么可以暂不考虑它,是吗?等用到这些特性时再回来讨论它。

就这样,我们的讨论一直进行下去(其中遇到的困难比上面显示的多得多)。我们一边进行“头脑风暴”式的讨论,一边对模型进行精化,边提问边回答。随着我对领域理解的加深,以及他们对模型在解决方案中作用的理解的加深,模型不断发展。图1-7显示了那个早期模型的类图。

图1-7

随后,我们又拿出一部分工作时间进行了几轮这样的讨论,我觉得自己已经理解了足够多的知识,可以试着编写一些代码了。我写了一个非常简单的原型,并用一个自动测试框架来测试它。我避开了所有的基础设施。这个原型没有持久化机制,也没有用户界面(UI)。这样我就可以专注于代码的行为。只不过几天我就能够演示简单的探针仿真了。虽然它使用的是虚拟数据,而且向控制台输出的是原始文本,但确实是使用Java对象对路径长度执行实际的计算。这些Java对象所反映的模型正是我和领域专家们一起开发出来的。

这个具体的原型使得领域专家们更清楚地理解了模型的含义,以及它与最终软件之间的联系。从那时起,我们的模型讨论越来越具有互动性了,因为他们可以看到我如何将新学到的知识融合到模型中,然后反映到软件上。他们也可以从原型得到具体的反馈,从而印证自己的想法。

11

模型中包含与我们要解决的问题有关的PCB领域知识,这些知识远远比我们在这里演示的复杂。模型将很多同义词和语言描写中的微小差别做了统一,并排除了数百条与问题没有直接关系的事实(虽然工程师们都理解这些事实),如元件的实际数字特性。像我这样的软件专业人员看到这张图后,几分钟内就能明白软件是做什么的。这个模型就相当于一个框架,开发人员可以借助它来组织新的信息并更快地学习,从而更准确地判断哪些部分重要,哪些部分不重要,并更好地与PCB工程师进行沟通。

当PCB工程师提出新的功能需求时,我就让他们带我走查对象交互的场景。当模型对象无法清楚地表达某个重要场景时,我们就通过头脑风暴活动创建新的模型对象或者修改原有的模型对象,并消化理解这些模型对象中的知识。在我们精化模型的过程中,代码也随之一步步演进。几个月后,PCB工程师们得到了一个远远超乎他们期望的功能丰富的工具。

以下几方面因素促使上述案例得以成功。

(1) 模型和实现的绑定。最初的原型虽然简陋,但它在模型与实现之间建立了早期链接,而且在所有后续的迭代中我们一直在维护该链接。

(2) 建立了一种基于模型的语言。最初,工程师们不得不向我解释基本的PCB问题,而我也必须向他们解释类图的含义。但随着项目的进展,双方都能够直接使用模型中的术语,并将它们组织为符合模型结构的语句,而且无需翻译即可理解互相要表达的意思。

(3) 开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。

12

(4) 提炼模型。在模型日趋完整的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,其他那些不要的概念就可以丢弃了。

(5) 头脑风暴和实验。语言和草图,再加上头脑风暴活动,将我们的讨论变成“模型实验室”,在这些讨论中可以演示、尝试和判断上百种变化。当团队走查场景时,口头表达本身就可以作为所提议的模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简捷,还是表达得很笨拙。

正是头脑风暴和大量实验的创造力才使我们找到了一个富含知识的模型并对它进行提炼,在这个过程中,基于模型的语言提供了很大帮助,而且贯穿整个实现过程中的反馈闭环也对模型起到了“训练”作用。这种知识消化将团队的知识转化为有价值的模型。

金融分析师要消化理解的内容是数字。他们筛选大量的详细数字,对其进行组合和重组以便寻求潜在的意义,查找可以产生重要影响的简单表示方式——一种可用作金融决策基础的理解。

高效的领域建模人员是知识的消化者。他们在大量信息中探寻有用的部分。他们不断尝试各种信息组织方式,努力寻找对大量信息有意义的简单视图。很多模型在尝试后被放弃或改造。只有找到一组适用于所有细节的抽象概念后,工作才算成功。这一精华严谨地表示了所发现的最为相关的知识。

13

知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。他们共同收集信息,并通过消化而将它组织为有用的形式。信息的原始资料来自领域专家头脑中的知识、现有系统的用户,以及技术团队以前在相关遗留系统或同领域的其他项目中积累的经验。信息的形式也多种多样,有可能是为项目编写的文档,有可能是业务中使用的文件,也有可能来自大量的讨论。早期版本或原型将经验反馈给团队,然后团队对一些解释做出修改。

在传统的瀑布方法中,业务专家与分析员进行讨论,分析员消化理解这些知识后,对其进行抽象并将结果传递给程序员,再由程序员编写软件代码。由于这种方法完全没有反馈,因此总是失败。分析员全权负责创建模型,但他们创建的模型只是基于业务专家的意见。他们既没有向程序员学习的机会,也得不到早期软件版本的经验。知识只是朝一个方向流动,而且不会累积。

有些项目使用了迭代过程,但由于没有对知识进行抽象而无法建立起知识体系。开发人员听专家们描述某项所需的特性,然后开始构建它。他们将结果展示给专家,并询问接下来做什么。如果程序员愿意进行重构,则能够保持软件足够整洁,以便继续扩展它;但如果程序员对领域不感兴趣,则他们只会了解程序应该执行的功能,而不去了解它背后的原理。虽然这样也能开发出可用的软件,但项目永远也不会从原有特性中自然地扩展出强大的新特性。

好的程序员会自然而然地抽象并开发出一个可以完成更多工作的模型。但如果在建模时只是技术人员唱独角戏,而没有领域专家的协作,那么得到的概念将是很幼稚的。使用这些肤浅知识开发出来的软件只能做基本工作,而无法充分反映出领域专家的思考方式。

14

在团队所有成员一起消化理解模型的过程中,他们之间的交互也会发生变化。领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发。领域专家被迫提炼自己已知道的重要知识的过程往往也是完善其自身理解的过程,而且他们会渐渐理解软件项目所必需的概念严谨性。

所有这些因素都促使团队成员成为更合格的知识消化者。他们对知识去粗取精。他们将模型重塑为更有用的形式。由于分析员和程序员将自己的知识输入到了模型中,因此模型的组织更严密,抽象也更为整洁,从而为实现提供了更大支持。同时,由于领域专家也将他们的知识输入到了模型中,因此模型反映了业务的深层次知识,而且真正的业务原则得以抽象。

模型在不断改进的同时,也成为组织项目信息流的工具。模型聚焦于需求分析。它与编程和设计紧密交互。它通过良性循环加深团队成员对领域的理解,使他们更透彻地理解模型,并对其进一步精化。模型永远都不会是完美的,因为它是一个不断演化完善的过程。模型对理解领域必须是切实可用的。它们必须非常精确,以便使应用程序易于实现和理解。

当开始编写软件时,其实我们所知甚少。项目知识零散地分散在很多人和文档中,其中夹杂着其他一些无关信息,因此我们甚至不知道哪些知识是真正需要的知识。看起来没什么技术难度的领域很可能是一种错觉——我们并没意识到不知道的东西究竟有多少。这种无知往往会导致我们做出错误的假设。

同时,所有项目都会丢失知识。已经学到了一些知识的人可能干别的事去了。团队可能由于重组而被拆散,这导致知识又重新分散开。被外包出去的关键子系统可能只交回了代码,而不会将知识传递回来。而且当使用典型的设计方法时,代码和文档不会以一种有用的形式表示出这些来之不易的知识,因此一旦由于某种原因人们没有口头传递知识,那么知识就丢失了。

15

高效率的团队需要有意识地积累知识,并持续学习[Kerievsky 2003]。对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧(如本书中所讲的那些技巧)。但这也包括认真学习他们正在从事的特定领域的知识。

那些善于自学的团队成员会成为团队的中坚力量,涉及最关键领域的开发任务要靠他们来攻克(有关这方面的更多内容,参见第15章)。这个核心团队头脑中积累的知识使他们成为更高效的知识消化者。

读到这里,请先停一下来问自己一个问题。你是否学到了一些PCB设计知识?虽然这个示例只对该领域作了些表面处理,但当讨论领域模型时,仍会学到一些知识。我学习了大量知识,但并没有学习如何成为一名PCB工程师,因为这不是我的目的。我的目的是学会与PCB专家沟通,理解与应用有关的主要概念,并学会检查所构建的内容是否合理。

事实上,我们的团队最终发现探针仿真并不是一项重要的开发任务,因此最后彻底放弃了这个功能。连同它一起删除的还有模型中的一些部分,这些部分只是帮助我们理解如何通过元件推动信号以及如何计算跳数。这样,应用程序的核心就转移到了别处,而且模型也随之改变,将新的重点作为核心。在这个过程中,领域专家们也学到了很多东西,而且更加清楚地理解了应用程序的目标(第15章会更深入地讨论这些问题)。

尽管如此,那些早期工作还是非常重要的。关键的模型元素被保留下来,而更重要的是,早期工作启动了知识消化的过程,这使得所有后续工作更加高效:团队成员、开发人员和领域专家等都学到了知识,他们开始使用一种公共的语言,而且形成了贯穿整个实现过程的反馈闭环。这样,一个发现之旅悄然开始了。

16

通过像PCB示例这样的模型获得的知识远远不只是“发现名词”。业务活动和规则如同所涉及的实体一样,都是领域的核心,任何领域都有各种类别的概念。知识消化所产生的模型能够反映出对知识的深层理解。在模型发生改变的同时,开发人员对实现进行重构,以便反映出模型的变化,这样,新知识就被合并到应用程序中了。

当我们的建模不再局限于寻找实体和值对象时,我们才能充分吸取知识,因为业务规则之间可能会存在不一致。领域专家在反复研究所有规则、解决规则之间的矛盾以及以常识来弥补规则的不足等一系列工作中,往往不会意识到他们的思考过程有多么复杂。软件是无法完成这一工作的。正是通过与软件专家紧密协作来消化知识的过程才使得规则得以澄清和充实,并消除规则之间的矛盾以及删除一些无用规则。

示例 提取一个隐藏的概念

我们从一个非常简单的领域模型开始学习,基于此模型的应用程序用来预订一艘船在一次航程中要运载的货物,如图1-8所示。

图1-8

我们规定这个应用程序的任务是将每件货物(Cargo)与一次航程(Voyage)关联起来,记录并跟踪这种关系。现在看来一切都还算简单。应用程序代码中可能会有一个像下面这样的方法:

public int makeBooking(Cargo cargo, Voyage voyage) {
   int confirmation = orderConfirmationSequence.next(); 
   voyage.addCargo(cargo, confirmation);
   return confirmation;
}

由于总会有人在最后一刻取消订单,因此航运业的一般做法是接受比其运载能力多一些的货物。这称为“超订”。有时使用一个简单的容量百分比来表示,如预订110%的载货量。有时则采用复杂的规则——主要客户或特定种类的货物优先。

17

这是航运领域的一个基本策略,从事航运业的业务人员都知道它,但在软件团队中可能不是所有技术人员都知道这条规则。

需求文档中包含下面这句话:

允许10%的超订。

现在,类图就应该像图1-9这样,代码如下:

public int makeBooking(Cargo cargo, Voyage voyage) {
 double maxBooking = voyage.capacity() * 1.1;
 if ((voyage.bookedCargoSize() + cargo.size()) > maxBooking) 
 return –1;
   int confirmation = orderConfirmationSequence.next(); 
   voyage.addCargo(cargo, confirmation);
   return confirmation;
}

图1-9

现在,一条重要的业务规则被隐藏在上面这段方法代码的一个卫语句中。第4章将介绍Layered Architecture,它会帮助我们将超订规则转移到领域对象中,但现在我们主要考虑如何把这条规则更清楚地表达出来,并让项目中的每个人都能了解到它。这将使我们得到一个类似的解决方案。

(1) 如果业务规则如上述代码所写,不可能有业务专家会通过阅读这段代码来检验规则,即使在开发人员的帮助下也无法完成。

(2) 非业务的技术人员很难将需求文本与代码联系起来。

如果规则更复杂,情况将更糟。

我们可以改变一下设计来更好地捕获这个知识。超订规则是一个策略,如图1-10所示。策略(policy)其实是Strategy模式[Gamma et al. 1995]的别名。我们知道,使用Strategy的动机一般是为了替换不同的规则,虽然在这里并不需要这么做。但我们要获取的概念的确符合策略的含义,这在领域驱动设计中是同等重要的动机(参见第12章)。

18

图1-10

修改后的代码如下:

public int makeBooking(Cargo cargo, Voyage voyage) {
 if (!overbookingpolicy.isallowed(cargo, voyage)) return –1;
   int confirmation = orderConfirmationSequence.next(); 
   voyage.addCargo(cargo, confirmation);
   return confirmation;
}

新的Overbooking Policy类包含以下方法:

public boolean isAllowed(Cargo cargo, Voyage voyage) {
   return (cargo.size() + voyage.bookedCargoSize()) <= 
         (voyage.capacity() * 1.1);
}

现在所有人都清楚超订是一个独特的策略,而且超订规则的实现即明确又独立。

现在,我并不建议将这样的精细设计应用到领域的每个细节中。第15章将深入阐述如何关注重点以及如何隔离其他问题或使这些问题最小化。这个例子的目的是说明领域模型和相应的设计可用来保护和共享知识。更明确的设计具有以下优点:

19

(1) 为了实现更明确的设计,程序员和其他各位相关人员都必须理解超订的本质,明白它是一个明确且重要的业务规则,而不只是一个不起眼的计算。

(2) 程序员可以向业务专家展示技术工件,甚至是代码,但应该是领域专家(在程序员指导下)可以理解的,以便形成反馈闭环。

有用的模型很少停留在表面。随着对领域和应用程序需求的理解逐步加深,我们往往会丢弃那些最初看起来很重要的表面元素,或者切换它们的角度。这时,一些开始时不可能发现的巧妙抽象就会渐渐浮出水面,而它们恰恰切中问题的要害。

前面的例子大体上是基于一个集装箱航运项目,这是本书列举的几个项目之一,本书还有几个示例会引用这个项目。本书所举的示例都很简单,即使不是航运专家也能理解它们。但在一个需要团队成员持续学习的真实项目中,要想建立实用且清晰的模型则要求团队成员既精通领域知识,也要精通建模技术。

在这个项目中,由于航运从预订货运开始,因此我们开发了一个能够描述货物和运货航线等事物的模型。这是必要且有用的,但领域专家却不买账。他们有自己的考虑业务的方式,这种方式是我们没有考虑到的。

最后,在经过几个月的知识消化后,我们知道货物的处理主要是由转包商或公司中的操作人员完成的,这包括实际的装货、卸货和运货。航运专家的观点是,各部分之间存在一系列的责任传递。法律责任和执行责任的传递由一个过程控制——从托运人传递到某个本地运输商,再从这家运输商传递到另一家运输商,最后到达收货人。通常,在一些重要的步骤中,货物停放在仓库里。在其他时间里,货物则是通过复杂的物理步骤来运输,而这些与航运公司的业务决策无关。在处理航线的物流之前,必须先确定诸如提单等法律文件以及支付流程。

20

对航运业务有了更深刻的认识后,我们并没有删除Itinerary(航线)对象,但模型发生了巨大改变。我们对航运业务的认识从“集装箱在各个地点之间的运输”转变为“运货责任在各个实体之间的传递”。处理这些责任传递的特性不再是一些附属于装货作业的次要特性,而是由一个独立的模型来提供支持,这个模型正是在理解了作业与责任之间的重要关系之后开发出来的。

知识消化是一种探索,它永无止境。

21

走查,walk through,原来是指一种非正式的代码评审活动,现在也广泛用于其他方面,一般是指一步步检查或分步讨论。——译者注

卫语句,guard clause,指起保护作用的语句。——译者注

Strategy一般是指定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。策略模式的优点是软件可以由许多可替换的部分组成,各个部分之间是弱连接的关系,这样软件具有更强的可扩展性、可维护性和可重用性。作者在这里提到策略模式,是指将每个规则当成一个算法。——译者注


领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型语言的语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。

这种基于模型的交流并不局限于UML(统一建模语言)图。为了最有效地使用模型,需要充分利用各种交流手段。基于模型的交流提高了书面文档的效用,也提高了敏捷过程中再度强调的非正式图表和交谈的效用。它还通过代码本身及对应的测试促进了交流。

在项目中,语言的使用很微妙,但却至关重要……

23

首先写下一个句子,

然后将它分成小段,

再将它们打乱并重新排序。

仿佛是巧合一样,

短语的顺序对意思完全没有影响。

——Lewis Carroll, “Poeta Fit, Non Nascitur”

要想创建一种灵活的、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言不断的试验——然而,软件项目上很少出现这样的试验。

虽然领域专家对软件开发的技术术语所知有限,但他们能熟练使用自己领域的术语——可能还具有各种不同的风格。另一方面,开发人员可能会用一些描述性的、功能性的术语来理解和讨论系统,而这些术语并不具备领域专家的语言所要传达的意思。或者,开发人员可能会创建一些用于支持设计的抽象,但领域专家无法理解这些抽象。负责处理问题不同部分的开发人员可能会开发出各自不同的设计概念以及描述领域的方式。

由于语言上存在鸿沟,领域专家们只能模糊地描述他们想要的东西。开发人员虽然努力去理解一个自己不熟悉的领域,但也只能形成模糊的认识。虽然少数团队成员会设法掌握这两种语言,但他们会变成信息流的瓶颈,并且他们的翻译也不准确。

在一个没有公共语言的项目上,开发人员不得不为领域专家做翻译。而领域专家需要充当开发人员与其他领域专家之间的翻译。甚至开发人员之间还需要互相翻译。这些翻译使模型概念变得混淆,而这会导致有害的代码重构。这种间接的沟通掩盖了分裂的形成——不同的团队成员使用不同的术语而尚不自知。由于软件的各个部分不能够浑然一体,因此这就导致无法开发出可靠的软件(参见第14章)。翻译工作导致各类促进深入理解模型的知识和想法无法结合到一起。

24

如果语言支离破碎,项目必将遭遇严重问题。领域专家使用他们自己的术语,而技术团队所使用的语言则经过调整,以便从设计角度讨论领域。

日常讨论所使用的术语与代码(软件项目的最重要产品)中使用的术语不一致。甚至同一个人在讲话和写东西时使用的语言也不一致,这导致的后果是,对领域的深刻表述常常稍纵即逝,根本无法记录到代码或文档中。

翻译使得沟通不畅,并削弱了知识消化。

然而任何一方的语言都不能成为公共语言,因为它们无法满足所有的需求。

所有翻译的开销,连带着误解的风险,成本实在太高了。项目需要一种公共语言,这种语言要比所有语言的最小公分母健壮得多。通过团队的一致努力,领域模型可以成为这种公共语言的核心,同时将团队沟通与软件实现紧密联系到一起。该语言将存在于团队工作中的方方面面。

Ubiquitous Language(通用语言)的词汇包括类和主要操作的名称。语言中的术语,有些用来讨论模型中已经明确的规则,还有一些则来自施加于模型上的高级组织原则(如第14章和第16章要讨论的Context Map和大型结构)。最后,团队常常应用于领域模型的模式名称也使这种语言更为丰富。

模型之间的关系成为所有语言都具有的组合规则。词和短语的意义反映了模型的语义。

开发人员应该使用基于模型的语言来描述系统中的工件、任务和功能。这个模型应该为开发人员和领域专家提供一种用于相互交流的语言,而且领域专家还应该使用这种语言来讨论需求、开发计划和特性。语言使用得越普遍,理解进行得就越顺畅。

至少,我们应该将它作为目标。但最初,模型可能不太好,因此无法很好地履行这些职责。它可能不会像领域的专业术语那样具有丰富的语义。但我们又不能直接使用那些术语,因为它们有歧义和矛盾。模型可能缺乏开发人员在代码中所创建的更为微妙和灵活的特性,这要么是因为开发人员认为模型不必具备这些特性,要么是因为编码风格是过程式的,只能隐含地表达领域概念。

25

尽管模型和基于模型的语言之间的次序像是循环论证,但是,能够产生更有用模型的知识消化过程依赖于团队投身于基于模型的语言。持续使用Ubiquitous Language可以暴露模型中存在的缺点,这样团队就可以尝试并替换不恰当的术语或组合。当在语言中发现缺失时,新的词语将被引入到讨论中。这些语言上的更改也会在领域模型中引起相应的更改,并促使团队更新类图并重命名代码中的类和方法,当术语的意义改变时,甚至会导致行为也发生改变。

通过在实现的过程中使用这种语言,开发人员能够指出不准确和矛盾之处,并和领域专家一起找到有效的替代方案。

当然,为了解释和给出更广泛的上下文,领域专家的语言会超出Ubiquitous Language的范围。但在模型应对的范围内,他们应该使用Ubiquitous Language,并在发现不合适、不完整或错误之处后要引起注意。通过大量使用基于模型的语言,并且不达流畅绝不罢休,我们可以逐步得到一个完整的、易于理解的模型,它由简单元素组成,并通过组合这些简单元素表达复杂的概念。

因此:

将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。

通过尝试不同的表示方法(它们反映了备选模型)来消除难点。然后重构代码,重新命名类、方法和模块,以便与新模型保持一致。解决交谈中的术语混淆问题,就像我们对普通词汇形成一致的理解一样。

26

要认识到,Ubiquitous Language的更改就是对模型的更改。

领域专家应该抵制不合适或无法充分表达领域理解的术语或结构,开发人员应该密切关注那些将会妨碍设计的有歧义和不一致的地方。

有了Ubiquitous Language,模型就不仅仅是一个设计工件了。它成为开发人员和领域专家共同完成的每项工作中不可或缺的部分。语言以动态形式传递知识。使用这种语言进行讨论能够呈现图和代码背后的真实含义。

我们在这里讨论的Ubiquitous Language假设只有一个模型在起作用。第14章将讨论不同模型(和语言)的共存,以及如何防止模型分裂。

Ubiquitous Language是那些以非代码形式呈现的设计的主要载体,这些包括把整个系统组织在一起的大尺度结构(参见第16章)、定义了不同系统和模型之间关系的限界上下文(参见第14章),以及在模型和设计中使用的其他模式。

示例 制定货运路线

下面这两段对话有着微妙但重要的差别。在每个对话场景中,注意观察讲话者有多少内容是谈论软件的业务功能,有多少内容是从技术上谈论软件的工作机理的。用户和开发人员用的是同一种语言吗?它的表达是否丰富,足以应对应用程序功能的讨论?

场景1:最小化的领域抽象

图2-1

用户:那么,当更改清关(customs clearance)地点时,需要重新制定整个路线计划

开发人员:是的。我们将从货运表(shipment table)中删除所有与该货物id相关联的行,然后将出发地、目的地和新的清关地点传递给Routing Service,它会重新填充货运表。Cargo中必须设立一个布尔值,用于指示货运表中是否有数据。

用户:删除行?好,就按你说的做。但是,如果先前根本没有指定清关地点,也需要这么做吗?

开发人员:是的,无论何时更改了出发地、目的地或清关地点(或是第一次输入),都将检查是否已经有货运数据,如果有,则删除它们,然后由Routing Service重新生成数据。

用户:当然,如果原有的清关数据碰巧是正确的,我们就不需要这样做了。

开发人员:哦,没问题。但让Routing Service每次重新加载或卸载数据会更容易些。

用户:是的,但为新航线制定所有支持计划的工作量很大,因此,除非非改不可,我们一般不想更改航线。

28

开发人员:哦,好的,当第一次输入清关地点时,我们需要查询表格,找到以前的清关地点,然后与新的清关地点进行比较,从而判断是否需要重做。

用户:这个处理不必考虑出发地和目的地,因为航线在此总要变更。

开发人员:好的,我明白了。

场景2:用领域模型进行讨论

图2-2

用户:那么,当更改清关地点时,需要重新制定整个路线计划

开发人员:是的。当更改Route Specification(路线说明)的任意属性时,都将删除原有的Itinerary(航线),并要求Routing Service(路线服务)基于新的Route Specification生成一个新的Itinerary。

用户:如果先前根本没有指定清关地点,也需要这么做吗?

开发人员:是的,无论何时更改了Route Spec的任何属性,都将重新生成Itinerary。这也包括第一次输入某些属性。

29

用户:当然,如果原有的清关数据碰巧是正确的,我们就不需要这样做了。

开发人员:哦,没问题。但让Routing Service每次重新生成一个Itinerary会更容易些。

用户:是的,但为新航线制定所有支持计划的工作量很大,因此,除非非改不可,我们一般不想更改路线。

开发人员:哦。那么需要在Route Specification添加一些功能。这样,当更改Route Specification中的属性时,查看Itinerary是否仍满足Specification。如果不满足,则需要由Routing Service重新生成Itinerary。

用户:这一点不必考虑出发地和目的地,因为Itinerary在此总是要变更的。

开发人员:好的,但每次只做比较就简单多了。只有当不满足Route Specification时,才重新生成Itinerary。

第二段对话表达了领域专家的更多意图。在这两段对话中,用户都使用了“itinerary”这个词,但在第二段中它是一个对象,这使得双方可以更准确、具体地进行讨论。他们明确讨论了“route specification”,而不是每次都通过属性和过程来描述它。

这两段对话有意使用了相似的结构。实际上,第一段对话显得更啰嗦,对话双方需要不断对应用程序的特性和表达不清的地方进行解释。第二段对话使用了基于领域模型的术语,因此讨论更简洁。

假如将交谈从沟通方式中除去的话,那会是巨大的损失,因为人类本身颇具谈话的天赋。遗憾的是,当人们交谈时,通常并不使用领域模型的语言。

可能开始时你并不认为上述论断是正确的,而且的确有例外情况。但下次你参加需求或设计讨论时,不妨认真听一下。你将听到人们用业务术语或者各种业余术语来描述功能。还会听到人们讨论技术工件和具体的功能。当然,你还会听到来自领域模型的术语;在人们共同使用的那部分业务术语中,那些显而易见的名词在编码时通常被用作对象名称,因此这些术语经常被人们提及。但你是否也听到一些使用当前领域模型中的关系和交互来描述的措辞呢?

30

改善模型的最佳方式之一就是通过对话来研究,试着大声说出可能的模型变化中的各种结构。这样不完善的地方很容易被听出来。

“如果我们向Routing Service提供出发地、目的地和到达时间,就可以查询货物的停靠地点,嗯……将它们存到数据库中。”(含糊且偏重于技术)

“出发地、目的地……把它们都输入到Routing Service中,而后我们得到一个Itinerary,它包含我们所需的全部信息。”(更具体,但过于啰嗦)

“Routing Service查找满足Route Specification的Itinerary。”(简洁)

使用单词和短语是极为重要的——其将我们的语言能力用于建模工作,这就如同素描对于表现视觉和空间推理十分重要一样。我们即要利用系统性分析和设计方面的分析能力,也要利用对代码的神秘“感觉”。这些思考方式互为补充,要充分利用它们来找到有用的模型和设计。在所有这些方式中,语言上的试验常常是最容易被忽视的(本书第三部分将深入探讨这种发现过程,并通过几段对话来显示它们之间的相互影响)。

事实上,我们的大脑似乎很擅长处理口语的复杂性(对于像我这样的门外汉,有本好书是Steven Pinker所著的The Language Instinct [Pinker 1994])。例如,当具有不同语言背景的人凑在一起做生意时,如果没有公共语言,他们就会创造一种称为“混杂语”(pidgin)的公共语言。混杂语虽然不像讲话者的母语那样详尽,但它适合当前任务。当人们交谈时,自然会发现词语解释和意义上的差别,而后会自然而然地解决这些差别。他们会发现这种语言中的简陋晦涩之处并把它们搞顺畅。

31

上大学时,我曾经修过西班牙语速成课。课堂上规定不准讲英语。起初,令人相当沮丧。这不仅感觉很别扭,而且需要很强的自制力。但最终我和同学们都达到了通过书面练习永远不可能达到的流利程度。

当我们在讨论中使用领域模型的Ubiquitous Language时,特别是在开发人员和领域专家一起推敲场景和需求时,通用语言的使用会越来越流利,而且我们还可以互相指点一些细微的差别。我们自然而然地共享了我们所说的语言,而这种方式是图和文档无法做到的。

想要在软件项目上产生一种Ubiquitous Language,说起来容易,做起来却难,我们必须充分利用自然赋予我们的才能来实现这个目标。正如人类的视觉和空间思维能力使我们能够快速传达和处理图形概述中的信息一样,我们也可以利用自己在基于语法的、有意义的语言方面的天赋来推动模型的开发。

因此,下面这段话可作为Ubiquitous Language模式的补充:

讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。

技术人员通常认为业务专家最好不要接触领域模型,他们认为:

“领域模型对他们来说太抽象了。”

“他们不理解对象。”

32

“这样我们就不得不用他们的术语来收集需求。”

上面只列举了我从一个使用两种语言的团队中听到的少数几个原因。忘掉它们吧。

当然,设计中有一些技术组件与领域专家无关,但模型的核心最好让他们参与。过于抽象?那你怎么知道抽象是否合理?你是否像他们一样深入理解领域?有时,某些特定需求是从底层用户那里收集的,他们在描述这些需求时可能会用到一小部分更具体的术语,但领域专家应该能够更深入地思考他们所从事的领域。如果连经验丰富的领域专家都不能理解模型,那么模型一定出了什么问题。

最初,当用户讨论系统尚未建模的未来功能时,他们没有模型可供使用。但当他们开始与开发人员一起仔细讨论这些新想法时,探索共享模型的过程就开始了。最初的模型可能很笨拙且不完整,但会逐渐精化。随着新语言的演进,领域专家必须付出更多努力来适应它,并更新那些仍然很重要的旧文档。

当领域专家使用这种语言互相讨论,或者与开发人员进行讨论时,很快就会发现模型中哪些地方不符合他们的需要,甚至是错误的。另一方面,模型语言的精确性也会促使领域专家(在开发人员的帮助下)发现他们想法中的矛盾和含糊之处。

开发人员和领域专家可以通过一步一步地使用模型对象来走查场景,从而对模型进行非正式的测试。每次讨论都是开发人员和专家一起使用模型的机会,在这个过程中,他们可以加深彼此的理解,并对概念进行精化。

领域专家可以使用模型语言来编写用例,甚至可以直接利用模型来具体说明验收测试。

有时,有人会反对使用模型语言来收集需求。毕竟,难道需求不应该独立于实现它们的设计吗?这种观点忽视了所有语言都要基于某种模型这一事实。词的意义是不明确。领域模型通常是从领域专家自己的术语中推导出来的,但已经经过了“清理”,以便具有更明确、更严密的定义。当然,如果这些定义与领域公认的意义有较大差别,领域专家应该反对。在敏捷过程中,需求是随着项目的前进而演变的,因为几乎不存在现成的知识可以充分说明一个应用程序。用精化后的Ubiquitous Language来重新组织需求应该是这种演变过程的一部分。

33

语言的多样性通常是必要的,但领域专家与开发人员之间不应该有语言上的分歧(第12章将讨论多个模型在同一个项目上共存的情况)。

当然,开发人员的确会使用领域专家无法理解的技术术语。开发人员有其所需的大量术语来讨论系统技术。几乎可以肯定的是,用户也会用开发人员无法理解的、超出应用程序范畴的专用术语。这些都是对语言的扩展。但在这些语言扩展中,同一领域的相同词汇不应该反映不同的模型。

图2-3 术语的交集产生了Ubiquitous Language

34

有了Ubiquitous Language之后,开发人员之间的对话、领域专家之间的讨论以及代码本身所表达的内容都基于同一种语言,都来自于一个共享的领域模型。

每当我参加讨论软件设计的会议时,如果不在白板或画板上画图,我就很难讨论下去。我画的大部分是UML图,主要以类图和对象交互图为主。

有些人天生是视觉动物,图可以帮助人们掌握某些类型的信息。UML图在传达对象之间的关系上真是游刃有余,而且也很擅长表现交互。但它们却无法给出这些对象的概念定义。在会议中,我会一边画图一边用语言来丰富它们的意义,或者在与其他参与者讨论时进行解释。

简单、非正式的UML图能够维系整个讨论。绘制一幅包含当前问题最关键的3~5个对象的图,这样每个人都可以集中注意力。所有人就对象关系会达成一致的认识,更重要的是,他们将使用相同的对象名称。如此,口头讨论会更加高效。当人们尝试不同的想法时,图也随之改变,草图在某种程度上可以反映讨论的变化,这是讨论中真正重要的部分。毕竟,UML就是统一建模语言。

当人们必须通过UML图表示整个模型或设计时,麻烦也随之而来。很多对象模型图在某些方面过于细致,同时在某些方面又有很多遗漏。说它们过于细致是因为人们认为必须将所有要编码的对象都放到建模工具中。而细节过多的结果是“只见树木,不见森林”。

尽管存在所有这些细节,但属性和关系只是对象模型的一部分。这些对象的行为以及这些对象上的约束就不那么容易表示了。对象交互图可以阐明设计中的一些复杂之处,但却无法用这种方式来展示大量的交互,就是工作量太大了,既要制作图,还要学习这些图。而且交互图也只能暗示出模型的目的。要想把约束和断言包括进来,需要在UML图中使用文本,这些文本用括号括起来,插入到图中。

35

操作名称可能会暗示出对象的行为职责,对象交互图(或序列图)中也会隐含地展示出这些职责,但无法直接表述。因此,这项任务就要靠补充文本或对话来完成。换言之,UML图无法传达模型的两个最重要的方面,一个方面是模型所表示的概念的意义,另一方面是对象应该做哪些事情。但是,这并不是大问题,因为通过仔细地使用语言(英语、西班牙语或其他任何一种语言)就可以很好地完成这项任务。

UML也不是一种十分令人满意的编程语言。我从未见过有人使用建模工具的代码生成功能达到了预期目的。如果UML的能力无法满足需要,通常人们就不得不忽略模型最关键的部分,因为有些规则并不适合用线框图来表示。当然,代码生成器也无法使用上面所说的那些文本注释。如果确实能使用UML这样的绘图语言来编写可执行程序,那么UML图就会退化为程序本身的另一种视图,这样,“模型”的真正含义就丢失了。如果使用UML作为实现语言,则仍然需要利用其他手段来表达模型的确切含义。

图是一种沟通和解释手段,它们可以促进头脑风暴。简洁的小图能够很好地实现这些目标,而涵盖整个对象模型的综合性大图反而失去了沟通或解释能力,因为它们将读者淹没在大量细节之中,加之这些图也缺乏目的性。鉴于此,我们应避免使用包罗万象的对象模型图,甚至不能使用包含所有细节的UML数据存储库。相反,应使用简化的图,图中只包含对象模型的重要概 念——这些部分对于理解设计至关重要。本书中的图都是我在项目中使用过比较典型的图。它们很简单,而且具有很强的解释能力,在澄清一些要点时,还使用了一些非标准的符号。它们显示了设计约束,但它们不是面面俱到的设计规范。它们只体现了思想纲要。

36

设计的重要细节应该在代码中体现出来。良好的实现应该是透明的,清楚地展示其背后的模型(下一章及本书其他许多章节的主题就是阐述如何做到这一点)。互为补充的图和文档能够引导人们将注意力放在核心要点上。自然语言的讨论可以填补含义上的细微差别。这就是为什么我喜欢把典型的UML使用方法颠倒过来的原因。通常的用法是以图为主,辅以文本注释;而我更愿意以文本为主,用精心挑选的简化图作为说明。

务必要记住模型不是图。图的目的是帮助表达和解释模型。代码可以充当设计细节的存储库。书写良好的Java代码与UML具有同样的表达能力。经过仔细选择和构造的图可以帮助人们集中注意力,并起到指导作用,当然前提条件是不能强制用图来表示全部模型或设计,因为这样会削弱图的清晰表达的能力。

口头交流可以解释代码的含义,因此可作为代码精确性和细节的补充。虽然交谈对于将人们与模型联系起来是至关重要的,但书面文档也是必不可少的,任何规模的团队都需要它来提供稳定和共享的交流。但要想编写出能够帮助团队开发出好软件的书面文档却是一个不小的挑战。

一旦文档的形式变得一成不变,往往会从项目进展流程中脱离出来。它会跟不上代码或项目语言的演变。

书面文档有很多编写方法。本书第四部分将介绍几种满足特定需要的具体文档,但不会列出项目需要使用的所有文档,而是给出两条用于评估文档的总体原则。

文档应作为代码和口头交流的补充

每种敏捷过程在编写文档方面都有自己的理念。极限编程主张完全不使用(多余的)设计文档,而让代码解释自己。实际运行的代码不会说谎,而其他文档则不然。运行代码所产生的行为是明确的。

37

极限编程只关注对程序及可执行测试起作用的因素。由于为代码添加的注释并不影响程序的行为,因此它们往往无法与当前代码及其模型保持同步。外部文档和图也不会影响程序的行为,因此它们也无法保持同步。另一方面,口头交流和临时在白板上画的图不会长久保留而产生混淆。依赖代码作为交流媒介可以促使开发人员保持代码的整洁和透明。

然而,将代码作为设计文档也有局限性。它可能会把读代码的人淹没在细节中。尽管代码的行为是非常明确的,但这并不意味着其行为是显而易见的。而且行为背后的意义可能难以表达。换言之,只用代码做文档与使用大而全的UML图面临着差不多相同的基本问题。当然,团队进行大量的口头交流能够为代码提供上下文和指导,但是,口头交流很短暂,而且范围很小。此外,开发人员并不是唯一需要理解模型的人。

文档不应再重复表示代码已经明确表达出的内容。代码已经含有各个细节,它本身就是一种精确的程序行为说明。

其他文档应该着重说明含义,以便使人们能够深入理解大尺度结构,并将注意力集中在核心元素上。当编程语言无法直接明了地实现概念时,文档可以澄清设计意图。我们应该把书面文档作为代码和口头讨论的补充。

文档应当鲜活并保持最新

我在为模型编写书面文档时,会仔细选择一个小的模型子集来画图,然后让文字放置在这些图周围。我用文字定义类及其职责,并且像自然语言那样把它们限定在一个语义上下文中。而图显示了在将概念形式化和简化为对象模型的过程中所做的一些选择。这些图可以随意一些,甚至是手绘的。手绘图除了节省工作量,也让人们一看就知道它们是不正式、临时的。这些优点都非常有利于交流,因为它们适用于我们的模型思想。

38

设计文档的最大价值在于解释模型的概念,帮助在代码的细节中指引方向,或许还可以帮助人们深入了解模型预期的使用风格。根据不同的团队理念,整个设计文档可能会十分简单,如只是贴在墙上的一组草图,也可能会非常详尽。

文档必须深入到各种项目活动中去。判断是否做到这一点的最简单方法,是观察文档与Ubiquitous Language之间的交互。文档是用人们(当前)在项目上讲的语言编写的吗?它是用嵌入到代码中的语言编写的吗?

注意听Ubiquitous Language,观察它是如何变化的。如果设计文档中使用的术语不再出现在讨论和代码中,那么文档就没有起到它的作用。或许是文档太大或太复杂了,或许是它没有关注足够重要的主题。人们要么不阅读文档,要么觉得它索然无味。如果文档对Ubiquitous Language没有影响,那么一定是出问题了。

相反,我们会注意到Ubiquitous Language随着文档渐渐过时而自然地改变。显然,要么人们不再关心文档,要么认为它不重要而不再去更新它。这时可以将它作为历史文件安全地归档,如果继续使用这样的文档可能会产生混淆并损害项目。如果文档不再担负重要的作用,那么纯粹靠意志和纪律保持其更新就是浪费精力。

Ubiquitous Language可以使其他文档(如需求规格说明)更简洁和明确。当领域模型反映了与业务最相关的知识时,应用程序的需求成为该模型内部的场景,而Ubiquitous Language可直接用Model-Driven Design(模型驱动设计)的方式描述此类场景(参见第3章)。结果就是规格说明的编写更简单,因为它们不必传达模型背后隐含的业务知识。

39

通过将文档减至最少,并且主要用它来补充代码和口头交流,就可以避免文档与项目脱节。根据Ubiquitous Language及其演变来选择那些需要保持更新并与项目活动紧密交互的文档。

现在,我们来考查一下XP社区和其他一些人为何选择几乎完全依赖可执行代码及其测试。本书主要讨论了如何通过Model-Driven Design使代码表达出设计的含义(参见第3章)。良好的代码具有很强的表达能力,但它所传递的信息不能确保是准确的。一段代码所产生的实际行为是不会改变的。但是,方法名称可能会有歧义、会产生误导或者因为已经过时而无法表示方法的本质含义。测试中的断言是严格的,但变量和代码组织方式所表达出来的意思未必严格。好的编程风格会尽力使这种联系直接化,但其仍然主要靠开发人员的自律。编码时需要一丝不苟的态度,只有这样才能编写出“言行全部正确”的代码。

消除这些差异是诸如声明式设计(参见第10章)这样的方法的最大优点,在这类方法中,程序元素用途的陈述决定了它在程序中的实际行为。从UML生成程序的部分动机就来源于此,虽然目前看来这通常不会得到好的结果。

尽管代码可能会产生误导,但它仍然比其他文档更基础。要想利用当前的标准技术使代码所传达的消息与它的行为和意图保持一致,需要纪律和思考设计的特定方式(第三部分将详细讨论这些问题)。要有效地交流,代码必须基于在编写需求时所使用的同一种语言,也就是开发人员之间、开发人员与领域专家之间进行讨论时所使用的语言。

40

本书的核心思想是在实现、设计和团队交流中使用同一个模型作为基础。如果各有各的模型,将会造成危害。

模型在帮助领域学习方面也具有很大价值。对设计起到推动作用的模型是领域的一个视图,但为了学习领域,还可以引入其他视图,这些视图只用作传递一般领域知识的教学工具。出于此目的,人们可以使用与软件设计无关的其他种类模型的图片或文字。

使用其他模型的一个特殊原因是范围。驱动软件开发过程的技术模型必须经过严格的精简,以便用最小化的模型来实现其功能。而解释性模型则可以包含那些提供上下文的领域方面——这些上下文用于澄清范围更窄的模型。

解释性模型提供了一定的自由度,可以专门为某个特殊主题定制一些表达力更强的风格。领域专家在一个领域中所使用的视觉隐喻通常呈现了更清晰的解释,这可以教给开发人员领域知识,同时使领域专家们的意见更一致。解释性模型还可以以一种不同的方式来呈现领域,并且各种不同角度的解释有助于人们更好地学习。

解释性模型不必是对象模型,而且最好不是。实际上在这些模型中不使用UML是有好处的,这样可以避免人们错误地认为这些模型与软件设计是一致的。尽管解释性模型与驱动设计的模型往往有对应关系,但它们并不完全类似。为了避免混淆,每个人都必须知道它们之间的区别。

示例 航运操作和路线

考虑一个用来追踪航运公司货物的应用程序。模型包含一个详细的视图,它显示了如何将港口装卸和货轮航次组合为一次货运的操作计划(“路线”)。如图2-4所示。但对外行而言,类图可能起不到多大的说明作用。

41

图2-4 航运路线的类图

在这种情况下,解释性模型可以帮助团队成员理解类图的实际含义。图2-5是表示相同概念的另一种方式。

图2-5 航运路线的解释性模型

图中的每根线段都表示货物的一种状态——或者正在港口装卸(装货或卸货),或者停放在仓库里,或者正在运输途中。这个图并没有与类图中的细节一一对应,但强调了领域的要点。

42

这种图连同对它所表示的模型的自然语言解释,能够帮助开发人员和领域专家理解更严格的软件模型图。综合使用这两种图要比单独使用一种图更容易理解。

43

清关即结关,习惯上又称通关,是指进口货物、出口货物和转运货物进出一国海关或国境时必须向海关申报,办理海关规定的各项手续,履行各项法规规定的义务。——译者注


本书中的所有图片均已得到使用许可。

Richard A. Paselk, Humboldt State University

星盘图(第3章,P30)

© Royalty-Free/Corbis

指印(第5章,P56),加油站(第5章,P67),Auto

工厂(第6章,P89),图书管理员(第6章,P97)

Martine Jousset

葡萄(第6章,P81),新种植的和长大后的橄榄林(结束语,P346和P347)

Biophoto Associates/Photo Researchers, Inc.

电子显微镜下的颤藻细胞(第14章,P235)

Ross J. Venables

划手(一群和单个)(第14章,P239和P260)

Photodisc Green/Getty Images

赛跑者(第14章,P250),儿童(第14章,P253)

U.S. National Oceanic and Atmospheric Administration

中国长城(第14章,P255)

© 2003 NAMES Project Foundation, Atlanta, Georgia.

Photographer Paul Margolies. www.aidsquilt.org

艾滋拼被(第16章,P303)


相关图书

程序员的README
程序员的README
有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程