微服务之软件架构层次设计分析
在实施领域驱动设计的过程中,限界上下文扮演了关键角色:它既是维护领域模型完整性与一致性的重要边界,又是系统架构的重要组成部分。随着社区对限界上下文的重视,越来越多的人开始尝试将更多的架构实践与限界上下文融合在一起,创造出符合领域驱动设计的架构模式。本文对传统分层架构、六边形架构、DDD经典分层架构以及对称菱形架构进行归纳分析。
一、传统分层架构(三层)缺陷
传统的分层架构具有广泛的应用,例如经典的三层架构,把系统分为表示层、业务逻辑层、数据访问层。在Martin Fowler的《企业应用架构模式》一书中做过深入阐述,本书04年出版,时至今日分层架构仍然是常用的设计方法,分层架构可以降低耦合、提高复用、分而治之,但同时也还是存在一些问题:
应用逻辑在不同层泄露,导致替换某一层变得困难、难以对核心逻辑完整测试(你是否有过困惑,代码到底应该放在哪个层,虽然定义了各层的职责,但是总有人不严格遵循层次的分界,对于三层架构,常常会出现业务逻辑写在了表示层,或者业务逻辑直接和数据访问绑定)
传统的分层架构是一维的结构,有时应用不光是上下的依赖,可能是多维的依赖,这时一维的结构就无法适应了。
其实主要还是传统的分层架构对于业务逻辑层的划分过于粗糙,针对特定的复杂场景,尤其是涉及多场景依赖的情况是很难控制层次的持久清晰划分。
二、DDD经典分层架构
DDD经典分层架构与传统的分层架构设计本质是一样的,都是通过层(Layer)来隔离不同的关注点(Concern Point),以此应对不同需求的变化,使得这种变化可以独立进行。
用户界面层: 负责向用户展现信息和解释用户命令。包含 web 端 UI 界面、移 动端 UI 界面、第三方服务等。
应用层: 很薄的一层,用来协调应用的活动,它不包含业务逻辑,不保留业务 对象的状态。在领域设计中,它其实是一个外观(Facade),一般是供其他限界上下文基础设置层中 controller 调用。
领域层: 本层包含领域的信息,是业务软件的核心所在。在这里保留业务对象的状态,对业务对象状态的持久化被委托给基础设置层。它包含领域设计中的:聚合、值对象、实体、服务、资源接口等。
基础设置层: 不要简单的理解为物理上的一层,它作为其他层的支撑库而存在。它提供了层间的通讯,实现对业务对象的持久化。一般包含:网络通讯、 资源实现(数据库持久化实现)、异步消息服务、网关、controller 控制等。
三、整洁架构
整洁架构(Clean Architecture)是由Bob大叔在2012年提出的一个架构模型,顾名思义,是为了使架构更简洁。整洁架构的模型是一个类似内核模式的内外层结构,它具有以下特点:
越靠近中心,层次越高。
由外到内:框架与驱动程序 --》接口适配器 --》应用级业务逻辑--》系统级业务逻辑。
源码中依赖关系必须指向同心圆内层,从外到内。即,外层依赖内存然而内存不能依赖外层。
在这个架构中,外层圆代表的是机制,内层圆代表的是策略;机制和具体的技术实现有关,容易受到外部环境变化;策略与业务有关,封装了最核心领域模型,最不容易受到外界环境变化的影响。
(从不同维度去对比层次特点)
四、六边形架构
六边形架构是Alistair Cockburn在2005年提出,解决了传统的分层架构所带来的问题,实际上它也是一种分层架构,只不过不是上下或左右,而是变成了内部和外部。六边形架构的六边并不重要,六边只是为了留足空间放置端口和适配器以及用六边形接入多个外部系统视觉上最简洁美观。在领域驱动设计(DDD)和微服务架构中都出现了六边形架构的身影,在《实现领域驱动设计》一书中,作者将六边形架构应用到领域驱动设计的实现,六边形的内部代表了application和domain层,而在Chris Richardson对微服务架构模式的定义中,每个微服务使用六边形架构设计,足见六边形架构的重要性。
解决传统分层架构问题
六边形架构实现了业务逻辑以一种松耦合的形式与多个外部系统通过“适配器-端口”的形式进行集成。因此,六边形架构又称为端口-适配器,这个名字更容理解。六边形架构将系统分为内部(内部六边形)和外部,内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。这样就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动,并且,可以在与实际运行的设备和数据库相隔离的情况下开发和测试。
上图六边形架构结构图,其中黑箭头为调用关系,白箭头为实现关系。右侧Message为相关消息机制,多用于微服务架构中多个微服务(六边形架构)之间通信。六边形架构的内部(业务逻辑)与外部(APP,WEB,数据库等)完全隔离,只通过adapter适配器进行交互实现了业务逻辑层与持久层的完全解耦,更一步实现“高内聚低耦合”。
核心部件
Inbound adapter 入站适配器:直接对外提供统一的接入协议,适配器可以根据不同的业务规则甚至是数据格式进行定义,其主要功能是为了转换成内部入站端口所需要的统一协议(也可以理解为统一的数据格式)。
Inbound port 入站端口:入站端口即对外提供服务所暴露的API接口,通常会以PRC的provider形式注册,但端口服务不直接与外部系统交互,还需要经过入站适配器进行转换。一个端口对应多个适配器,是对一类外部系统的归纳,它体现了对外部的抽象。应用通过端口为外界提供服务,这些端口需要被良好的设计和测试。内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换的(一般端口数不会超过4个)。
Outbound port 出站端口:为系统获取外部服务提供支持,如获取持久化状态、对结果进行持久化(要求对数据库增删改查操作的接口)。
Outbound adapter 出站适配器:将内部统一协议转换成外部系统所能识别的特定协议。并且在此实现出站端口的处理逻辑。
Business logic 业务逻辑:可以理解为DDD中的核心域,主要特征是具备抽象稳定的业务实现。六边形架构有一个明确的关注点,从一开始就强调把重心放在业务逻辑上,外部的适配器和端口存在可变性、可替换性,依赖具体技术细节的特征,而业务逻辑相对更加稳定,体现应用的核心价值,需要被详尽的测试。
依赖倒置
六边形架构必须遵循如下规则:内部相关的代码不能泄露到外部。所谓的泄露是指不能出现内部依赖外部的情况,只能外部依赖内部,这样才能保证外部是可以替换的。对于入站适配器,就是外部依赖内部属于主动驱动。但是对于出站适配器,实际是内部依赖外部,这时需要使用依赖倒置,由入站适配器将出站适配器注入到应用内部,这时端口的定义在应用内部,但是实现是由适配器实现。
这里需要明确两个概念
入站和出站:是按照应用服务调用流向说的,外部服务想调用业务逻辑要先经过入站适配器和入站端口。而内部逻辑需要外部数据支撑的时候,是先要通过出站端口调用出站适配器获取结果。
主动驱动和被动驱动:是按照端口的实现位置说的,主动驱动是在内部的业务逻辑处实现端口逻辑的。而被动驱动是在外部适配器里实现端口逻辑的。
调用关系及代码模块映射
以一个经典前端访问流程为例,体现六边形架构的调用链路:
根据六边形的每个部件核心价值可以将映射到下面的代码结构中,这是单一应用的参考结构,也可以根据实际的系统结构套用到微服务中,实际理念是一样的。
五、菱形架构
六边形架构仅仅区分了内外边界,提炼了端口与适配器角色,并没有规划限界上下文内部各个层次与各个对象之间的关系;而整洁架构又过于通用,提炼的是企业系统架构设计的基本规则与主题。因此,当我们将六边形架构与整洁架构思想引入到领域驱动设计的限界上下文时,还需要引入分层架构给出更为细致的设计指导,即确定层、模块与角色构造型之间的关系。而菱形对称架构在这方面更加便于理解和使用。
菱形对称架构模式脱胎于六边形架构与分层架构,它以领域为核心对限界上下文的关注点进行划分,建立了由内部领域模型与外部网关组成的内外分层架构,以菱形的对称结构清晰展现了限界上下文的内部结构,指导着限界上下文的协作关系。
其实菱形对称架构是解决了六边形架构中出口端口和出口适配器对于整个系统的调用出、入不对称的问题(因为出口端口在内存,出口适配器在外层实现了主要逻辑,也就是依赖倒置的实现对于整个调用链路来说是不对称的)。菱形对称架构将端口和适配器组合成一个“网关”,定义了南向网关和北向网关,用于体现调用方向的对称性。
核心部件
北向网关:北向网关定义的远程网关与本地网关同时承担了端口与适配器的职责,这实际上改变了六边形架构端口-适配器的风格;
远程网关:负责进程间通信。主要包含资源(Resource)服务、供应者 (Provider)服务、控制器(Controller)服务与事件订阅者(Event Subscriber)服务。
本地网关:负责进程内通信。当外部请求从远程服务进入时,如果需要调用领域层的领域逻辑,则必须经由本地服务发起对领域层的请求。此时的本地服务又扮演了端口的作用,可认为远程服务是本地服务的客户端。
南向网关:南向网关引入了抽象的端口来隔离内部领域模型对外部环境的访问,等同于上下文映射中的防腐层(ACL),同样它也扩大了防腐层的功能;南向网关的端口抽象负责各种资源库操作的抽象接口,可以被领域层依赖,从使用场景特性上又可分为:
资源库(repository)端口: 隔离对外部数据库的访问,对应的适配器提供聚 合的持久化能力。
客户端(client)端口: 隔离对上游限界上下文或第三方服务的访问,对应的 适配器提供对服务的调用能力。
事件发布者(event publisher)端口: 隔离对外部消息队列的访问,对应适 配器提供发布事件消息的能力。
适配器:负责网关端口的实现,运行时通过依赖注入将适配器实现注入到领域层。
调用链路
进程内通信:如果上下游的限界上下文位于同一个进程边界内,客户端适配器可以直接调用本地服务。
进程间通信:如果上下游的限界上下文处于不同的进程边界,就由远程服务来响应下游客户端适配器发起的调用。
代码结构
通用的代码模型实例:ohs 为开放主机服务模式的缩写,acl 是防腐层模式的缩写,pl 代表了发布语言;也可以使用北向(northbound)与南向(sourthbound)取代 ohs 与acl 作为包名,使用消息(messages)契约取代pl的包名。
支付工具系统真实案例:
详细项目介绍可参考下图: