架构设计内容分享(一百九十八):菱形对称架构的演进、定义和价值

目录

六边形架构

整洁架构思想

分层架构

层之间的协作

演进为菱形对称架构

菱形对称架构的组成

引入上下文映射

菱形对称架构的价值

展现上下文映射模式

响应变化的能力


限界上下文是基本的架构单元,每个限界上下文都是一个自治的独立王国。一个典型的限界上下文是以领域模型为核心关注点进行纵向切分的自治单元,它在边界内维护着由自己控制的架构体系,使得内部所有的软件元素共同形成一个相对独立的主体,为系统贡献了内聚的业务能力。

然而,Eric Evans在提出限界上下文的概念时,并没有提出与之匹配的架构模式。他提出的分层架构(Layered Architecture)是对整个系统的层次划分,核心思想是将领域单独分离出来。这是从业务维度对整个系统的横向切分,与限界上下文领域维度的纵向切分形成了一种交错的架构体系。从系统层次观察这种交错的架构体系,可以映射出系统级的架构,而对于限界上下文内部,我们也亟需一种架构模式来表达它内部的视图,以满足它的自治特性。

领域驱动设计社区做出的尝试是为限界上下文引入六边形架构。

六边形架构

六边形架构(Hexagonal Architecture)又被称之为端口适配器(Port and Adapter),由Alistair Cockburn提出,他的定义为:“无论是被用户、程序、还是自动化测试或批处理脚本驱动,应用程序(Application)都能一视同仁地对待,最终使得应用程序能独立于运行时设备和数据库进行开发与测试。”

应用程序封装了领域逻辑,放在六边形的边界内,它与外界的通信只能通过端口与适配器进行。端口存在两个方向:入口和出口。与之相连的适配器自然也存在两种适配器:入口适配器(Inbound Adapter,又可称为Driving Adapter)和出口适配器(Outbound Adapter,又可称为Driven Adaptor)。入口适配器负责处理系统外部发送的请求,也就是驱动应用程序运行的用户、程序、自动化测试或批处理脚本向入口适配器发起请求,适配器将该请求适配为符合内部应用程序执行的输入格式,转交给端口,再由端口调用应用程序。出口适配器负责接收内部应用程序通过出口端口传递的请求,对其进行适配后,向位于外部的运行时设备和数据库发起请求。

显然,从内外边界的视角观察端口与适配器的协作,整个过程如下图所示:

图片

在Cockburn对六边形架构的初始定义中,应用程序位于六边形边界内部,封装了支持业务功能的领域逻辑。入口端口与出口端口放在六边形边界之上,前者负责接收外部的入口适配器转换过来的请求,后者负责发送应用程序的请求给外部的出口适配器,由此可以勾勒出一个清晰的六边形:

图片

我们说限界上下文是在专有知识语境下业务能力的体现,这一业务能力固然以领域模型为核心,却必须通过与外部环境的协作方可支持其能力的实现,如此才能体现它以领域模型为核心关注点进行纵向切分建立自治架构单元的特征。因此,限界上下文的边界实则包含了对驱动它运行的入口请求的适配与响应,也包含了对外部设备和数据库的访问。若要将限界上下文与六边形架构结合起来,就需要将入口适配器和出口适配器放在限界上下文的边界之内,从而构成一个外部的六边形:

图片

六边形架构清晰地勾勒出限界上下文的两个边界:

  • 外部边界:通过外部六边形将单独的业务能力抽离出来,隔离了不同的业务关注点。我将此六边形称之为“应用六边形”。

  • 内部边界:通过内部六边形将领域单独抽离出来,隔离了业务复杂度与技术复杂度。我将此六边形称之为“领域六边形”。

以预订机票场景为例,用户通过浏览器访问订票网站,向订票系统发起订票请求。根据六边形架构,浏览器访问的网站前端位于应用六边形之外,属于驱动应用程序运行的起因。订票请求通过浏览器发送给以RESTful契约定义的控制器服务ReservationController,它作为入口适配器,介于应用六边形与领域六边形之间。

ReservationController在接收到以JSON格式传递的前端请求后,将其转换(反序列化)为入口端口ReservationAppService需要的请求对象。入口端口为应用服务,位于领域六边形的边界之上。当它接收到入口适配器转换后的请求对象后,调用位于领域六边形边界内的领域服务TicketReservation,执行领域逻辑。在执行订票的领域逻辑时,需要向数据库添加一条订票记录。这时,位于领域六边形边界内的领域模型对象会调用出口端口ReservationRepository

出口端口为资源库,位于领域六边形的边界之上,定义为接口,真正访问数据库的逻辑则由介于应用六边形与领域六边形边界内的出口适配器ReservationRepositoryAdapter实现。该实现访问了数据库,将端口发送过来的插入订票记录的请求转换为数据库能够处理的消息,执行插入操作。该业务场景在六边形架构中的体现为:

图片

六边形架构中的端口是解耦的关键。

入口端口体现了“封装”的思想,它既隔离了外部请求转换所必须的技术实现,如REST服务的序列化机制与HTTP请求路由等基础设施功能,又防止了领域模型向外泄露,因为端口公开的服务接口方法已经抹掉了领域模型的信息。出口端口体现了“抽象”的思想,它通常被定义为抽象接口,不包含任何具体访问外部设备和数据库的实现。显然,入口端口抵御了外部资源可能对当前限界上下文造成的侵蚀,入口适配器与入口端口之间的关系是一个依赖调用关系;出口端口隔离了领域逻辑对技术实现以及外部框架或环境的依赖,出口适配器与出口端口之间的关系是一个接口实现关系。

二者的结合共同为限界上下文两个边界层次的松散耦合提供助力,最终保证了限界上下文的自治能力。

整洁架构思想

Robert Martin总结了六边形架构以及其他相似架构如DCI架构的共同特征,他认为:“它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层只包含该软件的业务逻辑,而用户接口、系统接口则属于其他层。”

我们知道,限界上下文同样是对软件系统的切割,依据的关注点主要是根据领域知识的语境体现业务能力的差异。在限界上下文内部,我们又可以针对限界上下文进行关注点的切割,体现清晰的层次结构。这个层次结构必须遵循整洁架构(Clean Architecture)的思想。整洁架构可以通过下图体现:

图片


△ 选自Robert Martin的《架构整洁之道》

该架构思想提出的模型是一个类似内核模式的内外层架构。由内及外分为四层,包含的内容分别为:

  • 企业业务规则(Enterprise Business Rules)

  • 应用业务规则(Application Business Rules)

  • 接口适配器(Interface Adapters)

  • 框架与驱动器(Frameworks & Drivers)

解密整洁架构模型,我们发现许多值得深思的架构特征:

  • 不同层次的组件其变化频率也不相同,引起变化的原因也不相同。

  • 层次越靠内的组件依赖的内容越少,处于核心的业务实体(Entities)没有任何依赖。

  • 层次越靠内的组件与业务的关系越紧密,属于特定领域的内容,因而难以形成通用的框架。

  • 业务实体封装了企业业务规则,准确地讲,它组成了面向业务的领域模型。

  • 应用业务规则层是打通内部业务与外部环境的一个通道,因而提供了输出端口(Output Port)与输入端口(Input Port),但它对外呈现的接口是一个用例(Use Case),体现了系统的应用逻辑。

  • 接口适配器层包含了网关(Gateways)、控制器(Controllers)与展示器(Presenters),它们皆属于适配器(Adapter),用于打通应用业务逻辑与外层的框架和驱动器,实现逻辑的适配以访问外部资源。

  • 位于外部的框架与驱动器负责对接外部环境,不属于限界上下文的范畴,但选择这些框架和驱动器,是设计决策要考虑的内容。

遵循整洁架构思想的限界上下文就是要根据变化的速率与特征进行切割,定义出一个由同心圆组成的内外分离的架构模型。该模型的每个层次体现了不同的关注点,维持了清晰的职责边界。在这个架构模型中,“外层圆代表的是机制,内层圆代表的是策略”。机制属于技术实现的细节,容易受到外界环境变化的影响;策略与业务有关,封装了限界上下文最为核心的领域模型,最不容易受到外界影响而变化。那么,遵循稳定依赖原则(The Stable Dependencies Principle),即一个软件元素应该依赖于比自己更稳定的软件元素,依赖方向就应该从外层圆指向内层圆,如此即可保证核心的领域模型更加纯粹,不对外部易于变化的事物形成依赖,隔离了外部变化的影响。

整洁架构与六边形架构是一脉相承的。六边形架构中的应用六边形与领域六边形就是根据变化速率对关注点的切割,位于外层的适配器分别通过职责委派与接口实现依赖了内部对应的端口,端口又依赖了内部的领域模型。

但是,六边形架构仅仅区分了内外边界,分别提炼了端口与适配器角色,并没有规划限界上下文内部各个层次与各个对象之间的关系;整洁架构又是一个通用的架构思想,提炼的是企业系统架构设计的基本规则与主题。二者都无法完美地契合限界上下文的架构诉求。因此,当我们将六边形架构与整洁架构思想引入到限界上下文时,还需要引入分层架构给出更为细致的设计指导,即确定层、模块与角色构造型之间的关系。

分层架构

分层架构是运用最为广泛的架构模式,几乎每个软件系统都需要通过层(Layer)来隔离不同的关注点(Concern Point),以此应对不同需求的变化,使得这种变化可以独立进行。Scott Millett等人解释了在领域驱动设计中引入分层架构模式的原因和目的:“为了避免将代码库变成大泥球(BBoM)并因此减弱领域模型的完整性且最终减弱可用性,系统架构要支持技术复杂性与领域复杂性的分离。引起技术实现发生变化的原因与引起领域逻辑发生变化的原因显然不同,这就导致基础设施和领域逻辑问题会以不同速率发生变化。”

引起变化的原因不同导致了变化的速率不同,体现了单一职责原则(Single-Responsibility Principle,SRP)。Robert Martin认为单一职责原则就是“一个类应该只有一个引起它变化的原因”,换言之,如果有两个引起类变化的原因,就需要分离。若将单一职责原则运用到分层架构模式,考虑的变化粒度就是层。

软件的经典三层架构自顶向下由用户界面层(User Interface Layer)、业务逻辑层(Business Logic Layer)与数据访问层(Data Access Layer)组成:

经典的三层架构在过去的大多数企业系统中得到广泛运用,有其历史原因。在提出该分层架构模式的时代,多数企业系统往往较为简单,本质上都是采用客户端-服务器(Client-Server)风格的数据库管理系统,对业务的处理就是对数据库的管理,而用户界面的呈现与业务逻辑并未在物理上分离,如果在逻辑上不加以解耦,就无法有效隔离界面呈现、业务功能与数据访问,它们的代码纠缠在一起,极易形成大泥球一般的代码库。

领域驱动设计在经典三层架构的基础上做了进一步改良,在用户界面层与业务逻辑层之间引入了新的一层,即应用层(Application Layer)。同时,一些层次的命名也发生了变化。业务逻辑层被更名为领域层自然是题中应有之义,而将数据访问层更名为基础设施层(Infrastructure Layer),则突破了之前数据库管理系统的限制,扩大了这个负责封装技术复杂度的基础逻辑层的内涵。下图为Eric Evans定义的分层架构:

图片


△ 上图来自Eric Evans的《领域驱动设计》

该书对各层的职责作了简单的描述:

层次

职责

用户界面/展现层

负责向用户展现信息以及解释用户命令。

应用层

很薄的一层,用来协调应用的活动。它不包含业务逻辑。它不保留业务对象的状态,但它保有应用任务的进度状态。

领域层

本层包含关于领域的信息。这是业务软件的核心所在。在这里保留业务对象的状态,对业务对象和它们状态的持久化被委托给了基础设施层。

基础设施层

本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支撑库等作用。

当分层架构变得越来越普及时,我们的设计反而变得越来越僵化。一部分软件架构师并未理解分层架构的本质,只知道依样画葫芦地将分层应用到系统中。要么采用经典的三层架构,要么遵循领域驱动设计改进的四层架构,却未思考和叩问如此分层究竟有何道理?这是分层架构被滥用的根源。

视分层(Layer)为一个固有的架构模式,其滥觞应为Frank Buschmann等人著的《面向模式的软件架构》第一卷《模式系统》。该模式参考了ISO对TCP/IP协议的分层。《模式系统》对分层的描述为:“分层架构模式有助于构建这样的应用:它能被分解成子任务组,其中每个子任务组处于一个特定的抽象层次上。”

显然,这里所谓的“分层”是逻辑上的分层,对子任务组的分解需要考虑抽象层次,一种水平的抽象层次。既然为水平的分层,必然存在层的高与低;而抽象层次的不同,又决定了分层的数量。因此,对于分层架构,我们需要解决如下问题:

  • 分层的依据与原则是什么?

  • 层与层之间是怎样协作的?

分层的依据与原则

之所以要以水平方式对整个系统进行分层,是我们下意识地确定了一个认知规则:机器为本,用户至上机器是运行系统的基础,而我们打造的系统却是为用户提供服务的。分层架构中的层次越往上,其抽象层次就越面向业务、面向用户;分层架构中的层次越往下,其抽象层次就变得越通用,面向设备。为什么经典分层架构为三层架构?正是源于这样的认知规则:其上,面向用户的体验与交互;居中,面向应用与业务逻辑;其下,面对各种外部资源与设备。

在为系统建立分层架构时,我们完全可以基于这个经典的三层架构,沿着水平方向进一步切分属于不同抽象层次的关注点。因此,分层的第一个依据是基于关注点为不同的调用目的划分层次。领域驱动设计的分层架构之所以要引入应用层(Application Layer),目的就是为了给调用者提供完整的业务用例,无需与细粒度的领域模型对象直接协作。

分层的第二个依据是面对变化。分层时应针对不同的变化原因确定层次的边界,严禁层次之间互相干扰,或者至少将变化对各层带来的影响降到最低。例如,数据库结构的修改会影响到基础设施层的数据模型以及领域层的领域模型,但当我们仅需要修改基础设施层中数据库访问的实现逻辑时,就不应该影响到领域层了。层与层之间的关系应该是正交的。

进行分层时,我们还应该遵循Kent Beck在“组合方法”模式中提出的“单一抽象层次原则(SLAP)”。该原则本意是要求一个方法中的所有操作处于相同的抽象层,运用到分层架构中,就是确保同一层的组件处于同一个抽象层次

例如,在一个基于元数据的多租户报表系统中,我们在分层架构中特别定义了一个引擎层(engine layer),包含了报表引擎(report engine)、实体引擎(entity engine)和数据引擎(data engine)三个组件。引擎是一个隐喻(metaphor),它相当于为报表系统提供了报表生成、实体映射与数据处理的驱动引擎。引擎层之下,是基础设施层,提供了多租户、数据库访问与元数据解析与管理等功能。在引擎层之上是一个控制层,通过该控制层的组件可以将引擎层的各个组件组合起来。分层架构的顶端是面向用户的用户展现层。如下图所示:

图片

这一分层架构突破了经典三层架构的逻辑层划分,但它遵循了分层的依据与原则,可以认为是合理的分层设计。

层之间的协作

在我们固有的认识中,分层架构的依赖都是自顶向下传递的,这也符合大多数人对分层的认知模型。从抽象层次看,层次越处于下端,就会变得越通用,与具体的业务隔离得越远,从而形成基础设施层。为了避免重复制造轮子,它还会调用位于系统外部的平台或框架,如依赖注入框架、ORM框架、消息中间件等,以完成更加通用的功能。若依赖的传递方向仍然采用自顶向下,就会导致包含领域逻辑的领域层依赖于基础设施层,又因为基础设施层依赖于外部平台或框架,使得领域层也将受制于它们。

依赖倒置原则(Dependency Inversion Principle,DIP)提出了对自顶向下依赖的挑战,它要求“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”这个原则正本清源,给了我们当头棒喝——谁规定在分层架构中,依赖就一定要沿着自顶向下的方向传递?我们常常理解依赖,是因为被依赖方需要为依赖方(调用方)提供功能支撑,这是从功能重用的角度来考虑的。但我们不能忽略变化对系统产生的影响!与建造房屋一样,我们自然希望分层的模块“构建”在稳定的模块之上。谁更稳定?抽象更稳定。

因此,依赖倒置原则隐含的本质是:我们要依赖不变或稳定的元素(类、模块或层)也就是该原则的第二句话:抽象不应该依赖于细节,细节应该依赖于抽象。

这一原则实际是“面向接口设计”原则的体现,即“针对接口编程,而不是针对实现编程”。遵循这一原则,就使得作为调用者的高层模块对低层模块的实现做到一无所知。这样带来的好处是:

  • 低层模块的细节实现可以独立变化,避免变化对高层模块产生污染

  • 在编译时,高层模块可以独立于低层模块单独存在

  • 对于高层模块而言,低层模块的实现是可替换的

倘若高层依赖于低层的抽象,必然面对一个问题:如何将具体的实现传递给高层的类?由于在高层通过接口隔离了对具体实现的依赖,就意味着这个具体依赖被转移到了外部,究竟使用哪一种具体实现,由外部的调用者来决定,只有在运行调用者代码时,才将外面的依赖传递给高层的类。Martin Fowler形象地将这种机制称为“依赖注入(dependency injection)”。为了解除高层对低层的依赖,我们往往需要将面向接口设计与依赖注入结合起来。

层之间的协作并不一定是自顶向下的传递通信,也有可能是自底向上通信,例如在计算机集成制造系统(Computer Integrated Manufacture System, CIMS)中,往往会由低层的设备监测系统去监测(侦听)设备状态的变化。当状态发生变化时,需要将变化的状态通知到上层的业务系统。如果说自顶向下的消息传递被描述为“请求(或调用)”,则自底向上的消息传递则被形象地称之为“通知”。倘若我们颠倒一下方向,自然也可以视为这是上层对下层的观察,故而可以运用观察者模式(Observer Pattern),在上层定义Observer接口,并提供update()方法供下层主题(Subject)在感知状态发生变更时调用。

面向接口设计带来了低层实现对高层抽象的依赖,观察者模式带来了低层主题对高层观察者的依赖,它们在分层架构中都体现了低层对高层的依赖,颠覆了固有思维形成的由高及低的依赖方向。

现在,我们对分层架构有了更清醒的认识。它通过水平抽象体现了关注点分离,只要存在相同抽象层次的关注点,就可以单独为其建立一个逻辑层,抽象层数不是固定的,每一层的名称也不必一定遵循经典的分层架构要求。对系统逻辑层的划分需得结合该系统的具体业务场景而定。当然,我们也要认识到层次多少的利弊:过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,使得系统的结构不够合理。

我们还需要正视分层架构中各层之间的协作关系,打破高层依赖低层的固有思维,从解除耦合或降低耦合的角度探索层之间可能的协作关系。我们还需要确定分层的架构原则或约束,例如是否允许跨层调用,即每一层都可以使用比它低的所有层的服务,而不仅仅是相邻低层。这就是所谓的“松散分层系统(Relaxed Layered System)”。

演进为菱形对称架构

回到限界上下文的内部视图。六边形架构通过外部的应用六边形与内部的领域六边形将整个限界上下文分隔为三个区域:

图片

可惜,六边形架构并未对这三个区域命名,这就为团队的协作交流制造了障碍。例如,当团队成员正在讨论一个入口端口的设计时,需要确定入口端口在代码模型的位置,即确定入口端口所在的命名空间。我们既不可能说它放在“领域六边形的边线”上,也不可能为该命名空间定义一个冗长的包名,例如currentbc.boundaryofdomainhexagon。命名的目的是为了交流,然后形成一种约定,就可以做到不言自明。因此,我们需要寻找一种架构的统一语言为这些区域命名,如此即可将六边形的设计元素映射到代码模型对应的命名空间。

从关注点分离的角度看,六边形架构实则就是隔离内外的分层架构,因此我们完全可以将两个六边形隔离出来的三个区域映射到领域驱动设计的分层架构中。映射时,自然要依据设计元素承担的职责来划分层次:

  • 入口适配器:响应边界外客户端的请求,需要实现进程间通信以及消息的序列化和反序列化,这些功能皆与具体的通信技术有关,故而映射到基础设施层

  • 入口端口:负责协调外部客户端请求与内部应用程序之间的交互,恰好与应用层的协调能力相配,故而映射到应用层

  • 应用程序:承担了整个限界上下文的领域逻辑,包含了当前限界上下文的领域模型,毫无疑问,应该映射到领域层

  • 出口端口:作为一个抽象的接口,封装了对外部设备和数据库的访问,由于它会被应用程序调用,遵循整洁架构思想内部层次不能依赖外部层次的原则,只能将它映射到领域层

  • 出口适配器:访问外部设备和数据库的真正实现,与具体的技术实现有关,映射到基础设施层

如此就建立了六边形架构与领域驱动分层架构之间的映射关系:

图片

通过这一映射,我们就为六边形架构的设计元素找到了统一语言。例如,入口端口属于应用层,它的命名空间自然应命名为currentbc.application。这一映射关系与命名规则实则就是指导团队开发的架构原则。当团队成员在讨论设计方案时,一旦确定该类属于入口端口,不言自明,团队的所有成员都知道它归属于应用层,应该定义在application命名空间下。

在确定分层架构与六边形架构的映射关系时,对出口端口的层次映射是非常勉强的,二者在设计概念上存在冲突。六边形架构的出口端口用于抽象领域模型对外部环境的访问,位于领域六边形的边线之上。根据分层原则,我们应该将介于领域六边形与应用六边形的中间区域划分到基础设施层;根据六边形架构的协作原则,领域模型若要访问外部资源,又需要调用出口端口;根据整洁架构思想,位于内部的领域层不能依赖外部的基础设施层,自然也就不能依赖出口端口了。

要消弭设计原则的矛盾,唯一的办法是将出口端口放在领域层。对于访问数据库的出口端口而言,领域驱动设计定义的资源库(Repository)就放在了领域层。将资源库放在领域层确有论据佐证,毕竟,在抹掉数据库技术的实现细节后,资源库的接口方法就是对聚合领域模型对象的管理,包括查询、修改、增加与删除行为,这些行为也可视为领域逻辑的一部分。然而,限界上下文内部的领域模型不仅限于访问数据库,还可能访问同样属于资源环境的文件、网络与消息队列,也可能访问别的限界上下文乃至系统外部的伴生系统。为了隔离领域模型与外部环境,同样需要为它们定义抽象的出口端口,它们又该放在哪里呢?

如果仍然将这些出口端口放在领域层,就很难自圆其说。例如,出口端口EventPublisher负责将事件发布到消息队列,如果放在领域层,即使它并不提供任何具体实现,终归显得不伦不类。移出来放在外部的基础设施层,又违背了整洁架构思想。如果将资源库从其他出口端口单独剥离出来,又破坏了六边形架构对端口定义的一致性。

与其如此纠结,不如尝试观念的突破!

倘若我们将六边形架构看作是一个对称的架构,以领域为轴心,入口适配器和入口端口与出口适配器和出口端口相对称;同时,适配器必须和端口对应,如此方可保证架构的松耦合。剖析端口与适配器的本质,实质上都是对外部系统或外部资源的处理,只是处理的方向各有不同。Martin Fowler将“封装访问外部系统或资源行为的对象”定义为网关(Gateway),引入到限界上下文的内部架构,就代表了领域层与外部环境之间交互的出入口,即:

 

gateway = port + adapter

根据入口与出口方向的不同,为了体现它所处的方位,我将这个由端口与适配器共同组成的网关分别命名为“北向网关(northbound gateway)”与“南向网关(southbound gateway)”。

北向网关提供了由外向内的访问通道,这一访问方向符合整洁架构的依赖方向,因此不必对北向网关元素进行抽象,只需为外部的调用者提供服务契约即可。为了避免内部领域模型的泄露,北向网关的服务契约不能直接暴露领域模型对象,需要为组成契约的方法参数和返回值定义专门的模型,由于这个模型主要用于调用者的请求和响应,因而称之为“消息契约模型”。

北向网关的服务契约必须调用领域模型的业务方法才能满足调用者的请求,由于领域模型并不知道消息契约模型,需要北向网关负责完成这两个模型之间的互换。由于北向网关既要对外提供服务契约,又要对内完成模型的转换,相当于它同时承担了端口与适配器的作用,因而不用再区分入口端口和入口适配器。限界上下文的外部请求可能来自进程之外,也可能是进程之内,进程内外的差异,决定了通信协议的不同,有必要根据进程的边界将北向网关分为:本地网关与远程网关。前者支持进程内通信,后者用于进程间通信。

南向网关负责封装领域层对外部环境的访问。所谓“外部环境”,包括如数据库、消息队列、文件系统之类的环境资源,也包括系统内的上游限界上下文与系统外的伴生系统,它们也就是组成整洁架构的最外层圆环,包含了技术细节。这些外部环境变化的方向与频率和领域模型完全不同,需要分离抽象接口与具体实现,也就是六边形架构的出口端口与出口适配器,现在,它们共同组成了南向网关。

南向网关的命名已经代表了出口方向,就不要区分入口还是出口,可直接命名为端口与适配器。端口未提供任何实现,即使它被领域层的领域模型调用,也不会将技术实现混入到领域逻辑中。运行时,系统通过依赖注入将适配器实现注入到领域层,满足领域逻辑对外部设备的访问需求。

整个对称架构的结构如下所示:

  • 北向网关:

    • 远程(remote)

    • 本地(local)

  • 领域

  • 南向网关:

    • 端口(port)

    • 适配器(adapter)

入口适配器与入口端口共同组成了北向网关,在对称架构中,它们被合并为“网关”这一设计元素,并因为通信协议的区别而分为远程网关与本地网关;出口端口与出口适配器共同组成了南向网关,在对称架构中,它们分别代表了南向网关的抽象和实现。如此,即构成了由内部领域模型与外部网关组成的对称架构:

图片

这一对称架构凸显了领域层的重要地位,抹去了领域驱动设计原有分层架构中的基础设施层与应用层,以对称的外部网关层代替。在限界上下文内部,位于网关层内部提供基础设施功能的类,本质都是为领域层与外部环境协作提供支撑,区别仅在于方向。

该对称架构虽脱胎于六边形架构与分层架构,却又有别于二者。对称架构北向网关定义的远程网关与本地网关同时承担了端口与适配器的职责,这实际上改变了六边形架构端口-适配器的风格;领域层与南北网关层的内外分层结构,以及南向网关规定的端口与适配器的分离,又与领域驱动设计的分层架构渐行渐远。为了更好地体现这一架构模式的对称特质,我换用了菱形结构来表达,将其称之为“菱形对称架构(Rhombic Symmetric Architecture)”:

菱形对称架构的组成

菱形对称架构从分层架构与六边形架构汲取了营养,形成了以领域为轴心的内外分层对称结构,以此作为推荐的限界上下文内部架构。内部的领域层定义了核心的领域模型,外部的网关层根据方向划分为北向网关与南向网关。考虑到目前的系统多采用前后端分离的架构,且前端UI的设计更多是从用户体验的角度对视图元素进行划分,与限界上下文的边界划分并不吻合,因此,限界上下文边界并未将前端UI包含在内。一个遵循了菱形对称架构的限界上下文包括的设计元素为:

  • 北向网关的远程网关

  • 北向网关的本地网关

  • 领域层的领域模型

  • 南向网关的端口抽象

  • 南向网关的适配器实现

限界上下文以领域模型为核心向南北方向对称发散,从而在边界内形成清晰的逻辑层次,内部领域层与外部网关层恰好体现了业务复杂度与技术复杂度的分离。每个组成元素之间的协作关系表现了清晰直观的自北向南的调用关系。仍以预订机票场景为例,参与该场景的各个类在菱形对称架构下的位置与协作关系如下图所示:

图片

在通过菱形对称架构体现的预订机票业务场景中,本地网关ReservationAppService映射为领域驱动设计元模型中的应用服务,它对外提供了完整的预定机票用例,对内调用了领域层的领域模型对象。为了支持分布式调用,在本地网关之上还定义了远程网关ReservationController,它是一个面向前端视图遵循MVC模式设计的远程服务。本地网关和远程网关共同构成了北向网关。

端口ReservationRepository映射为领域驱动设计元模型中的资源库,是对数据库访问的抽象,适配器ReservationRepositoryAdapter提供了端口的实现。端口与适配器共同构成了南向网关。领域层的领域服务TicketReservation会调用端口,并在运行时将适配器注入到领域服务,以支持机票预订记录的持久化。

菱形对称架构改造了六边形架构与领域驱动设计中设计元素的定义。北向网关以本地网关和远程网关来体现通信机制的差异,端口和适配器仅仅适用于南向网关。北向网关的本地网关和南向网关的端口取代了领域驱动设计元模型中的应用服务与资源库,如此即可消除领域驱动设计社区对这两个概念普遍存在的争议。

菱形对称架构灵活地运用了分层架构的依据与原则,对经典的分层架构进行了调整,引入统一的网关层简化了层次,形成了由内部领域层与外部网关层组成的内外分层架构。菱形的边界即为限界上下文的边界,以“最小完备”的方式实现了业务关注点的纵向切分;内部的领域模型自成一体,以“自我履行”的方式响应外部网关对它的调用;远程网关与本地网关对领域模型的封装,避免了内部的变化对外部的调用者产生影响,满足了限界上下文的“独立进化”能力;端口对外部资源访问的抽象,防止了外部的变化对领域模型的影响,使得限界上下文的内部成为了“稳定空间”。显然,菱形对称架构是对自治单元设计的呼应,能够更好地保证限界上下文具有响应变化的演进能力。

引入上下文映射

菱形对称架构还能够有机地与上下文映射模式结合起来,充分展现了这一架构风格更加适用于领域驱动设计。二者的结合主要体现在北向网关与南向网关对上下文映射模式的借用。

北向网关的演变

对比上下文映射的通信集成模式,我们发现开放主机服务模式的设计目标与菱形对称架构的北向网关完全一致。开放主机服务为限界上下文提供对外公开的一组服务,以便于下游限界上下文方便地调用它。根据限界上下文通信边界的不同,进程内通信调用本地网关,进程间通信调用远程网关,二者都遵循开放主机服务模式,服务接口的消息契约则满足发布语言模式,形成两个限界上下文之间的交换模型。

为了更好地体现上下文映射模式,我们甚至可以将北向网关命名为开放主机服务(OHS)层,远程网关和本地网关分别命名为远程服务和本地服务。

远程服务是为跨进程通信定义的开放主机服务。根据通信协议和消费者的差异,远程服务可分为资源(Resource)服务、供应者(Provider)服务、控制器(Controller)服务与事件订阅者(Event Subscriber)服务。

本地服务是为进程内通信定义的开放主机服务,对应于应用层的应用服务。引入本地服务的价值在于:

  • 对领域模型形成了一层间接的外观层,避免领域模型被泄露在外

  • 对于进程内协作的限界上下文,降低了跨进程调用的通信成本与序列化成本

由于北向网关封装了内部的领域模型,因此远程服务和本地服务的方法定义都不应该包含领域模型对象,而是通过引入发布语言(PL)模式定义消息契约模型。

当远程服务为资源服务或提供者服务时,它们主要面向下游限界上下文或第三方调用者,服务的消息契约模型由请求消息与响应消息组成。

当远程服务为控制器服务时,面向的调用者往往为UI前端,服务的消息契约模型实际为面向前端的展现(Presentation)模型。

当远程服务为事件的订阅者时,毫无疑问,服务的消息契约模型就是事件。

当外部请求从远程服务进入时,必须经由本地服务发起对领域层的调用请求;因此,本地服务需要完成消息契约模型与领域模型之间的转换。有时候,远程服务与它调用的本地服务采用的消息契约模型并不相同,那么远程服务还需要完成两种不同的消息契约模型的转换。

南向网关的演变

防腐层(ACL)作为一种上下文映射模式,主要是为了防止上游限界上下文的变化,其核心思想为控制权的转移:下游团队不再寄希望于上游团队履行契约不变化的承诺,而是在自己的自治边界内定义接口,从而具有了该接口的控制权,如此才有可能守住内部领域模型的稳定性,满足“稳定空间”的自治特性。

如果将防腐层防止腐化的目标从上游限界上下文扩大至当前限界上下文的所有外部环境,包括如数据库、消息队列这样的环境资源,也包括系统外部的伴生系统,那么,防腐层就承担了菱形对称架构南向网关的角色,其中南向网关的端口提供了抽象,并由适配器封装访问外部环境的具体实现。

根据一个限界上下文可能要与之协作的外部环境的不同,南向网关的端口可以分为:

  • 资源库(repository)端口:隔离对外部数据库的访问,对应的适配器提供聚合的持久化能力

  • 客户端(client)端口:隔离对上游限界上下文或第三方服务的访问,对应的适配器提供对服务的调用能力

  • 事件发布者(event publisher)端口:隔离对外部消息队列的访问,对应的适配器提供发布事件消息的能力

  • 其他端口:若限界上下文还需要与其他外部环境,如文件、网络,也可以定义对应的端口

改进的菱形对称架构

当我们将上下文映射模式引入到菱形对称架构后,整个架构的设计元素变得更加简单,各层之间的关系与边界也更加地清晰:

图片

菱形对称架构对领域驱动设计的分层架构做出了调整。遵循整洁架构的精神,作为远程服务调用者的UI展现层视为外部资源被推出了限界上下文的边界之外。菱形对称架构还去掉了应用层和基础设施层的概念,以统一的网关层进行概括,并以北向与南向分别体现了来自不同方向的请求。如此形成的对称结构突出了领域模型的核心作用,更加清晰地体现了业务逻辑、技术功能与外部环境之间的边界。

菱形对称架构还调整了领域驱动设计的元设计模型,将资源库视为南向网关层(防腐层)的端口,资源库的实现就是端口对应的适配器。南向网关中的资源库端口和客户端端口作为领域建模时的角色构造型,巩固了领域模型的稳定性。应用层被去掉之后,弱化为北向网关层(开放主机服务层)的本地服务,相当于从设计层面回归到服务外观的本质,有助于解决应用服务与领域服务之间的概念之争。

遵循菱形对称架构的限界上下文代码模型如下所示:

 

currentcontext
- ohs(northbound)
- remote
- controllers
- resources
- providers
- subscribers
- local
- appservices
- pl(messages)
- domain
- acl(southbound)
- ports
- repositories
- clients
- publishers
- adapters
- repositories
- client
- publishers
- pl(messages)

该代码模型使用了上下文映射的模式名,ohs为开放主机服务模式的缩写,acl是防腐层模式的缩写,pl代表了发布语言。注意,北向网关和南向网关都定义了pl包,其中,北向网关定义的消息契约模型映射了当前开放主机服务的服务接口,南向网关定义的消息契约则映射了与之协作的上游限界上下文开放主机服务的服务接口。如果下游直接重用了上游的消息契约模型(通常指上下游限界上下文处于同一进程),南向网关就可以不用定义。

当然,我们也可以使用北向(northbound或north)与南向(sourthbound或sourth)取代ohs与acl作为包名,使用消息(messages)契约取代pl的包名。这取决于不同团队对这些设计要素的认识。无论如何,作为整个系统的架构师,一旦确定在限界上下文层次运用菱形对称架构,就意味着他向所有团队成员传递了统一的设计元语,且潜在地给出了架构的设计原则与指导思想,即:维持领域模型的清晰边界,隔离业务复杂度与技术复杂度,并将限界上下文之间的协作通信隔离在领域模型之外。

菱形对称架构的价值

当我们为限界上下文引入菱形对称架构之后,一方面可以更加清晰地展现上下文映射模式之间的差异,并凸显了防腐层与开放主机服务的重要性;另一方面,遵循菱形对称架构的领域驱动架构亦具有更好的响应变化的能力。

展现上下文映射模式

让我们以查询订单业务场景来展现菱形对称架构对上下文映射模式的体现。查询订单时,需要获取订单项对应商品的商品信息,即产生订单上下文与商品上下文的协作关系,进而产生两个限界上下文的“模型依赖”。随着设计视角的变化,选择的上下文映射模式也在相应发生变化,菱形对称架构可以清晰地体现这一变化。

作为上游的商品上下文,它的团队总是高高在上,不大愿意理睬下游团队的互换,而下游团队又不愿意抛开上游团队另起炉灶,就会无奈选择遵奉者模式。又或者你认为商品上下文设计的领域模型足够稳定,且具有非常大的重用价值,就可以主动选择共享内核模式。它们的共同特点都是重用上游的领域模型,此时的模型依赖应准确地描述为“领域模型的依赖”。通过菱形对称架构表现为:

图片

上图清晰地展现了重用领域模型的方式会突破菱形对称架构北向网关修筑的堡垒,让商品上下文的领域模型直接暴露在外,下游限界上下文修筑的南向网关防线也如同虚设,因为它被OrderService“完美”地忽略了。

如果订单上下文与商品上下文处于同一进程,根据菱形对称架构的定义,位于下游的订单上下文可以通过其南向网关发起对商品上下文北向网关中本地服务的调用。为了保护领域模型,商品上下文在北向网关中还定义了消息契约模型,表现为:

此时的菱形对称架构体现了防腐层模式开放主机服务模式的共同协作,两边的领域模型互不知情,但为了避免不必要的模型定义,位于下游的订单上下文直接重用了上游定义的消息契约模型类ProductResponse,如此还可减少转换消息模型对象的成本。此时的“模型依赖”可以视为“消息契约模型的依赖”,由于南向网关中的ProductClient端口对调用关系进行了抽象,防腐层的价值仍然存在。

注意:虽然ProductClientAdapter直接重用了上游的ProductResponse类,但在ProductClient端口的接口定义中,却不允许出现上游的消息契约模型。否则,就会让消息契约模型侵入到订单上下文的领域模型中。为此,南向网关客户端端口的接口方法应操作自己的领域模型,然后在适配器的内部实现完成消息契约模型与领域模型的转换。

端口与领域层之间的关系存在稍许的设计争议。如上图所示,领域层的领域服务OrderService依赖了ProductClient端口,从菱形对称架构的内外方向看,产生了内部对外部的依赖,违背了整洁架构思想;同时,ProductClient端口需要操作自己的领域模型,因而又依赖了Order聚合根实体,如此又产生了领域层与端口之间的双向依赖。这一设计看似不同,但由于我们要求端口皆为抽象,则说明它实则符合“依赖倒置原则”,即“高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖于抽象”。适配器是端口的细节,它依赖于端口是合理的;领域层本该依赖于适配器,引入端口抽象后,二者都依赖于端口。

如果订单上下文与商品上下文位于不同进程,它们之间的模型依赖就不存在了,我们需要为它们定义属于自己的模型对象。例如,订单上下文的ProductClientAdapter调用了商品上下文北向网关的外部服务ProductResource,该服务操作的消息契约模型为ProductResponse。为了支持消息反序列化,就需要在订单上下文的南向网关定义与之一致的ProductResponse类

图片

说明:如果这种跨进程调用方式采用了RPC,则远程服务将定义为提供者(Provider)服务。RPC的客户端通过部署在本地的Stub以代理方式发起对远程服务的调用,因此在设计时要遵循接口与实现分离的原则。作为提供者服务接口一部分的消息契约对象,是与抽象的服务接口放在同一个包中,并同时部署在客户端和服务端,相当于下游的限界上下文仍然重用了上游的消息契约模型,与RESTful服务协作产生的模型依赖略有不同。

如上图所示,通过在各自限界上下文内部定义各自的消息契约模型,彻底解除了两个限界上下文之间的“模型依赖”,并通过南向网关防腐层与北向网关开放主机服务降低了彼此的耦合,但并不意味着它们是彻底解耦的。即使各自定义消息契约模型,斩断了因为重用引起的依赖链条,它们仍然存在隐含的逻辑概念映射关系,真可以说是藕断丝连,并存在变化的级联反应。

譬如说,国家政策要求电商平台销售的所有商品都需要添加一个“是否绿色环保”的新属性。为此,商品上下文领域层的Product类新增了isGreen()属性,北向网关层定义的ProductResponse类也需随之调整。这一知识的变更也会传递到下游的订单上下文。通过对上游的开放主机服务进行版本管理,或在下游引入防腐层进行隔离保护,一定程度可维持订单领域的稳定性;但是,如果需求要求获得的商品信息同样呈现“是否绿色环保”的属性,就必须修改订单上下文中ProductResponse类的定义了。

若真正体会了限界上下文作为知识语境的业务边界特征,就可以将订单包含的商品信息视为订单上下文的领域模型,隐含的统一语言为“已购商品(Purchased Product)”,它与商品上下文的商品属于不同的领域概念,处于不同的业务边界,然后共享同一个productId

在订单上下文中,Product作为Order聚合的组成部分,它的生命周期与Order的生命周期绑定在一起,统一由OrderRepository管理。这意味着在保存订单时,业已保存了与订单相关的商品信息,在获取订单及其商品信息时,自然就无需求助于商品上下文。此时,查询订单的业务场景就不会带来二者之间的协作关系,形成了分离方式的上下文映射模式:

图片

或许有人会提出疑问:由于订单上下文的商品信息仅含订单需要的商品基本信息,若需获取更多商品信息,是否意味着订单上下文需要向商品上下文发起请求呢?其实不然,因为这一请求并非订单上下文发起,而是客户通过在前端点击商品的“查看详情”按钮发起调用。由于页面已经包含了productId的值,前端可直接向商品上下文的远程服务ProductController发起调用请求,与订单上下文无关。

上述三种场景以及对上下文映射模式的运用,实则完整地模拟了模型依赖的三种形式:

  1. 无模型依赖:上下文映射采用分离方式模式,限界上下文根据自己的知识语境定义自己的领域模型,从而解除了对上游的模型依赖关系。

  2. 领域模型依赖:上下文映射采用共享内核或遵奉者模式,下游上下文的领域模型直接重用上游上下文的领域模型。

  3. 消息契约模型依赖:上下文映射采用防腐层与开放主机服务模式的结合,下游上下文可以直接重用上游上下文的消息契约模型,也可以各自定义,但在逻辑概念上仍然存在依赖关系。

第一种形式是最佳的应对方案,真正展现了限界上下文的价值,即对领域模型的控制力。当限界上下文针对业务关注点进行垂直切割时,不仅要从语义相关性划分领域概念,还要考虑这些概念之间存在的功能相关性。

例如,商品与商品上下文是语义强相关的,但在查询订单这一领域场景中,获得订单时随之返回对应的商品信息,却是功能相关性发挥着作用。它也充分体现了“最小完备”的自治特性,因为在查询订单时,如果不为订单项提供对应的商品信息,该限界上下文就是不完备的。这种设计方式也体现了识别限界上下文边界的一种特征:定义领域模型时,如果一些领域概念存在矛盾或冲突,就是引入限界上下文维护概念一致性的时机,也是统一语言发挥作用的时候。

选择分离方式建立领域模型,会否引入数据冗余与同步的问题呢?这需要针对具体的业务场景进行分析:

  • 数据存于一处,领域模型存在业务边界,但数据模型建立了关联关系。例如,订单上下文与商品上下文的领域模型都定义了Product类,但数据库只有一张商品数据表。OrderRepository在管理Order聚合的生命周期时,同时管理该聚合内部Product实体的生命周期。虽然领域概念分属不同的限界上下文,但OrderRepositoryAdapter操作的商品表就是商品上下文ProductRepositoryAdapter操作的商品表,只是操作的数据列存在差异。这种形式实现了分离的领域模型与统一的数据模型之间的映射。

  • 数据按照不同的业务边界通过分库或者分表分散存储,之间用相同的ID保持关联。如此既维护了领域的边界,也维护了数据的边界。这一设计突出了限界上下文的边界保护作用,更易于从单体架构向微服务架构迁移。该设计对业务边界的要求更高,对属性定义也更为苛刻,它不允许在不同的业务边界中出现相同业务含义的属性,否则就会导致数据冗余,进而带来数据同步的问题。这就要求各个限界上下文的领域模型与对应的数据模型都是正交的,如此也能避免领域模型定义的重复。例如,订单上下文内部的Product类对应了订单库中的商品表,商品上下文内部的Product类对应了商品库中的商品表,它们之间通过ProductId来保证商品的唯一性。如果两个上下文的Product类都定义了name属性,就意味着两个商品表存在相同的name数据列,当一个表的name值进行了修改,就必须同步该修改到另一个表。采取分离方式时,不允许出现这一情形,就要守住限界上下文的领域模型边界,例如订单上下文的Product类就不应该定义name属性,如果需要获取商品名,应通过商品上下文获得。这一设计遵循了自治单元的“最小完备”性。可以认为,自治的限界上下文是采用分离方式的基础

  • 数据虽然看似存在冗余,实则在数据进入各自的限界上下文边界后,已经割裂开了彼此之间的关系,不再依据相同的变化原因。例如,订单上下文定义的Product类包含了price属性,商品上下文的Product类自然也拥有该属性。这是否意味着数据冗余呢?也就是说,当商品上下文的商品价格被修改后,需要同步保存在订单中的商品价格吗?事实并非如此!客户在提交订单后,订单包含的商品价格就与商品上下文脱离了关系,被订单上下文单独管理。事实也是如此,当客户提交订单的那一刹那,已购商品的价格就被冻结了,之后产生的任何调价行为或促销行为,都不会作用于一个已经提交的订单。

第二种形式倾心于“重用”的价值,尝试于满足DRY原则,力图保证整个系统只有一处表达领域概念的知识。当领域概念发生变化时,就能保证只修改这一处,避免了霰弹式修改(Shotgun Surgery)。

然而,在领域驱动设计中,DRY原则的适用范围应为限界上下文,即在一个限界上下文中考虑消除重复,因为过度强调重用会带来依赖的代价。我们无法预知未来的变化,在做出重用领域模型的决定时,却取决于当前;一旦两个限界上下文对相同的领域模型产生了不同的需求,变化的方向就会变得不一致,于是乎,修改开始发散。谁都希望重用它,重用的理由却各不相同,使得它承担的职责越来越多,导致引起它变化的原因也越来越多,慢慢就会沦为低内聚的领域模型对象。设计从最初对霰弹式修改的规避,走向了另一个极端,产生了发散式变化(Divergent Change)。

相较于重复,限界上下文之间的高度耦合更是不可原谅的缺点。因此,在两个或多个限界上下文之间重用领域模型是一种危险的选择。下游限界上下文缺少南向网关防腐层的隔离,使得它无法抵御外界的影响,违背了“稳定空间”的自治特性;上游限界上下文缺少北向网关开放主机服务的统一接口定义,使得它轻易地将领域模型的变化传递给外界,违背了“独立进化”的自治特性。

“取法乎上,仅得乎中”!虽然我们需竭力维持限界上下文的业务边界,但在领域模型的归属依旧蒙昧未明时,将分离方式妥协为对消息契约模型的依赖,不失为一条更具平衡特征的中策。它既能有效地维持限界上下文的领域模型边界,又能降低上下文映射带来的依赖强度。菱形对称架构在这一形式下发挥了有效隔离的价值,虽然付出了重复定义与模型转换的成本,换来的却是限界上下文独立演化的自由,这事实上正是菱形对称架构体现出来的响应变化的能力。

响应变化的能力

限界上下文之间产生协作时,通过菱形对称架构可以更好地响应协作关系的变化。它设定了一个基本原则:即下游限界上下文需要通过南向网关与上游限界上下文的北向网关进行协作,简言之,就是防腐层与开放主机服务的协作。这是两种通信集成模式的融合,当这一方式运用到两种团队协作模式上时,既能促进上下游团队之间的合作,又能保证各个团队相对的独立性。这两种团队协作模式就是:

  • 客户方/供应方模式

  • 发布者/订阅者模式

客户方/供应方模式是采用同步通信实现上下游团队协作的模式,参与协作的角色包括:

  • 下游客户方:防腐层的客户端端口作为适配上游服务的接口,客户端适配器封装对上游服务的调用逻辑

  • 上游供应方:开放主机服务的远程服务与本地服务为下游限界上下文提供具有业务价值的服务

客户方的适配器到底该调用供应方的远程服务还是本地服务,取决于这两个限界上下文的通信边界。

如果客户方与供应方位于同一个进程边界内,客户方的适配器就可以直接调用本地服务。对本地服务的调用发生在同一个进程中,通信机制更加健壮,更加可控,无需分布式通信的网络传输成本,也省掉了消息协议的序列化成本。供应方的本地服务作为北向网关,同样提供了对领域模型的保护,确保了领域模型的独立性和完整性。协作图如下所示:

图片

如果客户方与供应方处于不同的进程边界,就由远程服务来响应客户方适配器发起的调用。根据通信协议与序列化机制的不同,可以选择资源服务或供应者服务作为远程服务来响应这样的分布式调用。远程服务在收到客户端请求后,会通过本地服务将请求最终传递给领域层的领域模型。协作图如下所示:

虽然Eric Evans并未要求为每个参与协作的限界上下文都定义防腐层与开放主机服务,但菱形对称架构扩大了防腐层与开放主机服务的外延,使得防腐层与开放主机服务之间的协作成为了客户方/供应方映射模式的标准通信模式。南向网关的防腐层保证了端口与适配器的分离,解除了对供应方开放主机服务的强耦合;开放主机服务提供的本地服务与远程服务,允许客户方与供应方的协作能够相对自如地在进程内通信与进程间通信之间完成切换,自然就可以相对轻松地将一个系统从单体架构风格迁移到微服务架构风格。

发布者/订阅者模式是采用异步通信实现上下游团队协作的模式,参与协作的角色包括:

  • 上游发布者:防腐层的发布者端口负责发布事件,它并不需要关心下游订阅者会如何消费事件,但需要就事件契约与下游团队沟通达成一致

  • 下游订阅者:开放主机服务的订阅者远程服务需要侦听事件总线,获取发布者发布的事件,然后将事件传递给本地服务,由本地服务对事件进行处理

注意发布者/订阅者模式的上下游关系与参与协作的网关方向和客户方/供应方完全不同。发布者虽然是上游,却由南向网关的防腐层负责发布事件;订阅者虽然是下游,却由北向网关的开放主机服务负责订阅事件,进而处理事件。因为领域驱动设计定义的上下游关系,是因为上游的变动会影响下游,上游的知识会传递给下游。在发布者/订阅者模式中,二者之间的耦合主要来自对事件的定义。因此,当发布者修改了事件后,会影响到订阅者,发布者传递给下游的知识,其实就是事件本身。

当两个团队分别为事件的发布者与订阅者时,它们之间往往通过引入事件总线作为中介来维持彼此的通信。因此,在各自的限界上下文内部,都需要隔离领域模型与事件总线的通信机制。采用菱形对称模型,即可通过网关层的设计元素来实现这种隔离。事件的发布者位于防腐层,发布者端口提供抽象定义,发布者适配器负责将事件发布给事件总线;事件的订阅者属于开放主机服务层的远程服务,在订阅到事件之后,交由本地服务来处理事件。

我们还需要判断是谁引起了事件的发布?

如果事件由本地服务发布,考虑到本地服务实际就是领域驱动设计中的应用服务,可以将该事件命名为“应用事件(Application Event)”。既然由北向网关的本地服务发布事件,它的触发者就有可能是当前限界上下文外部的客户端调用,例如前端UI发起对远程服务的调用,然后委派给了本地服务。在本地服务调用领域层执行完整个用例的领域逻辑后,组装好待发布的应用事件,通过调用南向网关的发布者端口,由注入的发布者适配器最终完成事件的发布。整个调用时序如下所示:

图片

如果是领域层的领域模型对象在执行某一个领域行为时发布了事件,该事件就为“领域事件(Domain Event)”。由于发布事件需要与外部的事件总线协作,就需要调用南向网关的发布者端口。为了保证领域模型中聚合的纯粹性,就应该由领域模型的领域服务调用发布者端口发布领域事件。调用时序如下所示:

图片

引入事件总线的发布者/订阅者模式具有松散耦合的特点,在结合了防腐层与开放主机服务之后,领域模型并不依赖发布事件与订阅事件的实现机制,意味着它们对事件总线的依赖也能够降到最低。只要通过积极的团队协作,定义符合上下游共同目标的事件,它们就具有了非常强的响应业务变化的能力。

无论是客户方/发布方模式,还是发布者/订阅者模式,菱形对称架构都能够将上游的变化产生的影响降到最低。一个自治的限界上下文,需要菱形对称架构来保证。由采用菱形对称架构的限界上下文组成的业务系统,既有高内聚的领域内核,又有松耦合的协作空间,就能更好地响应变化,使得系统具有更强的架构演进能力。

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值