领域驱动设计

最早知道领域驱动设计是在2002年,那年9月份在伊利诺伊大学香槟分校参加PLoP2002(Pattern Language of Program),Eric Evans 的论文《Domain Driven Design ——Tackling Complexity in the Heart of Business Software 》在大模式语言小组进行评审,这篇长论文就是后面《领域驱动设计——软件核心复杂性应对之道》一书的前身。从那时起对领域驱动设计有了兴趣,总想找机会试一试。

2006年,笔者承担一个大型项目的设计,在项目的初始设计阶段决定在一个子系统中尝试使用领域驱动设计进行开发:先建立领域模型,然后使用存储库模式完成持久化。建模进行得比较顺利,使用内存存储库也可以完成模拟测试工作,但到了编写针对关系数据库的存储库时遇到了麻烦。当时使用的技术是.Net Framework 2.0,.Net当时提供的数据库访问技术是ADO.Net,基于数据集和数据表(DataSet和DataTable)完成持久化并与数据库打交道。那时还没有Entity Framework等ORM框架,我们需要自己编写对象的“开箱”和“装箱”代码。最开始不觉得是什么大问题,但编写起来发现没有那么简单。其中一个问题就是信息隐藏,如果我们希望保护一个实体的属性,就不能暴露这个属性的set方法,但当从数据库还原实体时,需要能够使用set为属性赋值。当然我们可以使用反射技术,但这极大增加了技术复杂程度,会使开发人员的注意力从业务转到对特定技术的研究,不符合项目的整体要求。最终的结果还是决定采用当时的主流技术完成项目,领域驱动设计只能作为阶段试验暂停了。

从实践的经验来看,领域驱动设计需要有强大的技术支撑才能够实现,当21世纪进入第二个十年之后,软件技术的发展使领域驱动设计有了坚实的基础。首先,ORM框架越来越成熟,很多框架内置存储库模式,领域对象的存储已经基本不是问题。然后,也是更重要的,微服务技术为限界上下文提供了理想的物理边界。从某种角度来说,微服务技术与领域驱动设计互相成就:领域驱动设计为微服务划分提供了理论基础,微服务技术为领域驱动设计提供了落地的技术保障。所以,随着微服务技术的广泛使用,领域驱动设计成为热门的设计方法。

为什么要了解领域驱动设计

软件的复杂性是天然的,既有业务复杂性,也有技术复杂性,领域驱动设计的目的就是试图给出应对软件复杂性的方法,Eric Evans著名的《领域驱动设计——软件核心复杂性应对之道》书名说明了这一点。但由于对“复杂性”的理解不同,导致了很多人认为所开发的系统没那么复杂,所以没有必要使用领域驱动设计。在实践中,需要编程完成的项目都是不“简单”的,一方面由于所处的领域不同,对用户或业务专家而言是简单的问题,对于开发人员来说往往是复杂的;另一方面,很多“复杂”的项目往往是从起初被认为是“简单”的项目演化而来的。所以说,不管在实际项目中是否使用领域驱动设计,了解相关的理论和概念都是非常必要的。

领域驱动设计使用中的难点

与所有分析设计方法一样,将理论应用到实践总会遇到各种各样的问题。有很多问题是相同的,诸如将某种方法作为包治百病的万灵药(英文中一般称银弹),团队的组织问题、与用户的沟通问题等等。这里只列出与领域驱动设计相关的问题。

对软件复杂性理解的偏差

可能是由于Eric Evans著名的《领域驱动设计——软件核心复杂性应对之道》书名的原因,领域驱动设计被认为是开发复杂软件的方法,只有架构师或者高级开发人员才能涉足,而大多数开发人员要么觉得过于高深而望而却步,要么觉得所开发的软件不那么复杂,没有必要使用。而实际上,软件的复杂性不单决定于规模,更决定于所要实现的业务。在实践中,需要编程完成的项目都是不“简单”的,一方面由于所处的领域不同,对用户或业务专家而言是简单的问题,对于开发人员来说却是复杂的;另一方面,很多最终“复杂”的项目往往是从起初被认为是“简单”的项目演化而来的。很多情况是,当意识到软件的复杂性时,项目已经进行一大半了,只能硬着头皮做下去。

术语的理解

领域驱动设计引入了很多术语,其中有些术语与其它设计方法中的术语在字面上是相同的,但实际含义不同,这往往对领域驱动的学习和使用造成障碍。实体、值对象、聚合这些术语在不同的上下文中的含义是不同的。这里通过几个例子进行简单的说明。

  • 通用语言(UL):在软件开发的语境中提到“语言”,给人的第一感觉是一种完备的编程语言或者建模语言,比如“统一建模语言(UML)”。而“通用语言”所强调的是在项目团队中的“通用”:参与项目的领域专家和开发者使用同一种语言进行交流,在设定的范围内(限界上下文),语言中的术语没有二义性。所以“通用语言”应该理解为“限界上下文内的专用语言”。

  • 实体:“实体”是一个使用广泛的术语,在很多设计方法中都有使用,这里简单说明“实体-关系”模型和UML中的实体与领域驱动设计中实体的区别。在“实体-关系”模型中,“实体”是数据的抽象和概念表示,而在领域驱动设计中,“实体”不仅包含数据抽象(属性),还包含行为抽象(方法和函数)。在UML中,将类分为三种:“实体类”、“控制类”和“边界类”,这里的“实体类”强调的是数据,虽然也包括方法,但主要用于操作数据,执行逻辑一般在“控制类”中完成。领域驱动设计中,没有“控制类”和“边界类”的概念,执行逻辑在实体或者领域服务中实现。

  • 聚合:“聚合”在UML中表示对象间的一种关联关系,而在领域驱动设计中“聚合”表示若干有密切关系的实体和值对象,这些实体和值对象之间的关系可能是UML中的“聚合”关系,也可能是“组合”关系。

还有很多术语,这里不一一说明。正确理解领域驱动设计中术语的含义是用好领域驱动设计的前提。

  • 技术框架问题
    任何软件开发方法在实现时都离不开技术框架的支撑,领域驱动设计也不例外。一方面,如果缺乏技术框架的支撑,很多设计思想无法实现,可以想象一下如果没有依赖注入框架,如何实现软件分层架构;另一方面,技术框架的使用或多或少会引入框架依赖,而这种依赖可能会破坏原本应该遵守的设计原则。

  • 英语障碍
    英语障碍来源于两方面,一方面是很多术语是由英文翻译过来的,属于外来语,很多术语本身就让人费解,而有些障碍则是翻译过程中人为制造的,本书作者高度怀疑有些翻译者故作玄虚,把简单的词语搞得高深莫测。这个障碍不仅存在于领域驱动设计中,很多地方都可以遇到。上中学物理时,对“功”的概念感觉很高深,不好理解,等到后来发现是从“work”翻译过来的,就觉得这个概念其实不那么复杂。上大学时,有个所谓“鲁棒性”的概念,感觉很神秘,后来发现是从“robustness”音译过来的,就觉得为什么不译成“健壮性”,好像生怕别人一看就懂。领域驱动设计的翻译中倒是没有这些故弄玄虚的翻译,却存在很多不一致性,比如Ubiquitous Language,有翻译为统一语言的,有翻译为通用语言的,再比如Repository,有翻译为资源库的,有翻译为存储库的。这些术语的障碍只能通过标准化来解决,可是没有这方面的权威机构进行这种标准化工作,目前也只能以现状为基础,将现状作为事实标准。

英语障碍的另一方面是编程语言和交流语言的不一致。我们采用以英文作为基础的计算机语言编写代码,这至少在目前是无法改变的现状,而现代编程技术更强调代码的人类可读性,很多编程的技术都要求命名具有人类可读性,包括函数的名称、测试用例的名称、变量的命名等等,都要求描述性,易于人类理解。领域驱动设计中领域模型和通用语言也是基于这种理念提出的,这样确保代码与交流语言包括文档的一致性,而所有这一切所基于的基础是代码与文档使用同一种语言——英语,这对于使用以汉语进行交流以英语进行开发的我们来说,增加了一重挑战。我们不仅需要将业务语言变为计算机语言,还要在中文与英文之间频繁切换,对于英文差强人意的开发人员(比如本书作者)来说一直是挑战。不仅是领域驱动设计如此,很多建模方法都有类似的问题,这需要我们自己摸索和建立适合国情的方法。