实现领域驱动设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

前言

所有的计算都表明它不工作,唯一的做法是:使其工作。

——Pierre-Georges Latécoère 早期法国航空企业家

是的,我们将使其工作。然而,在软件开发过程中采用领域驱动设计却是困难的。即便是有能力的开发者,也很难找到实现领域驱动设计的正确方法。

起飞,着陆

在我小的时候,我的父亲学习过驾驶小型飞机。我们经常会全家出去飞行,有时会飞到另一个机场,在那里吃过午饭后再返回。当父亲时间有限而他依然想飞时,父亲便带上我一起在机场上空盘旋,起飞,着陆,再起飞,再着陆。

也会有些长途飞行,这时我们会带上一张由父亲先前绘制好的路线图。我们几个小孩便当起了领航员:将图上的标志对应着陆地上的地标,以确保我们没有跑偏航线。这是一件很有趣的事情,因为要识别远在地面上的物体是很有挑战性的。事实上,我敢肯定父亲根本不用我们领航便知道我们处于什么方位——他能看到仪表盘上的所有信息,并且他拥有仪表飞行执照。

空中的景观的确改变了我的视野。不时地,父亲和我会飞过我们乡下的房子。在几百英尺的高空中,我体会到了另一种“家”的概念,而这在之前是没有过的。当我们飞过自家的房子时,母亲和我的姐妹们便会跑到院子里向我们挥手。我知道那是她们,即便我看不清楚她们是谁。谈话肯定是不行的,连大声喊都不行,她们是听不见的。我还可以看到将我家和外面公路分开的护栏,平时我们会像走平衡木一样在护栏上面走来走去。从空中看,它们就像被细心编排过的小树枝一样。我们家的院子很大,每每到了夏天,我都会开着割草机一排一排地修理院子里的草坪。而在空中时,我只能看到一片绿色,小草的叶子肯定是看不清楚的。

我喜欢在空中的时刻,直到现在我还不时回想起这些时刻,好像那个降落飞机的黄昏就发生在不久以前一样。虽然如此,在地面上的感觉依然是无法取代的,因为它给我一种脚踏实地的感觉。

着陆于领域驱动设计

一开始接触领域驱动设计(DDD)就像一个小孩之于飞行一样。天空中的景色是令人惊叹的,但有时我们却因为过于陌生而搞不明白它们到底是什么。要从甲地到乙地显得如此的遥远。然而,DDD的“成年人”们却总知道他们所处的方位,因为他们在很早之前便绘制好了路线图,并且能够完全按照仪表进行相应的操作。而还有很多人找不到“在地面上”的感觉,此时我们需要的是“稳定着陆”的能力,然后找到一张地图给我们指引方向。

Eric Evans的《领域驱动设计:软件核心复杂性应对之道》是一本经得住时间考验的经典之作。我坚定地相信,在接下来的几十年里,本书依然会是开发者的实用指导。和其他模式一样,该书为我们建立起了一种高屋建瓴式的宽阔视野。然而,对于如何实现DDD,我们可能将面对更多的挑战。通常来说,我们更渴望看到一些具体的例子。

我的目标之一便是帮助你来一个“软着陆”,保全飞机,然后沿着一条周知的线路带你回家。这将帮助你如何更好地去实现DDD,并且通过你所熟悉的工具和技术给出示例演示。当然,任何一个人都不可能一直呆在家里,所以我还会带领你到新的地带去冒险,这些地带你可能从来没有去过。冒险之路是险峻的,但是在正确的战术应对下,征服这些困难是可能的。在这条冒险之路上,你将学到另外的架构和模式来集成多个领域模型。你将接触到先前没有被研究过的集成方法,并且学到如何开发自治性服务。

我将向你提供一张对短途旅行和长途旅行均适用的地图,它可以帮助你更好地享受沿途风景,同时又不至于迷失途中。

对照地形,绘制飞行图

在软件开发的过程中,我们经常做的一件事便是将一种东西映射到另一种东西。我们将对象映射到数据库,映射到用户界面,或者映射到不同的应用层展现(包括作为消费方的其他系统或应用程序)。在所有这些映射中,我们很自然地希望在Evans提出的高层模式和具体实现之间存在一种映射。

即便你已经接触过DDD,你依然有很多可以获益的地方。有时,DDD首先被看作是一套技术工具集,有人将此称为DDD-Lite。我们可能已经对实体、服务等DDD概念非常熟悉了,并且大胆地尝试着设计聚合,还通过资源库来管理持久化。这些模式是大家相对熟知的,使用起来很容易,我们甚至还使用了值对象。以上这些都属于战术设计模式范畴,也即更加偏向技术层面。这些模式可以很好地帮我们解决软件问题。而同时,对于战术性模式,我们依然有许多需要学习的。我将战术模式映射到实现层面。

你曾了解过战术建模之外的东西吗?你曾了解过被称为DDD“另一半”的战略设计模式吗?如果你还没有使用过限界上下文和上下文映射图,那么你很有可能也没有使用过通用语言。

如果说Evans在软件开发社区有一项发明,那便是通用语言。通用语言是一种团队协作模式,用于捕捉特定业务领域中的概念和术语。一个特定领域的软件模型通过不同的名词、形容词和动词来表达,这些词汇是开发团队正式使用的,而团队中应该包含一个或多个领域专家。然而,将通用语言仅限定于一些词汇则是错误的。就像自然语言反映人们的思想一样,DDD的通用语言反映了领域专家对于软件系统的思维模型。通用语言和那些战略和战术性的建模模式同等重要,在有些情况下甚至更具有持久性。

简单地讲,DDD-Lite将导致劣质的领域对象,因为通用语言、限界上下文和上下文映射图的作用太大了,你从其中获得的并不只是一套团队共用的语言。在限界上下文中用通用语言来表述一个领域模型可以增加业务价值,并且使我们确信所开发软件的正确性。即使从技术的角度,它也可以帮助我们创建更好的领域模型,这样的模型行为丰满,业务纯净,并且可以减少犯错误的可能性。因此,我将战略设计模式映射到了可理解的实际例子中。

本书对于DDD的映射可以帮助你同时体会到战略设计和战术设计的好处。通过一些具体的例子,你将感受到这些DDD映射的业务价值和技术展现力。

如果我们对于DDD的所有实践都只是停留在“地面上”,那将是令人失望的。过度地拘泥于细节将使我们丧失在空中俯瞰的机会。所以,不要将自己局限在地面的细节上,要勇敢地飞翔在空中,居高临下。搭上战略设计的航班,去了解限界上下文和上下文映射图,你将获得更广阔的视野。当你从DDD的航班中获益时,我的目的也就达到了。

各章概要

以下是各章的主要内容以及你将如何从中获益。

第1章:DDD入门

本章向你介绍DDD的好处,并且教你如何尽可能多地去实现DDD。你将学到当你在应对复杂的软件系统时,DDD可以为你的项目和团队带来什么。同时,你将了解到通常的DDD替代方案以及这些方案为什么会导致问题。作为对DDD的基础讲解,本章将教你如何在项目中开始采用DDD,还有如何向你的领域专家和技术团队推销DDD。在DDD的武装下,你将学会如何迎接挑战,勇往直前。

本章将介绍关于一个公司及其团队的案例研究,虽然该公司是虚构的,但是他们所面临的DDD挑战却是真实存在的。该公司旨在开发一个新的多租户SaaS(Software as a Service,软件即服务)软件产品。不出所料,在使用DDD时,他们犯了一些常见的错误。不过还好,他们发现了这些错误,并解决了一些问题,因此项目还算没有偏离正轨。该团队需要开发一套基于Scrum的项目管理软件。该案例还会在本书的后续章节中连续讲到。每一种战略和战术模式都将教给这个团队。在这个过程中,团队有误入歧途的时候,但最终他们将向着成功的DDD实践昂首阔步。

第2章:领域、子域和限界上下文

领域、子域和核心域分别是什么?限界上下文是什么,我们为什么要使用它,并且如何使用?这些问题将在这个SaaS项目团队犯错误的时候给予解答。在他们的第一个DDD项目中,他们并不了解子域、限界上下文和通用语言这些概念。事实上,他们根本不知道什么是战略设计,只是采用了战术设计来解决一些技术问题。这样他们在开始设计领域模型的时候便遇到了不少问题。幸运的是,他们及时地意识到了这些问题,项目还有挽回的余地。

本章还讲到了如何使用限界上下文对模型进行分离,这是非常重要的;同时还讲到了一些模型分离不当的反例,并且给出了有效的实现建议。在采用了这些建议之后,该团队的成员们重新创建了两个不同的限界上下文。这种合理的模型分离带来的好处是引出了第三个限界上下文——核心域,这将是本书使用的主要例子。

对于那些苦于单单从技术层面应用DDD的人来说,本章应该能引起你的共鸣。如果你还是DDD战略设计的外行,那么本章将为你指明方向。

第3章:上下文映射图

上下文映射图帮助我们理解业务领域、模型间的边界,以及这些模型之间的集成方式。

上下文映射图绝对不只是绘制系统架构图这么简单,它处理的是不同限界上下文之间的关系,以及如何在不同的模型之间映射对象。对于在复杂的业务系统中使用好限界上下文,这是至关重要的。在第2章中,团队成员们在首次尝试限界上下文时碰到了问题。本章中,他们将学着如何利用上下文映射图来解决这些问题。这样的结果是产生了两个体面的限界上下文,这两个上下文将被另外一个负责核心域的团队所使用。

第4章:架构

我们都知道分层架构,但它是开发DDD软件的唯一方式吗,也或许还存在另外的方式?在本章中,我们将讲到:六边形架构(端口和适配器)、面向服务架构、REST、CQRS、事件驱动(管道和过滤器,长时处理过程,事件源)和数据网格,其中好几种架构都将被该团队成员所采用。

第5章:实体

在DDD的战术模式中,我们将首先讲到实体。团队成员们一开始过于强调实体的作用而忽视了值对象。受到数据库和持久化框架的影响,实体被该团队滥用了,此时他们开始讨论如何避免大范围地使用实体。

在本章中,你将看到很多优秀的实体设计例子。同时,本章还将讲到如何使用实体来表达通用语言,以及如何对实体进行测试、实现和持久化。

第6章:值对象

早些时候,团队成员们错过了采用值对象的好机会。他们过于注重为实体创建一些单一的属性,这种方式是欠妥的,更好的方式是将这些单一的属性聚合成一个不变的整体。本章将从不同的角度讲解如何设计值对象,以及在什么时候采用值对象会优于实体。同时,本章还包含了一些其他话题,比如值对象在集成中的角色和对标准类型的建模等。然后,本章讲到了如何设计以领域为中心的测试,如何实现值对象。此外,本章还讲到了在聚合中存储值对象时,如何避免持久化机制所带来的不利影响。

第7章:领域服务

本章将讲到,在领域模型中,什么时候应该将一个概念建模成粒度适中,并且无状态的领域服务。你将学到何时应该使用领域服务而不是实体或值对象,以及如何使用领域服务来处理业务逻辑和技术上的集成。团队成员们向我们展示了何时应该使用领域服务,以及如何设计领域服务。

第8章:领域事件

Eric Evans并没有在他的书中正式介绍领域事件,领域事件是在他那本书出版之后才进入人们视野的。在本章中,你将学到为什么领域事件如此有用,以及使用领域事件的不同方法。领域事件甚至被用来辅助集成和自治性服务。在软件系统中,我们经常使用一些技术层面的事件机制,但本章将着重讲解领域事件与这些事件机制的区别。本章还将指导你如何设计并实现领域事件,包括一些可行的方案和对这些方案的权衡选择。然后,本章将讲到如何创建一个发布-订阅机制;如何利用事件来集成整个企业软件中的各个订阅方;如何创建和管理事件存储;如何处理消息机制所面临的常见挑战等。

第9章:模块

对于模型中的对象,我们应该如何将他们组织在大小适中的容器中呢?我们又如何保证不同容器中的对象之间只存在有限的耦合?另外,我们如何对这些容器进行命名以体现通用语言?除了包和命名空间之外,我们如何使用由语言和框架提供的现代模块化机制,比如OSGi和Jigsaw?在本章中,你将看到SaaS团队成员是如何在不同的项目中使用模块的。

第10章:聚合

在DDD的战术模式中,聚合可能是最不容易理解的了。然而,在遵循一定的经验法则的情况下,我们是能够更简单、更快地实现聚合的。在本章中你将学到:如何利用聚合在不同的小规模对象集群间创建一致性边界,从而降低模型的复杂性。由于在细枝末节上花了太多精力,SaaS团队成员们在设计聚合时总是磕磕绊绊。我们将仔细研究该团队所面临的挑战,并且分析错误的原因以及他们的应对策略。结果,团队成员们对他们的核心域有了更深层次的理解。我们将看到,在合理的事务处理和保证最终一致性(Eventual Consistency)的前提下,该团队更正了他们所犯的错误,并且在一个分布式环境中设计出了更具有伸缩性和更高效的模型。

第11章:工厂

工厂已经在[Gamma et al.]中被大量地谈及了,为什么还要讲呢?本章并不打算重蹈覆辙,而是将重点放在“工厂应该存在于何处”这个问题上。在本章中,我们将讲到在DDD中实现工厂的技巧。团队成员在他们的核心域中创建的工厂可以简化客户端接口,并且对模型的消费方起到保护作用,从而避免了在多租户环境中引入灾难性的bug。

第12章:资源库

资源库只是一个数据访问对象(Data Access Object,DAO)吗?如果不是,它们之间有什么区别呢?我们为什么应该将资源库看成是对集合的模拟而非数据库呢?在本章中,我们将讲到如何利用ORM来实现资源库,其中有两种ORM方案,一种采用基于网格的分布式缓存,另一种则采用NoSQL的键值对存储。团队成员们可以采用任何一种作为他们的持久化机制。

第13章:集成限界上下文

到现在为止,你已经了解了战略层次的上下文映射图和多种战术层次的模式。本章将讲到,在DDD中,我们如何通过上下文映射图来集成不同的模型。在团队对核心域和其他辅助性的限界上下文进行集成时,我们将给出相应的建议和指导。

第14章:应用程序

对于每一个核心域的通用语言,我们都设计了相应的模型,并且进行了足够的测试,模型工作正常。然而,客户应该如何使用我们的模型呢?他们应该使用DTO将数据在模型和用户界面之间传输吗?或者存在其他方案可以实现模型和展现组件间的数据传递?DDD中的应用服务和基础设施是如何工作的?对于这些问题,本章都将做出解答。

附录A:聚合与事件源:A+ES

事件源是一种持久化聚合的重要技术,同时也是事件驱动架构的基础。事件源通过一系列的事件来表示聚合的所有状态。通过有序的事件重放,我们可以重新构建聚合的状态。当然,使用事件源的前提是:它能够简化对数据的持久化,并且能够捕捉到那些具有复杂行为属性的概念。

Java和开发工具

本书中的绝大多数例子都是使用Java语言编写的。我本来可以用C#的,但是我有意识地使用了Java。

首先,我认为Java社区正在抛弃好的软件设计和开发实践。现在,对于多数Java项目而言,要在其中找到一个好的领域对象恐怕是困难的。在我看来,Scrum和敏捷被人们看成了优良设计的替代品,而其中的产品待定项(Product Backlog)被看成了设计本身。多数敏捷人士并不会过多地去思考这些待定项是否会影响到业务模型。我得说明,Scrum的本意绝对不是要取代设计。不管有多少项目经理想将你捆绑在持续交付这条路上,我得说Scrum并不仅仅是要取悦于那些甘特图(Gantt chart)的追随者们。然而,太多的时候,情况的确是这样的。

我认为这是个很大的问题,所以我想鼓励Java社区重新回到领域建模中来,同时我会通过本书向大家说明,设计是可以使我们获益的。

此外,在.NET社区中已经有很好的DDD资源了,比如Jimmy Nilsson的《领域驱动设计与模式实战》[Nilsson]。由于Jimmy的出色工作和其他人对Alt.NET的倡导,.NET社区中正掀起一阵优秀设计的开发浪潮,这是Java社区需要注意的。

其次,我意识到C#.NET人员在理解Java代码上并不存在什么困难。由于很多DDD社区的人都在使用C#.NET,而本书的早期校对人员也都是C#程序员,但是我从来就没有收到他们的抱怨。因此,我便不用顾虑这些了。

在我写这本书时,业内正将目光从关系型数据库转向基于文档和键值对的存储方案。这是有原因的,Martin Fowler将这些存储方案称为“面向聚合存储”。这种命名是恰当的,它很好地描述了在DDD中使用NoSQL的好处。

但是,就我从事咨询的经验来看,很多开发者还是认定了关系型数据库和对象-关系映射。因此我想,NoSQL追随者们应该能够理解我在书中包含对象-关系映射的章节。然而,我的确得承认,这可能会招致那些认为存在对象-关系阻抗失配(Object-Relational Impedance)的人的鄙视。这无所谓,对此我表示接受,因为绝大多数人在他们的日常工作中都还得面对这种对象-关系阻抗失配。

当然,在第12章“资源库”中,我同样提供了基于文档的、键值对的和数据网格的存储方案。在多处地方,我都讨论到了NoSQL对聚合设计的影响。NoSQL趋势很有可能持续下去,那些对象-关系型的开发者们应该注意了。在本书中你将看到,我能够同时理解两个阵营的观点,并且对于双方的观点我都同意。这些都是技术趋势所导致的摩擦,而这对于积极的变革是有必要的。