不同团队落地DDD所采取的应用架构风格可能不同,并没有统一的、标准的DDD工程架构。有些团队可能遵循经典的DDD四层架构,或改进的DDD四层架构,有些团队可能综合考虑分层架构、整洁架构、六边形架构等多种架构风格,有些在实践中可能引入CQRS解决读模型与写模型的差异化等等。即使无法制定通用的、标准的工程应用架构,但为团队制定一个遵循领域驱动设计思想的参考架构依然有价值。基于以下原因:
虽然无法制定完全通用的DDD参考架构,但制定某个特定上下文下的参考架构却具有可行性和实践价值。针对于上下文的选择要尽量贴合实际的工程实践场景并考虑多维度的因素。
本文所述参考工程架构遵循以下原则:
希望工程参考架构能涵盖以下范围
基于以上原则,参考工程考虑单个应用内多上下文的场景,以期在模块化和服务粒度及成本间进行权衡折衷。应用架构对多上下文的支持的逻辑示意图如下所示,在解决方案域对限界上下文进行识别和划分之后,基于其业务内聚性和关联性,把多个上下文是现在某单个工程应用中。单个应用内的多个限界上下文间可能存在交互,交互的形式可以是基于事件驱动,也可以是基于进程内调用。采用事件驱动的方式上下文间的耦合性对低一些,但一般需要引入事件总线的支持,额外组件的引入必然会导致复杂性的上升。进程内调用则耦合性会高一些,但从实现角度复杂度会低一些。具体选择哪种方式开发人员可以基于实际情况进行权衡。
需要再次说明的是,这种应用架构决策是一种多因素的权衡,可能与我们所了解的子域与限界上下文1:1的理想化实践不一致。但从特定的架构决策上下文来整体考量依然具有实际应用价值。
从上图的逻辑示意图我们再深入一层,从分层的维度去剖析一下详细的应用架构的展现形式,如下图所示:
客户端
客户端与应用处于不同的进程,是应用能力的消费端,在实际项目中可能是APP端、PC端、小程序端、公众号端或三方的业务调用端等等。
接入层
接入层是外部系统与应用内部业务能力的中间层,接入层是应用层对外的门面,是当前应用对外暴露业务能力的入口。该层的组成可能是对外提供的HTTP接口声明、分布式定时任务调度、消息监听器、RPC服务等等。其重要职责包括对外部系统的请求进行基础的参数校验、入参适配和服务路由(转发至系一层的应用服务)以及响应数据的适配。
业务层:
该层是应用的业务逻辑所在层,整个架构风格采用模块化单体风格,在该层不同的限界上下文体现为不同的模块。在每个限界上下文内采用分层架构,独立划分为应用层、领域层和网关层。
应用层:
协调领域对象、领域服务或外部依赖服务完成业务用例,该层只做能力协调,不处理任何领域逻辑。
领域层:
领域层是整个分层的核心,与技术实现无关,主要负责领域模型、领域事件、领域服务定义,以及业务相关外部服务的接口抽象以及仓库的接口抽象等。
领域层与应用服务的本质区别是:应用层不包含领域逻辑,领域逻辑全部下沉到领域层实现。
网关层:
网关层定位是应用的出口网关,是应用与外部基础设施交互的适配层,处理所有技术相关实现。
该组件的命名有多种方式,比如有些团队将其命名为 “rpc”,也有些团队将其命名为 “infrastructure”,不同的命名体现了团队对其背后所表达的隐喻的决策选择。在本文的参考架构中选择了 网关-Gateway这一命名,决策原因是:限界上下文自身是高内聚的,其与外部的交互需要统一出口,Gateway所表达的网关的含义恰当的表现了这种统一出口的理念。如果Facade层是应用的北向网关,则此时的Gateway则表达的是限界上下文的南向网关。
从宏观的分层我们再深入一层看下每层的组件划分。如下图所示:
Start组件:
整个应用的启动入口、加载应用配置信息等等。
Common组件
提供在不同的限界上下文间复用的领域模型元素的抽象,比如对Command、Query、Event、Entity、ValueObjec通用抽象t等。当然,领域模型的通用抽象不是必须在Common组件内以提供复用,也可以作为一个独立的限界上下文,并以共享内核方式与其它上下文进行共享,或者也可以实现为独立的jar包组件。
API 组件
RPC类型服务的接口声明组件,以公司内部使用的JSF为例,该组件是应用对外部系统暴露的JSF API的组件。该组件可以是独立的工程,当然,有些团队会将其作为一个Module放入应用工程中。
统一门面组件:Facade
外部客户端触达应用系统的入口,也是内部应用服务的统一门面,类似于六边形架构风格下的适配器。参考架构中基于不同场景划分为 provider(RPC服务)、task(定时任务)、listener(MQ监听)、rest(http接口)等几个子包。外部请求进入系统后,由Facade组件完成入参基本校验、入参转换、服务路由以及出参转换等操作。同时,还可以承担处理登录态、鉴权以及日志等相关能力。
应用服务组件:Application Service
应用服务代表着用例以及系统行为,其通过委托到领域层和基础设施层(参考架构中的Gateway组件)完成用例的应用逻辑逻辑处理,可以理解为应用服务是领域层的客户端。该组件典型的职责:
External API
应用服务的逻辑不仅仅需要协调领域层,有时还需要依赖于外部的三方服务。External API 组件负责对这些外部服务进行接口声明定义,不做具体实现。
应用服务组件不直接依赖这些外部服务实现,而是依赖其接口抽象。同时,此处的模型定义是基于该限界上下文的语义,是一种对外部模型的适配。
该组件不依赖其它组件,且仅被应用服务组件和Gateway组件依赖。网关组件依赖External API 组件的接口声明并提供底层技术实现,应用服务组件依赖其接口,并通过IOC方式将具体实现注入完成服务调用。
注意,该组件所依赖的服务不涉及领域概念,只是用于支撑应用服务的编排。如果是涉及了领域逻辑,则对外部服务依赖的接口定义需要下沉到Domain层。
Query
Query组件解决领域相关的报表查询场景,在限界上下文内作为与应用服务对等的组件存在,两个组件分别负责业务的查询和命令逻辑。
虽然引入了Query组件对报表场景提供支持,但没有完全的引入CQRS这一模式。在很多资料中CQRS与DDD同时提及的概率比较高,因为,在DDD下我们解决了复杂的面向领域的写侧模型,但在报表场景下,这种富领域模型有可能并不是最佳选择。如果读侧和写侧都基于统一的领域模型,一般会导致领域模型的折衷设计。为了满足查询侧诉求,领域模型不得不引入额外的、领域无关的属性,由此造成领域模型的污染。
Domain
Domain组件是领域逻辑核心,承担整个系统领域逻辑的实现,其定义了领域模型、领域服务、领域事件以及仓储层的抽象。该组件不依赖其它组件(除了通用的领域模型抽象组件Common之外。
上图所体现的参考架构使用了DDD的战术设计的经典建模元素,比如聚合、实体、值对象、仓储、工厂以及领域事件等。在实际落地过程中,这些设计元素的抽象具有一定的挑战,设计过程中需要经过不断分析、权衡和重构以完成建模,这正是核心设计所在。
Gateway
网关层(有些架构下可能成为基础设施层Infrastructure)承担整个技术相关性的实现,是内部应用的出口网关。
技术相关性是网关组件区别于其它组件的根本特性。在该组件内要处理技术实现的所有细节,比如与外部服务、中间件、DB的交互等。同时,其与Domain组件以及External API组件的接口抽象协作,共同承担了系统与外部依赖间(包括外部服务以及应用依赖的中间件、DB等基础设施)的防腐层职能,负责内部模型到外部模型转化、外部模型到内部模型转化以及具体交互。基于网关组件的特性,也非常适合在该层做统一的外部服务数据缓存及降级熔断处理。
网关层依赖于Domain、Application Service、External API和Query组件,负责上述四个组件定义的接口实现。在Gateway组件通过子包对实现进行隔离:
应用架构模式是架构的重要维度之一,结构不仅仅是简单的包结构和命名,其传达的是一种顶层抽象,背后包含了大量的实践和知识。制定符合团队情况的工程参考架构,并在团队成员间达成共识非常重要。领域驱动设计并没有统一的、通用的架构,试图定义标准架构是不切实际的。本文描述的工程架构只是一个参考,实践过程中应该基于团队特定情况而有所差异,但原则上都应该遵循业务域与技术域分离的核心理念。
关注微信公众号:系统工程实验室,获取更多资料