mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-11-16 22:23:45 +08:00
fix img
This commit is contained in:
@@ -638,7 +638,7 @@ public class InventoryProviderImpl implements InventoryProvider {
|
||||
</ul>
|
||||
<p>分析 Dubbo 服务的最佳实践,了解 Dubbo 框架自身对服务定义的限制,再对比领域驱动设计的分层架构,就可以确定在领域驱动设计中使用 Dubbo 作为分布式通信机制时远程服务与应用服务的设计实践。</p>
|
||||
<p>首先,应用服务的方法本身就是为了满足完整业务价值引入的外观接口,服务粒度与 Dubbo 服务的要求是保持一致的。应用服务的参数定义为消息契约对象,它作为 DTO 模式的体现,通常会定义为不依赖于任何框架的 POJO 值对象,这也是符合 Dubbo 服务要求的。Dubbo 服务的版本号定义在配置文件中,版本自身并不会影响服务定义。结合接口与实现分离原则与整洁架构思想,可以认为应用层的应用服务即 Dubbo 服务提供者的接口,消息契约对象也定义在应用层中,而远程服务则为 Dubbo 服务提供者的实现,它依赖了 Dubbo 框架:</p>
|
||||
<p><img src="assets/7882b510-1c1e-11ea-9327-c7a4473fd236" alt="65247722.png" /></p>
|
||||
<p><img src="assets/7882b510-1c1e-11ea-9327-c7a4473fd236" alt="png" /></p>
|
||||
<p>至于对 Dubbo 服务的调用,除了必要的配置与部署需求之外,与进程内通信的上下文协作没有任何区别,因为 Dubbo 服务接口与消息契约对象就部署在客户端,可以直接调用服务接口的方法。若有必要,仍然建议在防腐层的客户端实现中调用 Dubbo 服务。与 REST 服务不同,一旦服务接口发生了变化,不仅需要修改客户端代码,还需要重新编译服务接口包,然后在客户端上下文进行重新部署。若希望客户端不依赖服务接口,可以使用 Dubbo 提供的泛化服务 GenericService。泛化服务接口的参数与返回值只能是 Map,若要表达一个自定义契约对象,需要以 Map<String, Object> 来表达,获取泛化服务实例也需要调用 ReferenceConfig 来获得,无疑增加了客户端调用的复杂度。</p>
|
||||
<p>Dubbo 服务的实现皆位于上游限界上下文所在的服务端。如果调用者希望在客户端也执行部分逻辑,如 ThreadLocal 缓存,验证参数等,就需要在客户端本地提供存根(Stub)实现,并在服务配置中指定 Stub 的值。这在一定程度上会影响客户端防腐层代码的编写。</p>
|
||||
<h4>消息传递</h4>
|
||||
@@ -646,13 +646,13 @@ public class InventoryProviderImpl implements InventoryProvider {
|
||||
<p>消息传递通常采用发布/订阅事件模式来完成限界上下文之间的协作。在 3-18 课《发布者—订阅者模式》中,我谈到了在限界上下文之间通过应用事件(Application Event)来实现彼此的协作。考虑到事件的解耦性,这一协作方式能够最大程度地保证限界上下文的自治性。</p>
|
||||
<p>如果使用了事件流在当前限界上下文缓存和同步了本该由上游限界上下文提供的数据,还可以将跨限界上下文的同步查询操作改为本地查询操作,使得跨限界上下文之间产生的所有协作皆为允许异步模式的命令操作,那么限界上下文就获得了真正的自治,即不存在任何具有依赖调用关系的上下文协作(事件消息协议产生的耦合除外)。例如,订单上下文本身需要同步调用库存上下文的服务,以验证商品是否缺货;为了避免对该服务的调用,就可以在订单上下文的数据库中建立一个库存表,并通过订阅库存上下文的 InventoryChanged 事件,将库存记录的变更同步反应到订单上下文的库存表。这样就可以将跨上下文的同步查询服务转为本地查询操作。</p>
|
||||
<p>以订单、支付、库存与通知上下文之间的关系为例。首先考虑下订单业务用例,通过事件进行通信的时序图如下所示:</p>
|
||||
<p><img src="assets/a5842940-1c1e-11ea-be19-d517f4b6048e" alt="61614303.png" /></p>
|
||||
<p><img src="assets/a5842940-1c1e-11ea-be19-d517f4b6048e" alt="png" /></p>
|
||||
<p>订单上下文内的对象在同一个进程内协作,在下订单成功之后,由 OrderEventPublisher 发布 OrderPlaced 应用事件。注意,InventoryService 也是订单上下文中的领域模型对象,这是因为订单上下文通过事件流同步了库存上下文的库存数据。在支付场景中,我们可以看到这个同步事件流的时序图。通知上下文的 OrderPlacedEventSubscriber 关心下订单成功的事件,并在收到事件后,由 OrderEventHandler 处理该事件,最后通过 NotificationAppService 应用服务发送通知。</p>
|
||||
<p>再考虑支付业务用例:</p>
|
||||
<p><img src="assets/bc7fa520-1c1e-11ea-b794-6fc9e66c0b74" alt="63973053.png" /></p>
|
||||
<p><img src="assets/bc7fa520-1c1e-11ea-b794-6fc9e66c0b74" alt="png" /></p>
|
||||
<p>上图所示的服务间协作相对比较复杂,彼此之间存在事件的发布与订阅关系,但对于每个限界上下文而言,它只负责处理属于自己的业务,并在完成业务后发布对应的应用事件即可。在设计时,我们需要理清这些事件流的方向,但每个限界上下文自身却是自治的。注意,订单上下文对 InventoryChanged 事件的订阅,目的就是为了实现库存数据向订单上下文的同步,在订单上下文的 InventoryAppService 与 InventoryService 修改的是订单上下文同步的库存表。</p>
|
||||
<p>当我们引入消息队列中间件如 Kafka 后,以上限界上下文之间的事件通信时序图就可以简化为:</p>
|
||||
<p><img src="assets/e5b08950-1c1e-11ea-9697-b5daa308a319" alt="69373935.png" /></p>
|
||||
<p><img src="assets/e5b08950-1c1e-11ea-9697-b5daa308a319" alt="png" /></p>
|
||||
<p>事件的传递通过 Kafka 进行,如此即可解耦限界上下文。传递的事件消息既是通信的数据,需要支持序列化,又是服务之间协作的接口。事件的定义有两种风格:事件通知(Event Notification)和事件携带状态迁移(Event-Carried State Transfer)。我在 3-18 课《发布者—订阅者模式》已有阐述,这里略过不提。</p>
|
||||
<p>分析前面所示的事件通信时序图,参与事件消息传递的关键角色包括:</p>
|
||||
<ul>
|
||||
@@ -662,12 +662,12 @@ public class InventoryProviderImpl implements InventoryProvider {
|
||||
</ul>
|
||||
<p>如果将发布应用事件的限界上下文称之为发布上下文,订阅应用事件的限界上下文称之为订阅上下文,则事件发布者定义在发布上下文,事件订阅者与事件处理器定义在订阅上下文。</p>
|
||||
<p>事件发布者需要知道该何时发布应用事件,发布之前还需要组装应用事件。既然应用事件作为分布式通信的消息契约对象,被定义在应用层(当然也可能定义在领域层,此时的领域事件即为应用事件),而应用服务作为完整业务用例的接口定义者,它必然知道发布应用事件的时机,因此,发布上下文的应用服务就应该是发布应用事件的最佳选择。它们之间的关系如下所示:</p>
|
||||
<p><img src="assets/f251dfb0-1c1e-11ea-948e-4f74e3d0b5b8" alt="71638417.png" /></p>
|
||||
<p><img src="assets/f251dfb0-1c1e-11ea-948e-4f74e3d0b5b8" alt="png" /></p>
|
||||
<p>图中的远程服务不是为下游限界上下文提供的,它实际上属于远程服务中的控制器,用于满足前端 UI 的调用,例如下订单用例,就是买家通过系统前端通过点击“下订单”按钮发起的服务调用请求。事件发布者是一个抽象,扮演了南向网关的角色,基础设施层的 KafkaProducer 实现了该接口,在其内部提供对 Kafka 的实现。代表业务用例的应用服务在组装了应用事件后,可以调用事件发布者的方法发布事件。</p>
|
||||
<p>事件订阅者需要一直监听 Kafka 的 topic。不同的订阅上下文需要监听不同的 topic,获得对应的应用事件。由于它需要调用具体的消息队列实现,一旦接收到它关注的应用事件后,需要通过事件处理器处理事件,因此可以认为事件订阅者是远程服务的一种,它负责接收消息队列传递的远程消息。事件的处理是一种业务逻辑,有时候,在处理完事件后,还需要发布事件,由应用服务来承担最为适宜。当然,具体的业务逻辑则由应用服务转交给领域服务来完成。通常,一个处理应用事件的应用服务需要对应一个事件订阅者:</p>
|
||||
<p><img src="assets/071d5c30-1c1f-11ea-b827-b9d087973f62" alt="71864689.png" /></p>
|
||||
<p><img src="assets/071d5c30-1c1f-11ea-b827-b9d087973f62" alt="png" /></p>
|
||||
<p>以订单上下文为例,参与下订单和支付业务场景的相关类型在分层架构中的关系如下图所示:</p>
|
||||
<p><img src="assets/11158b90-1c1f-11ea-ba91-17ef1800c6ba" alt="73012430.png" /></p>
|
||||
<p><img src="assets/11158b90-1c1f-11ea-ba91-17ef1800c6ba" alt="png" /></p>
|
||||
<p>注意,图中的 ApplicationEventPublisher 参与发布上下文的业务场景,ApplicationEventHandler 则属于订阅上下文。如果一个限界上下文既要发布事件消息,又要订阅事件消息,则应用服务会成为首选的中转站。在订阅上下文一方,负责侦听消息队列的订阅者,属于远程服务的一种。</p>
|
||||
<p>整体来看,无论采用什么样的分布式通信机制,明确基础设施层中远程服务与应用层之间的边界仍然非常重要。不管是 REST 资源与控制器、Dubbo 服务提供者,还是事件订阅者,都是分布式通信的直接执行者,它们不应该知道领域模型的任何一点知识,故而也不应干扰到领域层的设计与实现。应用服务与远程服务接口保持相对一致的映射关系,但对业领域逻辑的调用都交给了应用服务。应用层扮演了外观的角色,分布式通信传递的消息契约对象包括它与领域模型对象之间的转换逻辑都交给了应用层。有时候,为了限界上下文内部架构的简便性,可以考虑合并应用层和基础设施层的远程服务,但是我们需要明白,你在获得简单性的同时,可能牺牲的是架构的清晰性、模型与层次之间的解耦,以及由此带来的拥抱变化的扩展性。</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user