写在前面:
DDD的一大好处便是它并不需要使用特定的架构。我们可以在整个系统中使用多种风格的架构。有些架构包围着领域模型,能够全局性地影响系统,而有些架构则满足了某些特定的需求。我们的目标是选择适合于自己的架构和架构模式。
在选择架构风格和架构模式时,我们应该将软件质量考虑在内,必须对每种架构做出正确的评估。对架构风格和模式的选择受到功能需求的限制,换句话说,在没有功能需求的情况下,我们是不能对软件质量做出评判的,亦不能做出正确的架构选择。这也说明用例驱动架构在当今的软件开发中依然适用。
传统分层架构
分层架构原则:
每一层只能与直接位于其下方的层发生耦合。这确保了各层之间的独立性。
分层架构也分为两种:
- 严格分层架构:在严格分层架构中,每一层只能与其下方的层发生耦合,以确保更严格的分离。
- 松散分层架构:相比之下,松散分层架构允许任何上方层与下方层发生耦合,通常用于用户界面层和应用服务需要与基础设施互动的系统。
用户界面层:
主要责任是处理用户显示和请求,而不应包含领域或业务逻辑。(用户界面需要对数据进行验证,但和领域内的验证是不同的。)
应用服务(Application Services):
- 作用:可以处理持久化事务、安全认证、事件通知、邮件发送等任务。接收来自用户界面的输入参数,再通过资源库获取到聚合实例,然后执行相应的命令操作,
- 对接领域模型:应用服务是领域模型的直接客户,用于协调对领域对象的操作,比如聚合(数据聚合、业务操作聚合)。
基于DIP的分层
依赖倒置(DependencyInversionPrinciple)定义:
高层模块不应该依赖于低层模块,两者都应该依赖于柚象。
抽象不应该依赖于细节,细节应该依赖于抽象。
根据该定义,低层服务(比如基础设施层)应该依赖于高层组件(比如用户界面层、应用层和领域层)所提供的接口。因此架构演变为:
当我们在分层架构中采用依赖倒置原则时,我们可能会发现,事实上已经不存在分层的概念了。无论是高层还是低层,它们都只依赖于抽象,好像把整个分层架构给推平了一样。
六边形架构
六边形架构 = 端口与适配器 = Onion架构。DDD首选架构
该架构中存在两个区域,分别是“外部区域”和“内部区域”.
- 适配器A\B\C使用了HTTP协议来输入,适配器D使用AMQP(如kafka)来输入。
- 在外部区域中,不同的客户均可以提交输入;
- 而内部的系统,则用于获取持久化数据,并对程序输出进行存储(比如数据库),或者在中途将输出到其他系统(如消息)
- 适配器E、F、G使用关系型数据库、基于文档的存储、分布式缓存和内存存储输出数据;适配器H处理消息输出。
如何理解端口、适配器?
- 可以将端口想成是HTTP,而将适配器想成是Java的Servlet 、 JAX-RS的REST、Spring-RESTful Web请求处理类。
- 为ServiceBus或RabbitMQ创建消息监听器,在这种情况下,端口是消息机制,而适配器则是消息监听器,因为消息监听器将负责从消息中提取数据,并将数据转化为应用层API所需的参数。
REST 架构
REST作为一种架构风格
架构风格将不同架构实现所共有的东西抽象出来,使得我们在谈及架构时不至于陷入到技术细节中。
分布式系统架构存在着多种架构风格,包括客户端-服务器架构风格和分布式对象风格。而REST就应该是属于Web架构的一种架构风格。
REST和DDD
不建议将领域模型直接暴露给外界,因为这样会使得系统变得脆弱,原因在于对领域模型的每次改变都会导致对系统接口的改变。将两种方式结合有两种方法。
-
第一种方法是为系统接口层单独创建一个界限上下文,再在此上下文中通过适当的策略来访问实际的核心模型。它将系统看做一个整体,通过资源抽象将系统功能暴露给外界,而不是通过服务或者远程接口。
- 这种方法应该被优先考虑,因为它在核心域和系统接口模型之间完成了解耦,使得我们可以先做修改,再决定哪些修改应该反应到系统接口模型上。
-
另一种方法用于需要使用标准媒体类型的时候。我们可以创建一个领域模型来处理每一种媒体类型,并且能在服务器和客户端之间进行重用。
为什么是REST?
符合REST原则的系统具有更好的 松耦合性和可伸缩性,基于REST的系统被分为很多较小的资源块,每一个资源块都可以独立地测试和调试,并且每一个资源块都表示了一个可重用的入口点。HTTP设计本身以及URI成熟的重写与缓存机制使得RESTful HTTP成为一种不错的架构。
CQRS架构
什么是CQRS?
CQRS(Command Query Responsibility Segregation),Command 与 Query 分离的一种模式。
- Command:命令则是对会引起数据发生变化操作的总称,即新增,更新,删除这些操作,都是命令
- Query:查询则不会对数据产生变化的操作,只是按照某些条件查找数据
CQRS 的核心思想是将这两类不同的操作进行分离,可以是两个独立的应用,两个不同的数据源,也可以是同一个应用内的不同接口上。
引入的目的
一个业务系统会存在各种查询功能,但没有业务逻辑(或者很少),如果按照常规的领域分层会导致大量的模型转换工作,并且存在大量的分页信息(pageSize\pageNo\totalPage等)无处安放,因为它们并不属于Domain内的属性或行为,如果算Domain里的属性,会导致Domain越来越大。
因此,CQRS旨在解决领域驱动中,数据显示复杂性问题。说白了,就是“数据查询”和“业务操作”分离。
架构图:
CQRS 在 DDD 中是一种常常被提及的模式,它的用途在于将领域模型与查询功能进行分离,让一些复杂的查询摆脱领域模型的限制,以更为简单的 DTO 形式展现查询结果。同时分离了不同的数据存储结构,让开发者按照查询的功能与要求更加自由的选择数据存储引擎。
事件驱动架构
事件驱动架构(Event-Driven Architecture,EDA)是一种用于处理事件的生成、发现和处理等任务的软件架构。
一个系统的输出端口所发出的领域事件,被发送到另一个系统的输入端口,此后输入端口事件的订阅方将对事件进行处理。
在一个界限上下文处理某个事件时,应用程序API将采用该事件模型中的属性值来执行响应的操作。应用程序API所执行的命令操作将反映到命令模型中。
长时处理过程
设计长时处理过程有三种方法:
-
将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤和任务完成情况进行持久化。
-
将处理过程设计成一组聚合,这些聚合在一系列的活动中相互协作。一个或多个聚合实例充当执行组件并维护整个处理过程的状态。
-
设计一个无状态的处理过程,其中每一个消息处理组件都将对所接收到的消息进行扩充一一即向其中加入额外的数据信息,然后再将消息发送到下一个处理组件。在这种方法种,整个处理过程的状态包含在每条消息中。
事件源
有时,我们的业务可能需要对发生在领域对象上的修改进行跟踪。有如下三种跟踪方式:
-
关注于业务数据的创建时间(create_time)、修改时间(modify_time)和删除时间(delete_time),以及相关的操作人。对于这种跟踪不敏感的业务场景,只用多列维护即可;
-
对于相对敏感的业务场景,每次新增、修改、删除(逻辑删除),我们都会针对其操作时间和操作人记录一条详细的操作记录,这样方便后续对业务数据修改的回溯与跟踪。
-
更敏感的场景,就是需要记录对数据的改变前和改变后的状态,通过操作记录,可以实现数据的重放或回滚(和Git、SVN等非常相似)。
那么我们将第三种概念应用在单个实体或聚合上,这种变化跟踪便是事件源(Event Sourcing)的核心。事件源模式,如下图所示:
事件源是对于某个聚合上的每次命令操作,都有至少一个领域事件发布出去,该领域事件描述了操作的执行结果。每一个领域事件都将被保存到事件存储(Event Store)中。每次从资源库中获取某个聚合时,我们将根据发生在该聚合上的历史事件来重建该聚合实例,事件的作用顺序应该与它们的产生顺序相同。
这种也类似于针对聚合状态的快照(Snapshot),但是对于请求量级比较大的情况,频繁的去创建快照也是非常消耗资源的,所以,我们可以自定义一个阈值(例如:事件数超过50个),当超过这个阈值的时候,我们再创建这个聚合状态的快照,从而获得最优的聚合创建与获取效果。
事件源有助于获得高吞吐量的领域模型,从而极大地提高事务处理效率。另外,事件源还有助于提高CQRS查询模型的伸缩性,因为此时查询模型的数据源可以在事件存储更新之后得到静默更新。