子域(Subdomain)和限界上下文(Bounded Context,BC)是领域驱动设计(Domain-Driven Design,DDD) 中战略设计阶段的重要概念,但很多 DDD 学员却对这两个概念及其关系理解的不够透彻,难以有效应用在设计实践中。本文以代码打靶业务为例,尝试说透子域和限界上下文的概念及其关系。
假设我们通过事件风暴建立了代码打靶业务的统一语言,其中领域名词及其说明如下:
领域名词 | 说明 |
靶子 | 一段或几段存在缺陷的代码,可以有多个源码文件,一般200行左右,最多不超过500行 |
靶环 | 一条正确的缺陷记录 |
靶标 | 所有靶环的汇总,即靶子的所有靶环 |
语言 | 通用编程语言或 DSL,每类文档也可以对应一种自定义语言,一个靶子仅涉及一种语言 |
靶场 | 用户打靶时进入的场所,一个靶场可以选择多个靶子,一个靶子也可以被多个靶场选择 |
打靶记录 | 用户打靶时提交评审缺陷的最小单位,与靶环对应 |
答卷 | 用户将当前靶子打完后提交,形成一个答卷,包含一条或多条打靶记录 |
规范 | 缺陷分类标准 |
工作空间 | 隔离不同的规范,一个工作空间只能有一个规范 |
版本 | 一个规范可以有多个版本,可以同时生效,一个靶子对应一个规范的某个版本 |
靶子成绩 | 命中的靶环数和获得的总分 |
靶场成绩 | 靶场内多个靶子成绩的汇总 |
证书 | 提供 HTTPS 加密协议访问 |
用户 | 使用系统的人员 |
权限 | 对资源的访问、使用和操作能力控制的规则 |
角色 | 权限的集合 |
组织 | 企业经营活动过程中形成的一种管理结构 |
评价 | 对用户打靶能力的评估,比如用户画像,成长曲线 |
推荐 | 根据用户的能力弱项推送相关知识,这里将动词名词化了 |
个人信息 | 个人资料 |
我的靶场 | 我打过的靶场 |
我的靶子 | 我上传的靶子 |
个人看板 | 个人维度的度量标 |
组织看板 | 组织维度的度量指标 |
产品介绍 | 产品说明 |
帮助文档 | 用户手册 |
版本特性 | 每个版本的功能列表 |
关于我们 | 主创团队介绍 |
子域
领域建模首先是一个定义问题的方法,其次才是解决问题的方法。就是说,一方面,我们要知道解决的问题是什么;另一方面,我们要知道怎么去解决问题。
我们要解决的问题就是领域问题,在 DDD 中,通过子域模式先把问题从大面上进行分解。软件开发是解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到 DDD 中,就是要把一个大领域分解成若干个小领域,而这个分解出来的小领域就是子域。
现在我们的大领域是代码打靶领域,如下所示:
big-domain.png
分离子域
业务的目标是打靶。首先,我们把打靶相关的名词找出来,包括靶场、靶子、语言、靶标、靶环、答卷、打靶记录、靶子成绩和靶场成绩;其次,我们日常评审流程也可以通过打靶的方式来完成,这时领域名词并没有增加。我们将相关的领域名词分离出来,并命名为打靶子域。
hit.png
代码打靶业务的铁三角是规范、内容和工具,而在工具中要管理规范和内容的生命周期。接着,我们把规范管理和内容管理这两件事分离出来:
规范管理主要包括工作空间、规范和版本。每个规范文件承载了多个语言的规范,所以规范管理中也有语言的概念。考虑到之前在打靶子域中已经有语言了,这里我们将语言记为语言2;
内容管理中也有靶场、靶子、语言、靶标和靶环,同时在线建设靶标时也有打靶记录和答卷。同理,我们将这些领域名词记为靶场2、靶子2、语言3、靶标2、打靶记录2和答卷2。
manager.png
对于用户,访问工具(系统)时都按照HTTPS协议访问,然后登陆。用户登录后,根据权限管理配置,大多数用户是打靶人员,而一小部分用户是管理员。对于打靶人员,因所属的组织不同会看到不同的靶场。这里涉及的领域名词包括用户、证书、权限、角色和组织。我们将相关的领域名词分离出来,并命名为安全子域。
每个用户都有个人信息,一般从配置服务统一获取,然后再让用户根据需要定制修改。打靶人员进入靶场完成打靶后,后续可以统一查看曾经打过的靶场。管理员上传靶子后,由于规范的升级,可能导致一些靶标涉及已废弃的规范条目,这时可以在自己已上传的靶子中统一核查这种情况,从而找到目标靶子,并对其靶标进行刷新。这里涉及的领域名词包括个人信息、我的靶场和我的靶子。我们将相关的领域名词分离出来,并命名为个人中心子域。
对于工具来说,需要向用户提供度量看板。这里涉及的领域名词包括个人看板和组织看板。我们将相关的领域名词分离出来,并命名为看板子域。
对于打靶人员,打靶不是目的,代码评审能力和编码能力提升才是。如何让打靶人员快速感知自己的提升?如何让打靶人员低成本知道自己的弱项?如何让打靶人员快速提升代码评审能力和编码能力?这是工具的重要扩展方向,涉及的领域名词包括评价和推荐。我们将相关的领域名词分离出来,并命名为智能学习子域。
最后,还剩下一组领域名词,包括产品介绍、帮助文档、版本特性和关于我们。我们将相关的领域名词分离出来,并命名为运营子域。
综上,我们看一下分离子域的全景图:
subdomain.png
区分子域
对于代码打靶领域而言,划分出来的子域有 8 个,但并非每个子域都一样重要。所以,我们还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。
核心域是整个系统最重要的部分,是整个业务得以成功的关键。关于核心域,Eric 曾提出过几个问题,帮我们识别核心域:
为什么这个系统值得写?
为什么不直接买一个?
为什么不外包?
如果你对这几个问题的回答能够帮你找到这个系统非写不可的理由,那它就是你的核心域。对于代码打靶领域来说,打靶子域、规范管理子域和内容管理子域都是核心域。
有一些子域不是你的核心竞争力,但却是系统不得不做的东西,市场上也找不到一个现成的方案,这种子域就是支撑域。对于代码打靶领域来说,智能学习子域、个人中心子域是和运营子域都是支撑域。
还有一种子域叫通用域,就是行业里通常都是这么做,即便不自己做,也并不影响你的业务运行。对于代码打靶领域来说,安全子域和看板子域都是通用域。
我们之所以要区分不同的子域,关键的原因就在于,我们可以决定不同的投资策略。核心域要全力投入,支撑域次之,通用域甚至可以花钱买服务。
对于代码打靶领域的 8 个子域,区分核心域、支撑域和通用域后,子域全景图变为:
image.png
限界上下文
通过分离子域,区分核心域、支撑域和通用域,我们将 DDD 在问题域的概念已经说清楚了。接下来,就要进入到解决方案域了。我们现在有了 8 个子域,如何将这些子域落实到代码上呢?首先要解决的就是这些子域如何组织的问题,是写一个程序将所有子域都放在里面,还是将每个子域分别放在一个独立的应用,亦或是有些放在一起,有些分开。这就引出了另一个重要的概念,限界上下文。
限界上下文,顾名思义,它形成了一个边界,一个限定了统一语言自由使用的边界,一旦出界,含义便无法保证。每个限界上下文有一个领域模型,保证了概念的严格一致性,而限界上下文之间则没有必要一致。这就是说,全局概念一致性从根本上是不可能的,我们不再追求全局一致性,而是退而求其次,仅追求局部的一致性,使概念不一致的问题得到合理管控,从而实现业务目标。
有了对限界上下文的理解,我们就可以将代码打靶业务拆分到不同的限界上下文中,但从哪里开始比较好呢?或许你也想到了,那就是子域。
我们先将每个子域对应一个候选限界上下文,然后再根据业务边界、工作边界和技术边界来拆分或合并不满足约束的候选限界上下文,形成真正的限界上下文。
代码打靶领域有 8 个子域,分别是打靶子域、规范管理子域、内容管理子域、安全子域、个人中心子域、看板子域、智能学习子域和运营子域。根据每个子域对应一个候选限界上下文,那么我们就得到 8 个候选限界上下文,分别是打靶上下文、规范管理上下文、内容管理上下文、安全上下文、个人中心上下文、看板上下文、智能学习上下文和运营上下文。
我们简单介绍一下业务边界、工作边界和技术边界:
类型 | 说明 |
业务边界 | 对领域模型的控制,维护了模型的完整性与一致性,从而降低系统的业务复杂度 |
工作边界 | 对团队协作的控制,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度 |
技术边界 | 对技术风险的控制,做出对系统质量属性的响应与承诺,功能复用,管理变化,确定服务之间的集成方式,从而降低系统的技术复杂度 |
想深入了解业务边界、工作边界和技术边界的读者,可以阅读笔者的另一篇文章《聊聊服务化架构的边界》。
经过对业务边界、工作边界和技术边界的分析,打靶候选上下文和安全候选上下文需要拆分,规范管理候选上下文和内容管理候选上下文需要合并,而其他候选上下文保持不变:
打靶候选上下文涉及两个业务:一个是根据管理员建设的靶场进行打靶,提升代码评审能力;另一个是以打靶的方式完成日常代码评审任务,体现提升的代码评审能力产生的价值。虽然两个业务都是打靶,但是概念却不完全一致:一个靶场是管理员建设的,但另一个靶场是 Git 评审单;一个靶子是管理员建设的,有靶标,提交打卷后需要阅卷,但另一个靶子是同事新写的代码,没有靶标,不需要阅卷。因此,打靶候选限界上下文中概念存在不一致性,违反了业务边界的约束,需要拆分成两个独立的限界上下文,不妨称作打靶上下文和日常评审上下文。
安全候选上下文包括证书访问、用户登陆和分权分域等功能,但证书访问功能比较特殊,应放在系统的网关,以便用户通过 HTTPS 协议安全的访问系统,而系统内的交互则可以通过 HTTP 协议来完成。因此,根据技术边界的约束,我们将安全候选上下文拆分为反向代理上下文和访问控制上下文。
内容管理候选上下文包括靶场管理和靶子管理,其中靶子管理依赖规范管理候选上下文,因为靶子在创建时要选择规范所在的工作空间和规范的版本,并且靶子对应的语言也要从规范支持的语言列表中选择。规范管理候选上下文相对来说比较单薄,又与内容管理候选上下文同属于管理面,在满足工作边界约束的前提下应尽可能粗粒度,可以考虑将这两个候选限界上下文合并。在这个限界上下文中,靶场组合靶子,靶子依赖规范,靶场就成了更重要的概念,我们就将合并后的限界上下文称作靶场管理上下文。
综上,我们的拆分限界上下文一共有 9 个:
打靶上下文
日常评审上下文
靶场管理上下文
反向代理上下文
访问控制上下文
智能学习上下文
个人中心上下文
看板上下文
运营上下文
对于每一个限界上下文,并不意味着一定要独立部署成一个微服务,要根据成本收益等因素来权衡。
两者关系
问题域和解决方案域有映射关系,那么子域和限界上下文的映射关系是什么?
代码打靶业务子域和限界上下文及其关系整理如下:
子域 | 限界上下文 | 关系 |
打靶子域 | 打靶上下文 日常评审上下文 |
一对多 |
规范管理子域 内容管理子域 |
靶场管理上下文 | 多对一 |
安全子域 | 反向代理上下文 访问控制上下文 |
一对多 |
智能学习子域 | 智能学习上下文 | 一对一 |
个人中心子域 | 个人中心上下文 | 一对一 |
看板子域 | 看板上下文 | 一对一 |
运营子域 | 运营上下文 | 一对一 |
可以看出,子域和限界上下文的关系有三种:
一对一的映射关系,即一个子域对应一个限界上下文(这是最简单的关系);
一对多的映射关系,即一个子域可以拆分为多个限界上下文(这里用了拆分两字,意味着这个子域的任何一个限界上下文都不能再包含其他子域);
多对一的映射关系,即多个子域可以被一个限界上下文包含(这里用了包含两字,意味着这个限界上下文的任何一个子域不能再被包含在其他限界上下文中)。
小结
本文结合代码打靶领域 DDD 战略设计实践案例,说透了子域和限界上下文的概念,并彻底理清了它们之间的关系:
什么是子域?首先,我们根据关注点分离将统一语言中识别出来的领域名词进行分组,每组的关注点就是一个子域,它分解了问题域的复杂度;其次,对于一个领域而言,划分出来的子域可能有多个,还要将这些子域再做一下区分,分成核心域、支撑域和通用域(对于不同的子域,我们有不同的投资策略)。
什么是限界上下文?首先,限界上下文限定了统一语言自由使用的边界,每个限界上下文有一个领域模型,保证了概念的严格一致性,而限界上下文之间则没有必要一致,它分解了解决方案域的复杂度;其次, 对于限界上下文的拆分,子域是一个很好的起点,我们先将每个子域对应一个候选限界上下文,然后再根据业务边界、工作边界和技术边界来拆分或合并不满足约束的候选限界上下文,进而形成真正的限界上下文。
问题域和解决方案域有映射关系,那么子域和限界上下文的映射关系是什么?首先,是一对一的映射关系,即一个子域对应一个限界上下文(这是最简单的关系);其次,是一对多的映射关系,即一个子域可以拆分为多个限界上下文(这里用了拆分两字,意味着这个子域的任何一个限界上下文都不能再包含其他子域);再次,是多对一的映射关系,即多个子域可以被一个限界上下文包含(这里用了包含两字,意味着这个限界上下文的任何一个子域不能再被包含在其他限界上下文中)。