张逸:限界上下文的边界

边界通过限界上下文来确定,这在领域驱动设计中具有非凡的意义。对应于通用语言,限界上下文是语言的边界,对于领域模型,限界上下文是模型的边界,二者对应于问题空间(Problem Space)的界定。对于系统的架构,限界上下文还确定了应用边界和技术边界,进而帮助我们确定整个系统及各个限界上下文的解决方案。可以说,限界上下文是连接问题空间与解决方案空间的重要桥梁。

那么,限界上下文所界定的边界,究竟是逻辑边界,还是物理边界?这并没有定论,需得依据不同场景而做出不同的决策。

逻辑边界

根据业务对领域进行逻辑分解时,分与合是两个矛盾而又统一的概念。合是目标,分是降低复杂度的一种手段。分实则是为了更好的合。通过业务分解,每个分解出来的限界上下文规模就变得更小,因而更容易理解和把控。由于这种分解是从业务相关性来考虑的,使得领域可以更加细分,业务分析师或者领域专家就可以只要求掌握更加细分的专精领域。

从系统的代码模型(Code Model)看,所谓逻辑边界有两种表现形式。以Java为例,归纳如下:

  • 命名空间级别:逻辑边界仅仅通过命名空间进行界定,但是所有的限界上下文其实都处于同一个模块中,编译后都属于同一个Jar包。
  • 模块级别:在命名空间上是逻辑分离的,而不同限界上下文则属于同一个项目的不同模块,编译后会生成各自的Jar包。若限界上下文之间存在依赖,则在运行时,这些Jar会被同时加载到同一个Java虚拟机中。这里所谓的“模块”,在Java代码中也可以创建为Jigsaw的module。

将限定上下文的边界视为逻辑边界是最常见也是最简单的一种形式。一方面逻辑的分离可以保证系统代码的清晰结构,另一方面它也使得限界上下文之间的协作变得更加容易,更加高效。在物理上,限界上下文彼此之间的通信其实是无缝集成的,要重用的领域模型都可以直接访问,并对模型类进行实例化。如下是国际报税系统的逻辑边界(Java): 

然而,正所谓越容易重用,就越容易产生耦合。编写代码时,我们需要谨守这条无形的逻辑边界,时刻注意不要逾界,并确定限界上下文各自对外公开的接口,避免对具体的实现产生依赖。

采用逻辑边界划分限界上下文的系统架构是单块(Monolithic)架构,所有的限界上下文都部署在同一个进程中,因此不能针对某一个限界上下文进行水平伸缩。需要对限界上下文的实现进行替换或升级时,会影响到整个系统。即使我们守住了逻辑边界,这种耦合仍然存在,导致各个限界上下文的开发互相影响,团队之间的协调成本也随之而增加。

物理边界

逻辑边界的坏,正是物理边界的好;反过来,物理边界的坏,同样是逻辑边界的好。当我们将限界上下文的边界定义为物理边界时,每个限界上下文就变成了一个个细粒度的微服务。

这里,我们需要针对Eric Evans提出的“限界上下文”概念做进一步澄清:限界上下文究竟是仅仅针对领域模型的边界划分,还是对整个架构(包括基础设施层以及需要使用的外部资源)垂直方向的划分?正如前面对Eric Evans观点的引用,他在《领域驱动设计》一书中明确地指出:“根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。”显然,限界上下文不仅仅作用于领域层和应用层。它是架构设计而非仅仅是领域设计的关键因素。

倘若我们将限界上下文的边界视为物理边界,则可以保证边界内的服务、基础设施乃至于存储资源、中间件等其他外部资源的完整性,最终形成自治的服务。限界上下文之间仅仅通过限定的方式以限定的通信协议和数据格式进行通信,除此之外,彼此没有任何共享,这种架构被称之为零共享架构。这种架构的表现形式为:每个限界上下文都有自己的代码库、数据存储以及开发团队,每个限界上下文选择的技术栈和语言平台也可以不同。当每个限界上下文都被物理隔离时,一个限界上下文的开发人员就不能调用另一个限界上下文的方法,或者将数据存储在共享结构中了,这可以避免因为共享带来的耦合。下图为危机分析系统的架构: 

物理分隔开的限界上下文变得小而专,使得我们可以很好地安排遵循2PTs规则的小团队去治理它。然而,这种架构的复杂度也不可低估。限界上下文之间的通信是跨进程的,我们需要考虑通信的健壮性。数据库是完全分离的,当需要关联之间的数据时,需得跨限界上下文去访问,无法享受数据库自身提供的关联福利。由于每个限界上下文都是分布式的,如何保证数据的一致性也是一件棘手的问题。当整个系统都被分解成一个个可以独立部署的限界上下文时,运维与监控的复杂度也随之而剧增。

数据库共享

在逻辑边界和物理边界中间,还存在一种折中的手段。在考虑限界上下文划分时,分开考虑代码模型与数据库模型,就可能出现在代码上分离,而在数据库层面却存在数据共享的形式,即多个限界上下文共享同一个数据库。

因为没有分库,在数据库层面就可以更好地保证事务的ACID。这或许是该方案最有说服力的证据,但也可以视为是对“一致性”约束的妥协。

数据库共享的问题在于数据库的变化方向与业务的变化方向会不一致。这种不一致性体现在两个方面:

  • 耦合:虽然业务上限界上下文之间是解耦的,但是在数据库层面依然存在强耦合关系
  • 水平伸缩:部署在应用服务器的应用服务可以根据限界上下文的边界单独进行水平伸缩,但是在数据库层面却无法做到

根据Netflix团队提出的微服务架构最佳实践,其中一个最重要特征就是“每个微服务的数据单独存储”。但是服务的分离并不绝对代表数据应该分离。数据库的样式(Schema)与领域模型未必存在一对一的映射关系。在对数据进行分库设计时,如果仅仅站在业务边界的角度去思考,可能会因为分库的粒度太小,导致不必要的跨库关联。因此,我们可以将“数据库共享”模式视为一种过渡方案,不要在一开始设计微服务的时候,就直接将数据彻底分开,而是采用演进式的设计。

为了便于在演进设计中将分表重构为分库,从一开始要注意避免在两个表之间建立外键约束关系。某些关系型数据库可能通过这种约束关系提供级联更新与删除的功能,这种功能反过来会影响代码的实现。一旦因为分库而去掉表之间的外键约束关系,需要修改的代码太多,会导致演进的成本太高,甚至可能因为某种疏漏带来隐藏的Bug。

没有外键约束关系可能在当前增加了开发成本,却为未来的演进打开了方便之门。例如,在针对某手机品牌开发的舆情分析系统中,危机查询服务提供对识别出来的危机的查询,需要通过userId获得危机处理人、危机汇报人的详细信息。左图为演进前直接通过数据库查询的方式,右图则切断了这种数据库耦合,改为服务调用的方式: 

倘若架构被设计为数据库共享,且两个服务需要操作同一张数据表(这张表被称之为“共享表”),则传递了一个信号,即我们的设计可能出现了错误:

  • 遗漏了一个限界上下文,共享表对应的是一个被重用的服务:买家在查询商品时,商品服务会查询价格表中的当前价格,而在提交订单时,订单服务也会查询价格表中的价格,计算当前的订单总额;共享价格数据的原因是我们遗漏了价格上下文,通过引入价格服务就可以解除这种不必要的数据共享。
  • 职责分配出现了问题,操作共享表的职责应该分配给已有的服务:舆情服务与危机服务都需要从邮件模板表中获取模板数据,然后再调用邮件服务组合模板的内容发送邮件;实际上从邮件模板表获取模板数据的职责应该分配给已有的邮件服务。
  • 共享表对应两个限界上下文的不同概念:仓储上下文与订单上下文都需要访问共享的产品表,但实际上这两个上下文需要的产品信息是完全不同的,应该按照限界上下文的边界分开为产品建表。

为什么会出现这三种错误的设计?根本原因还是在于我们没有通过业务建模,而是在数据库中隐式地进行建模,因而在代码中没有体现正确的领域模型,从而导致了数据库层面的耦合或共享。

部分PPT内容

原文发布时间为:2017-12-14

本文作者:张逸

时间: 2024-10-09 08:22:55

张逸:限界上下文的边界的相关文章

一次设计演进之旅 | 张逸

需求背景: 我们需要实现对存储在HDFS中的Parquet文件执行数据查询,并通过REST API暴露给前端以供调用.由于查询的结果可能数量较大,要求API接口能够提供分页查询.在第一阶段,需要支持的报表有5张,需要查询的数据表与字段存在一定差异,查询条件也有一定差异. 每个报表的查询都牵涉到多张表的Join.每张表都被创建为数据集,对应为一个Parquet文件.Parquet文件夹名就是数据集名,名称是系统自动生成的,所以我们需要建立业务数据表名.Join别名以及自动生成的数据集名的映射关系.

由参加领域驱动设计大会与自己所想的

2017首届领域驱动技术大会一直是我非常期望的,要非常感谢右军赠送的门票能够让我领略大会风采. 这届大会组织者非常用心,组织了非常多的话题可供探讨,确实大会的内容给我带来的感觉是震撼的,我之前对领域的了解也仅从<领域驱动设计>以及<实现领域驱动设计>这两本书中有过学习,以及在实现微服务生态体系的过程中有过一些接触. 在大会的整个进程中,听了很多老师不同主题的演讲,让我印象极为深刻的还是:张逸老师的<Bounded Context的实践意义>.腾云老师的<DDD-没

Re:从零开始的领域驱动设计

领域驱动的火爆程度不用我赘述,但是即便其如此得耳熟能详,但大多数人对其的认识,还只是停留在知道它的缩写是DDD,知道它是一种软件思想,或者知道它和微服务有千丝万缕的关系.Eric Evans对DDD的诠释是那么地惜字如金,而我所认识的领域驱动设计的专家又都是行业中的资深前辈,他们擅长于对软件设计进行高屋建瓴的论述,如果没有丰富的互联网从业经验,是不能从他们的分享中获取太多的营养的,可以用曲高和寡来形容.1000个互联网从业者,100个懂微服务,10个人懂领域驱动设计. 可能有很多和我一样的读者,

可视化与领域驱动设计

序言:  张逸者,70年代生人.软件开发生涯经历了从程序员.项目经理.测试经理.开发部长.技术总监到架构师的一个循环,而后程序员,现在是创业公司CTO.三大爱好是开发.写作与阅读.著译作包括<软件设计精要与模式>.<WCF服务编程>.<Java设计模式>与<恰如其分的软件架构>.张逸现居于锦官城,与中生代诸君多有往来.张君于设计模式.软件架构.DDD以及clean code等方面研究尤深.开公众号而不刷粉,则多数文字并不显于街市,中生代编辑之以飨读者,使佳作

领域驱动设计的实现之路

2004年,当Eric Evans的那本<领域驱动设计--软件核心复杂性应对之道>(后文简称<领域驱动设计>)出版时,我还在念高中,接触到领域驱动设计(DDD)已经是8年后的事情了.那时,我正打算在软件开发之路上更进一步,经同事介绍,我开始接触DDD. 我想,多数有经验的程序开发者都应该听说过DDD,并且尝试过将其应用在自己的项目中.不知你是否遇到过这样的场景:你创建了一个资源库(Repository),但一段时间之后发现这个资源库和传统的DAO越来越像了,你开始反思自己的实现方式

CAP的相对论

CAP是什么? CAP理论,被戏称为[帽子理论].CAP理论由Eric Brewer在ACM研讨会上提出,而后CAP被奉为分布式领域的重要理论[1] . 分布式系统的CAP理论:首先把分布式系统中的三个特性进行了如下归纳:  一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值.(等同于所有节点访问同一份最新的数据副本)  可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求.(对数据更新具备高可用性) 分区容忍性(P):以实际效果而言,分区相当于对通信的时

Asp.net管道模型(管线模型)之一发不可收拾

前言   为什么我会起这样的一个标题,其实我原本只想了解asp.net的管道模型而已,但在查看资料的时候遇到不明白的地方又横向地查阅了其他相关的资料,而收获比当初预想的大了很多.   有本篇作基础,下面两篇就更好理解了:   理解并自定义HttpHandler   理解并自定义HttpModule 目录   一般不写目录,感觉这次要写的东西有些多就写一个清晰一下吧.   1.Asp.net管道模型:   2.进程的子进程与进程的线程:   3.应用程序域(AppDomain):   4.IIS5

一个更好的可视化微服务架构的方式

本文讲的是一个更好的可视化微服务架构的方式[编者的话]如何快速地可视化一个微服务架构,本文作者有一个很酷的办法,赶紧来看看吧! [3 天烧脑式容器存储网络训练营 | 深圳站]本次培训以容器存储和网络为主题,包括:Docker Plugin.Docker storage driver.Docker Volume Pulgin.Kubernetes Storage机制.容器网络实现原理和模型.Docker网络实现.网络插件.Calico.Contiv Netplugin.开源企业级镜像仓库Harbo

2016,我们一起追过的架构。中生代邀您一起构建2017!

01属性派 任何系统必有其自身的架构属性. An architecture-a system's attributes-and what an architect produces-a setof documents-definitely are not the same thing. An architectural description (AD) is a set of artifacts that documents anarchitecture in a way its stakeho