边界由有界上下文决定,这在领域驱动设计中具有非凡的意义。对应于通用语言,限界上下文是语言的边界,而对于领域模型来说,限界上下文是模型的边界,两者对应问题空间(ProblemSpace)的定义。对于系统架构来说,限界上下文也决定了应用边界和技术边界,进而帮助我们确定整个系统和每一个限界上下文的解决方案。可以说,限界上下文是连接问题空间和解空间的重要桥梁。那么,限界上下文定义的边界到底是逻辑边界还是物理边界呢?这不是定论,需要根据不同的场景做出不同的决定。逻辑边界当基于业务逻辑分解域时,分离和集成是两个相互矛盾但又统一的概念。集成是目的,分离是降低复杂性的手段。分工其实是为了更好的合作。通过业务分解,每个分解后的限界上下文的规模变小,更容易理解和控制。由于这种分解是从业务相关性的角度考虑的,所以领域可以进一步细分,业务分析师或领域专家只能要求掌握更细分的专业领域。从系统的代码模型(CodeModel)来看,所谓的逻辑边界有两种表现形式。以Java为例,归纳如下:命名空间层:逻辑边界仅由命名空间定义,但所有限界上下文实际上都在同一个模块中,编译后属于同一个Jar包。模块级:命名空间在逻辑上是分离的,不同的限界上下文属于同一个项目的不同模块,编译后会生成各自的Jar包。如果限界上下文之间存在依赖关系,这些Jar会在运行时同时加载到同一个Java虚拟机中。这里所谓的“模块”也可以在Java代码中创建为Jigsaw模块。将上下文边界视为逻辑边界是最常见和最简单的形式。逻辑的分离一方面可以保证系统代码的结构清晰,另一方面也使得限界上下文之间的协作更加容易和高效。在物理上,限界上下文之间的通信是无缝集成的,可以直接访问和实例化要重用的领域模型。以下是国际报税系统(Java)的逻辑边界:然而,俗话说越容易复用,越容易产生耦合。我们在写代码的时候,需要遵守这个看不见的逻辑边界,时刻注意不要跨越边界,确定限界上下文对外暴露的接口,避免依赖具体的实现。使用逻辑边界划分限界上下文的系统架构是一个单体架构,所有的限界上下文都部署在同一个进程中,因此无法针对某个限界上下文进行水平扩展。当限界上下文实现需要替换或升级时,它会影响整个系统。即使我们保留了逻辑边界,这种耦合仍然存在,导致各个边界上下文的开发相互影响,团队之间的协调成本也随之增加。物理边界的坏和逻辑边界的坏恰恰是物理边界的好;反过来,物理边界的坏也是逻辑边界的好。当我们将有界上下文的边界定义为物理边界时,每个有界上下文就变成了一个细粒度的微服务。在这里,我们需要进一步厘清EricEvans提出的“限界上下文”的概念:限界上下文是仅仅针对领域模型的边界划分,还是整个架构(包括基础设施层和外部)的垂直划分需要使用的资源)?正如前面引用EricEvans的观点,在他的书《领域驱动设计》中,他明确指出:“根据团队的组织设置模型的价值,各个部分的使用软件系统和物理表示(代码和数据库模式等)的边界。”显然,限界上下文不仅仅适用于领域层和应用层。它是架构设计的关键因素,而不仅仅是域设计。如果我们把限界上下文的边界看成一个物理边界,就可以保证边界内的服务、基础设施、存储资源、中间件等外部资源的完整性,最终形成一个自治的服务。限界上下文之间仅以有限的通信协议和数据格式以有限的方式进行通信,彼此之间不存在共享。这种架构称为零共享架构。这种架构的表现形式是:每个限界上下文都有自己的代码库、数据存储和开发团队,每个限界上下文选择的技术栈和语言平台也可以不同。当每个BoundedContext在物理上是隔离的,一个BoundedContext的开发者不能调用另一个BoundedContext的方法,也不能将数据存储在共享结构中,从而避免了共享带来的耦合。下图是危机分析系统的架构:物理上分离的boundedcontext变得小而专,这样我们就可以很好的安排一个遵循2PTs规则的小团队去治理它。但是,这种架构的复杂性不可低估。限界上下文之间的通信是跨进程的,我们需要考虑通信的健壮性。数据库完全分离。当需要关联之间的数据时,需要跨限界上下文访问,无法享受数据库本身提供的关联好处。由于每个限界上下文都是分布式的,如何保证数据的一致性也是一个棘手的问题。当整个系统被分解成可以独立部署的限界上下文时,运维和监控的复杂度也急剧增加。数据库共享在逻辑边界和物理边界之间,还是有折衷的。在考虑限界上下文的划分时,分别考虑代码模型和数据库模型,代码上可能存在分离,但在数据库层面存在一种数据共享的形式,即多个限界上下文共享同一个数据库。因为没有分库,事务的ACID可以在数据库层面得到更好的保证。这也许是该方案最有说服力的证据,但也可以看作是对“一致性”约束的妥协。数据库共享的问题是数据库的变化方向会和业务的变化方向不一致。这种不一致体现在两个方面:耦合:虽然业务上界上下文解耦,但在数据库层面仍然存在强耦合关系水平扩展:部署在应用服务器上的应用服务可以独立根据微服务的最佳实践Netflix团队提出的架构,最重要的特性之一就是“为每个微服务单独存储数据”。但是服务的分离并不绝对意味着数据就应该分离。数据库模式(Schema)和领域模型之间可能不存在一对一的映射关系。在设计数据分库时,如果仅仅从业务边界的角度思考,分库的粒度可能太小,导致不必要的跨库关联。因此,我们可以将“数据库共享”模式视为一种过渡性的解决方案。一开始设计微服务时不要直接将数据完全分离,而是采用演进式设计。为了便于在演化设计中将分表重构为分库,需要注意避免一开始就在两表之间建立外键约束关系。一些关系型数据库可能会通过这种约束关系提供级联更新和删除功能,进而影响代码的实现。一旦分库去掉表间的外键约束关系,需要修改的代码太多,导致演化成本高,甚至可能因为某些遗漏而导致隐藏bug。没有外键约束可能会增加目前的开发成本,但它为未来的发展打开了大门。例如,某手机品牌的舆情分析系统中,危机查询服务提供对识别出的危机进行查询,需要通过userId获取危机处理者和危机报告者的详细信息。左图是演化前直接通过数据库查询的方式,而右图是切断了这种数据库耦合,改为服务调用的方式:如果架构设计为共享数据库,并且两个服务需要操作同一个数据表(这个表叫做“共享表”),这发出了一个信号,表明我们的设计可能存在错误:缺少一个有界上下文,而共享表对应的是一个重用的服务:买家查询商品时,商品服务会查询价格表中的当前价格,提交订单时,订单服务也会查询价格表中的价格,计算出当前订单总额;共享价格数据的原因是我们已经忽略了价格上下文,通过引入价格服务可以消除这种不必要的数据共享。职责分配有问题,应该将操作共享表的职责分配给现有服务:舆情服务和危机服务都需要从邮件模板表中获取模板数据,然后调用内容邮件服务组合模板发送邮件;事实上,从邮件模板表中获取模板数据的责任应该分配给一个现有的邮件服务。共享表对应两个限界上下文的不同概念:存储上下文和订单上下文都需要访问共享商品表,但实际上两个上下文需要的商品信息是完全不同的,商品信息应该根据限界上下文的边界分别创建。表面。为什么会出现这三种错误的设计?根本原因是我们没有对业务进行建模,而是在数据库中隐式建模,所以代码中没有体现出正确的领域模型,从而导致了数据库层面的问题。耦合或共享。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
