领域和子域

前面的章节已经介绍了领域驱动设计的作用和特点,接下来为大家介绍领域驱动设计中的相关核心概念。

首先,需要理解什么是领域。领域(Domain)在数学、计算机学和生物学有不同的解释和应用,在软件中常用domain来表示域名。例如,HTML中document.domain可以获取当前页面的域名,Cookie中的domain属性用来标识Cookie的域名。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(1)

从字面意思上理解,领域是指某一专业或事物方面所涵盖的范围,或者表示某个专属的领地,总之是一个用来划定范围大小的东西。在DDD中,我们做设计时面对的是一个组织所在的领域,即该组织做的所有事情以及其中所包括的一切,当为某个组织开发软件时,面对的是这个组织的领域。例如,我们要开发一个商城系统,就要涉及商城系统领域,一个完整的商城系统领域是一个庞大的领域,涵盖了很多小领域,如产品检索、库存、订单、发票、物流、促销等子系统,在领域驱动设计中把它们称为子域,每个组织都有着自己独特的业务范围和做事方式,越是庞大的组织所涵盖的业务越复杂,所在领域也越难以清晰地定义,所以要弄清楚所在的领域和子域在DDD过程中的目标,通过各种手段不断明晰领域的过程,用领域去驱动设计。

通过之前商城的例子,我们可以了解到在一个业务的领域中,可以根据不同的上下文将领域划分为不同的子域,通常子域中也可以分为不同的类型,用于主要业务的领域称为核心域。从战略层面上讲,核心领域是企业的核心竞争力,拥有最高的优先级,投入最多的资源,这便是核心域。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(2)

子域中除了有核心域,还会出现一些其他的领域来支撑我们的部分业务,这些领域不负责主要的业务活动,但主要的业务实现也需要依赖这些领域的支撑,我们称为支撑子域,支撑子域往往专注于业务的某一个方面。如果某个子域不是核心子域,也并不只涉及某个特定的方面,而是被用于整个系统,对于这一类的子域,就称为通用子域。通常我们会把关注点放在核心领域上,但支撑子域和通用领域在领域模型中同样重要,少了它们,核心领域就不能独立支撑起整个业务。所以,在设计上对核心领域投入更多的精力,如采用深层建模、严格统一语言,对于支撑子域和通用子域的要求会相对低一些。

领域事件

在学习和实施领域驱动设计的过程中,领域事件是一个重要的概念和工具。事件字面意思是比较重大、对一定的人或事物会产生一定影响的事情,在物理学中,事件是由它的时间和空间所指定的时空中的一点。总体来说,事件就是表示会对事物产生一定的影响、变化或痕迹的事情,领域事件就是表示发生在领域中的事件。

领域事件的定义

不同的团队对于领域事件的定义不同,但作为一个建模工具,最好在开始就定义清楚规则。我们将能改变领域状态的事件称为领域事件,如一个项目管理系统,对系统中的项目进行查询,这确实是一件事情,但不是领域事件,因为查询并不会改变项目的状态,项目也不会因为被查询而发生变化,而添加一个项目信息,就会导致项目的数量和信息发生变化,项目添加就是一个领域事件。

领域事件有什么用处?通过对事件的分析,我们能更容易地发现具有相同特征的事件,如添加或删除项目信息,都是对项目这个对象进行操作,将项目构建成一个领域模型,这个领域模型拥有自己的资源仓库,可以进行数据的存储,同时设计领域对应的应用服务,用来被其他的客户端适配器调用,从而触发领域事件。关于资源仓库、应用服务和适配器等关系会在后面详细介绍。所以,领域模型用来发布领域事件,通过对领域中所有的事件进行发掘和分析,我们将领域中的模型逐步设计出来。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(3)

用领域事件来捕获领域中发生的事情,帮助我们更加了解业务的细节。当了解到领域事件和业务细节后,能更准确地设计出领域模型,梳理清楚模型的关系和边界。

事件风暴什么是风暴?在实施软件项目的过程中,我们经常使用头脑风暴的方法来集思广益,通过团队的力量创造出满意的解决方案。头脑风暴又称为脑力激荡法,是一种为激发创造力、强化思考力而设计出来的方法。一场风暴中通常可以由一个人或一组人进行。参与者围在一起,随意将脑中与研讨主题有关的见解提出来,再将大家的见解重新分类整理。在整个过程中,无论提出的意见多么可笑、荒谬,其他人都不得打断和批评,从而产生很多的新观点和问题的解决方法。

事件风暴就是采用头脑风暴的方式,用来发现领域中的全部事件的一项活动。虽然我们可以在一开始就定义出领域事件的规则,但在一场风暴中,不同的人对于不同的定义有不同的理解,所以除了提前定义规则,还应该以共用的方式达成一致的行动,使识别的领域事件更加一致。

一般采用什么方式来识别或发现领域事件?还是回到项目管理系统的例子中,之前所说添加一个项目信息是一个领域事件,我们来分析一下,添加一个项目信息这个领域事件中有哪些是领域事件共同的特征。首先,每个领域事件都会由一个动作来触发,可以称它为指令(Command),然后每个指令都会有触发指令的人,称它为执行者(Actor),通过分析执行者和指令,可以发现一些事件(Event),如图7.6所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(4)

通常,我们会使用一些物理卡片将指令、执行者和事件都贴到墙上,方便随时添加、修改和挪动卡片,事件风暴物理墙如图7.7所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(5)

领域事件并不是事件风暴最终的输出,把卡片贴在墙上的好处是可以任意挪动,这样我们就可以将类似的事件挪到一起,并且分析这些事件是否都在同一个领域或类似同一个领域上发生的。例如,创建一个用户、修改用户的信息、删除用户都是对用户的领域事件,我们就可以将这些领域事件合并,建立一个集合多个指令和领域事件的领域模型,在DDD中被称为聚合,如图7.8所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(6)

用户旅程与事件风暴

关于领域事件还有一个重要的活动,就是识别出用户旅程(UserJourney),在大多数项目中,我们会有专门的产品经理或业务分析师与客户进行沟通,通常是现场调研,然后得出用户旅程,如图7.9所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(7)

用户旅程就像用户的体验地图,能够清楚并有逻辑地将系统的业务范围表达出来,我们可以通过用户旅程清晰了解用户的使用流程。

这与领域事件又有什么关系?在第7.4.2节中我们了解到事件风暴的方式和作用,在识别领域事件和聚合的过程中,大量使用执行者和指令用来识别和标识领域事件,但如何准确和快速地找出领域事件的执行者和指令?这就需要用到用户旅程,在得到准确和完整的用户旅程后,顺着用户地图找出执行者和指令,知道哪些用户完成哪些操作及调用哪些指令,从而梳理出领域事件,建立出完善的模型。邀请领域专家和技术人员参与讨论在用户旅程中尽早发现隐式业务概念,使我们的用户旅程贴近业务价值、范围明确,降低设计和技术的风险,提高建模的准确度。

聚合和聚合根

从7.4节中我们知道,聚合实际上也是一种领域模型,而且聚合集合了多个领域事件,并且可以被多种指令调用。在清楚聚合的用法前,我们先来了解实体和值对象。

什么是实体?首先我们要明白聚合是一种领域模型,而模型是对业务概念的一种抽象定义,那什么又是抽象?例如,一个Java的类就是对一种类型对象的一种抽象定义,而实体就是对抽象的类的实例,而且实体的核心是拥有唯一的标识,而不是对象的属性。又如,程序员这个抽象的实体可以是程序员小王、程序员小张和程序员小明,小王、小张和小明都带眼睛,都是Java程序员,而且都28岁。以上列举的实体的属性都相同,却是完全不同的3个人,因为他们都有自己唯一的标识,如长相、指纹、身份证号码,要判断是否为同一个人,就必须满足唯一的标识相同才行。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(8)

相反,没有唯一标识的模型的实例就是值对象,如程序员的衣服,衣服的属性有面料、颜色、样式、大小、品牌等,如果衣服的这些属性都相同,那么我们可以认为它们是同一件衣服,衣服就是值对象。实体和值对象的本质区别是实体拥有唯一标识,而值对象没有,而且实体往往是有状态的、有存活周期的对象。我们常用的值对象往往是数字、文本或时间等包含简单属性的对象。虽然值对象没有唯一标识,但并不意味着它不重要;相反,我们在做模型设计时应该更多考虑使用值对象,因为值对象更加简单,而且没有特殊的状态和判定逻辑,容易对值对象进行创建、使用、模拟和测试,维护成本也低于实体。而聚合就是由值对象和实体所组成的,三者的关系如图7.10所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(9)

一个聚合中有自己的唯一标识,如ID,在领域驱动设计中,为了使模型更加具有通用性,我们一般将聚合的ID设计为一个值对象,而聚合中通常还会有很多实体的存在。例如,一个项目信息作为一个聚合,那么项目的ID是值对象,项目的负责人是一个实体,项目的所在部门也是一个实体,如果转换为代码,内容如下。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(10)

在一个领域模型中会有多个聚合,如上面代码的例子,部门(Department)和项目负责人(Principal)也可以看作一个小型的聚合,通过项目(Project)的唯一标识进行关联,这个Project就是一个聚合根,一个领域模型中只会有一个聚合根。

聚合根和聚合密不可分,可以把聚合根理解成聚合的根节点,想要存取聚合必须要经过聚合根。例如,我们要买车,付款去提车,不可能今天提一个车轮,明天提一个车窗,肯定是一次性提一部整车;车脏了需要洗车,我们肯定是将整车送去洗,而不可能先送个车门,再送个车灯,车就可以看作一个聚合根,而车门、发动机、车玻璃、车轮等零部件都是聚合。

因此,聚合根可以说比聚合的范围更大,聚合可以是聚合根的一部分,而且是不可分离的一部分,一个领域模型中可以有多个聚合,但只会有一个聚合根,聚合根就是这个领域模型的建模核心产物,只有聚合根会拥有领域模型的适配器和应用服务。

限界上下文

限界上下文用来表示上下文的界限,在领域驱动设计中通常用于划分不同的子域,是一种对于领域的显示边界。换句话说,领域模型就被划分在不同的限界上下文中,通常限界上下文会被当作拆分微服务的重要依据。

笔者之前参与的项目管理平台的项目中,核心子域有项目健康状态监控,支撑子域有项目信息管理,通用子域有组织机构管理,子域与限界上下文划分如图7.11所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(11)

什么是领域驱动设计(领域驱动设计中的相关核心概念)(12)

一般我们会考虑的基本原则是关联性,如果两个聚合的关系密切,就不适合分开。如何分辨两个聚合之间是否有关联?通常,如果一个聚合发生变化会影响另一个聚合的状态,就可以认为两者之间密不可分,或者两个聚合之间为了支撑某一业务常有交互,也可以将这两个聚合放在一个限界上下文中。

限界上下文也有名称,最好是语义化的,如项目监控上下文、组织机构上下文和项目信息管理上下文,不同上下文可能模型的名称相同,但意义却完全不同。在DDD中有个比较有名的例子,在商城系统中有多种订单的聚合,同样是订单,但在不同的限界上下文中有不同的意义。例如,在商品上下文中,订单更多关注的是商品的信息,如价格;在库存管理上下文中,订单更多关注的是商品的购买数量、库存的数量等;在物流系统中,订单更多关注的是商品的物流状态、用户地址等属性,如图7.12所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(13)

通常,我们将一个上下文划分为一个微服务,在一些特殊情况下也会存在一个微服务中会有两个上下文的情况。由于领域驱动设计演进式的特性,一般在项目中我们不会先将不同的上下文物理地拆分开来,而是可以采用分模块或分包的方式先在同一个工程中开发,等到模型足够清晰时再将不同的上下文拆分成不同的微服务。

六边形架构

介绍了这么多概念,在实施DDD时,我们应该如何构建代码结构呢?在传统项目中会使用三层架构的形式(控制层、视图层、模型层)来构建软件,其示意图如图7.13所示。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(14)

在领域驱动设计中,同样也有更加适合自己的代码结构层次,那就是六边形架构。图7.14所示为笔者参与的一个项目的代码结构示意图。

什么是领域驱动设计(领域驱动设计中的相关核心概念)(15)

在六边形架构中,很明显地可以看出其核心是我们的领域模型,中间一层是应用服务层,最外层是适配层。领域模型如聚合或聚合根,应用服务层则是领域的边界,对外提供服务,外界的任何执行者要得到或操作领域模型都需要通过应用服务层,这比较好理解,六边形架构中比较特别的是适配层(Adapter),适配层是领域与外界交流的唯一途径,什么是外界?在六边形架构中,把所有外部的东西都视为外界。例如,模型最终会存储的数据库、缓存、消息队列,是一个第三方的系统或另一个微服务。再如,模型最终对外提供的RESTful接口、Web Service等,都可以称为外界。而六边形架构的“六”是个虚数,表示所有与外部的交互边界,可以是三边,也可以是十边,所有的数据都会在适配层进行交换,也就是适配,适配成可以和应用服务交互的数据。

如果从一个用户的查询接口调用,一直到数据库的SQL执行,采用六边形架构的执行流程如图7.15所示。

这样做的好处是什么?分析发现,这样做我们的领域模型更加纯洁和通用,纯洁在于无论外部的数据或模型的设计是什么,核心的领域模型都不会受影响,保持自己的设计,遵循自己的业务价值;通用在于无论外部的系统使用的是什么技术、什么组件。例如,存储一个用户信息,可以是关系型数据库,也可以是NoSQL,还可以是第三方的CRM系统,甚至可以是一个文件,核心的领域模型都不关心,完全由适配层进行隔离和解耦,模型本身不会受到任何约束,更加通用。

六边形架构是领域驱动设计中最好理解的部分,如果不知道DDD从哪里入手的读者可以先试着将三层架构的代码结构改造成六边形架构,然后逐步引入事件风暴、深度建模等过程。

以上是关于领域驱动设计的作用和好处,与微服务一样,领域驱动设计也面临着很多的挑战:学习曲线高、领域专家持续参与、思维方式的转变、投入更多的时间和精力。

本文给大家讲解的内容是领域驱动设计中的相关核心概念——领域和子域
  1. 下篇文章给大家讲解的是DDD的挑战;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
,