烂翻译系列之学习领域驱动设计——第九章:通信模式

Chapters 5–8 presented tactical design patterns that define the different ways to implement a system’s components: how to model the business logic and how to organize the internals of a bounded context architecturally. In this chapter, we will step beyond the boundaries of a single component and discuss the patterns for organizing the flow of communication across a system’s elements.

第5-8章介绍了战术设计模式,这些模式定义了实现系统组件的不同方式:如何对业务逻辑进行建模,以及如何在架构上组织有界上下文的内部结构。在本章中,我们将跨越单个组件的边界,讨论组织系统中元素间通信流的模式。

The patterns you will learn about in this chapter facilitate cross-bounded context communication, address the limitations imposed by aggregate design principles, and orchestrate business processes spanning multiple system components.

您将在本章中学习的模式有助于跨有界上下文通信,解决聚合设计原则带来的限制,并协调跨多个系统组件的业务流程。

Model Translation

模型(翻译)转换

A bounded context is the boundary of a model—a ubiquitous language. As you learned in Chapter 3, there are different patterns for designing communication across different bounded contexts. Suppose the teams implementing two bounded contexts are communicating effectively and willing to collaborate. In this case, the bounded contexts can be integrated in a partnership: the protocols can be coordinated in an ad hoc manner, and any integration issues can be effectively addressed through communication between the teams. Another cooperation-driven integration method is shared kernel: the teams extract and co-evolve a limited portion of a model; for example, extracting the bounded contexts’ integration contracts into a co-owned repository.

有界上下文是模型的边界——一种通用语言。正如您在第三章中学到的,跨不同有界上下文设计通信有不同的模式。假设实现两个有界上下文的团队正在有效沟通并愿意合作。在这种情况下,有界上下文可以以合作伙伴的方式集成:协议可以临时协调,任何集成问题都可以通过团队之间的沟通得到有效解决。另一种以合作驱动的集成方法是共享内核:团队提取并共同演进模型的一部分(有限的一部分);例如,将有界上下文的集成契约提取到共同拥有的代码库中。

In a customer–supplier relationship, the balance of power tips toward either the upstream (supplier) or the downstream (consumer) bounded context. Suppose the downstream bounded context cannot conform to the upstream bounded context’s model. In this case, a more elaborate technical solution is required that can facilitate communication by translating the bounded contexts’ models.

在客户-供应商关系中,权力平衡或向上游(供应方)有界上下文倾斜或向下游(客户)有界上下文倾斜。假设下游有界上下文不能适配上游有界上下文的模型。在这种情况下,需要一个更精细的技术解决方案,通过翻译(转换)有界上下文的模型来促进通信。

This translation can be handled by one, or sometimes both, sides: the downstream bounded context can adapt the upstream bounded context’s model to its needs using an anticorruption layer (ACL), while the upstream bounded context can act as an open-host service (OHS) and protect its consumers from changes to its implementation model by using an integration-specific published language. Since the translation logic is similar for both the anticorruption layer and the open-host service, this chapter covers the implementation options without differentiating between the patterns and mentions the differences only in exceptional cases.

这种翻译(转换)可以由一方(有时是双方)处理:下游有界上下文可以使用防腐层(ACL)使上游有界上下文的模型满足其需要,而上游有界上下文可以作为开放主机服务(OHS),并通过使用特定于集成的发布语言来保护其客户免受其实现模型变化的影响。由于防腐层和开放主机服务的翻译逻辑是相似的,本章将介绍实现选项,而不对这些模式进行区分,仅在特殊情况下提及差异。

The model’s translation logic can be either stateless or stateful. Stateless translation happens on the fly, as incoming (OHS) or outgoing (ACL) requests are issued, while stateful translation involves a more complicated translation logic that requires a database. Let’s see design patterns for implementing both types of model translation.

模型的翻译(转换)逻辑可以是无状态的,也可以是有状态的。当发出传入(OHS)或传出(ACL)请求时,无状态翻译(转换)会即时进行,而有状态翻译(转换)涉及更复杂的翻译(转换)逻辑,需要数据库。让我们看看实现这两种模型翻译(转换)的设计模式。

Stateless Model Translation

无状态模型翻译(转换)

For stateless model translation, the bounded context that owns the translation (OHS for upstream, ACL for downstream) implements the proxy design pattern to interject the incoming and outgoing requests and map the source model to the bounded context’s target model. This is depicted in Figure 9-1.

对于无状态模型翻译(转换),拥有翻译(转换)功能的有界上下文(上游为 OHS,下游为 ACL)实现代理设计模式,以插入传入和传出请求,并将源模型映射到有界上下文的目标模型。如图9-1所示。

Figure 9-1. Model translation by a proxy

图9-1.  由代理实现的模型翻译(转换)

Implementation of the proxy depends on whether the bounded contexts are communicating synchronously or asynchronously.

代理的实现取决于有界上下文是通过同步还是异步方式进行通信。

Synchronous

同步

The typical way to translate models used in synchronous communication is to embed the transformation logic in the bounded context’s codebase, as shown in Figure 9-2. In an open-host service, translation to the public language takes place when processing incoming requests, and in an anticorruption layer, it occurs when calling the upstream bounded context.

在同步通信中翻译(转换)模型的典型方式是将转换逻辑嵌入到限界上下文的代码库中,如图9-2所示。在开放主机服务中,当处理传入请求时,会将其转换为公共语言;而在防腐层中,当调用上游有界上下文时,也会发生转换。

Figure 9-2. Synchronous communication

图9-2.  同步通信

In some cases, it can be more cost-effective and convenient to offload the translation logic to an external component such as an API gateway pattern. The API gateway component can be an open source software-based solution such as Kong or KrakenD, or it can be a cloud vendor’s managed service such as AWS API Gateway, Google Apigee, or Azure API Management.

在某些情况下,将翻译(转换)逻辑卸载到外部组件(如 API 网关模式)可能更具成本效益和方便。API 网关组件可以是基于开源软件的解决方案,如 Kong 或 KrakenD,也可以是云供应商的托管服务,如 AWS API Gateway、 Google Apigee 或 Azure API Management。

For bounded contexts implementing the open-host pattern, the API gateway is responsible for converting the internal model into the integration-optimized published language. Moreover, having an explicit API gateway can alleviate the process of managing and serving multiple versions of the bounded context’s API, as depicted in Figure 9-3.

对于实现开放主机模式的有界上下文,API 网关负责将内部模型转换为经过集成优化的发布语言。此外,拥有一个明确的 API 网关可以减轻管理和提供有界上下文 API 多个版本的痛苦,如图9-3所示。

Figure 9-3. Exposing different versions of the published language

图9-3.  公开发布语言的不同版本

Anticorruption layers implemented using an API gateway can be consumed by multiple downstream bounded contexts. In such cases, the anticorruption layer acts as an integration-specific bounded context, as shown in Figure 9- 4.

使用API网关实现的防腐层可以被多个下游有界上下文使用。在这种情况下,防腐层充当特定于集成的有界上下文,如图9-4所示。

Figure 9-4. Shared anticorruption layer

图9-4.  共享防腐层

Such bounded contexts, which are mainly in charge of transforming models for more convenient consumption by other components, are often referred to as interchange contexts.

这种有界上下文通常被称为交换上下文,它主要负责转换模型以便其他组件更方便地使用。

Asynchronous

异步

To translate models used in asynchronous communication you can implement a message proxy: an intermediary component subscribing to messages coming from the source bounded context. The proxy will apply the required model transformations and forward the resultant messages to the target subscriber (see Figure 9-5).

为了在异步通信中转换(翻译)使用的模型,你可以实现一个消息代理:一个中间组件,它订阅来自源限界上下文的消息。此消息代理将应用所需的模型转换并将结果消息转发给目标订阅者(参见图9-5)。

Figure 9-5. Translating models in asynchronous communication

图9-5.  在异步通信中转换模型

In addition to translating the messages’ model, the intercepting component can also reduce the noise on the target bounded context by filtering out irrelevant messages.

除了转换消息模型之外,拦截组件还可以通过过滤掉不相关的消息来减少目标有界上下文的噪声。

Asynchronous model translation is essential when implementing an open host service. It’s a common mistake to design and expose a published language for the model’s objects and allow domain events to be published as they are, thereby exposing the bounded context’s implementation model. Asynchronous translation can be used to intercept the domain events and convert them into a published language, thus providing better encapsulation of the bounded context’s implementation details (see Figure 9-6).

在实现开放主机服务时,异步模型转换是必不可少的。一个常见的错误是,为模型的对象设计并公开发布语言,并允许领域事件按原样发布,从而暴露了有界上下文的实现模型。异步转换可以用来拦截领域事件并将它们转换成一种发布语言,从而更好地封装有界上下文的实现细节(参见图9-6)。

Moreover, translating messages to the published language enables differentiating between private events that are intended for the bounded context’s internal needs and public events that are designed for integration with other bounded contexts. We’ll revisit and expand on the topic of private/public events in Chapter 15, where we discuss the relationship between domain-driven design and event-driven architecture.

此外,将消息翻译为公共语言使得可以区分旨在为有界上下文内部需求服务的私有事件和旨在与其他有界上下文集成的公共事件。我们将在第15章中重新探讨并延伸关于私有/公共事件的话题,届时我们将讨论领域驱动设计与事件驱动架构之间的关系。

Figure 9-6. Domain events in a published language

图9-6.  发布语言中的领域事件

Stateful Model Translation

有状态的模型翻译(转换)

For more significant model transformations—for example, when the translation mechanism has to aggregate the source data or unify data from multiple sources into a single model—a stateful translation may be required. Let’s discuss each of these use cases in detail.

对于更复杂的模型翻译(转换)(例如,当转换机制必须聚合源数据或将来自多个源的数据合并到单个模型中时),可能需要进行有状态翻译(转换)。让我们详细讨论这些用例中的每一个。

Aggregating incoming data

聚合传入数据

Let’s say a bounded context is interested in aggregating incoming requests and processing them in batches for performance optimization. In this case, aggregation may be required both for synchronous and asynchronous requests (see Figure 9-7).

假设一个有界上下文对聚合传入的请求并以批处理方式进行性能优化感兴趣。在这种情况下,对于同步和异步请求都可能需要聚合(见图9-7)。

Figure 9-7. Batching requests

图9-7.  批处理请求

Another common use case for aggregation of source data is combining multiple fine-grained messages into a single message containing the unified data, as depicted in Figure 9-8.

另一个常见的源数据聚合用例是将多个细粒度的消息组合成一个包含合并后数据的单个消息,如图9-8所示。

Figure 9-8. Unifying incoming events

图9-8.  合并传入事件

Model transformation that aggregates incoming data cannot be implemented using an API gateway, and thus requires more elaborate, stateful processing. To track the incoming data and process it accordingly, the translation logic requires its own persistent storage (see Figure 9-9).

聚合传入数据的模型翻译(转换)不能使用 API 网关实现,因此需要更复杂的有状态处理。为了跟踪传入的数据并相应地处理它,翻译(转换)逻辑需要它自己的持久化存储(参见图9-9)。

Figure 9-9. Stateful model transformation

图9-9.  有状态的模型翻译(转换)

In some use cases, you can avoid implementing a custom solution for a stateful translation by using off-the-shelf products; for example, a stream-process platform (Kafka, AWS Kinesis, etc.), or a batching solution (Apache NiFi, AWS Glue, Spark, etc.).

在某些用例中,通过使用现成的产品,可以避免为有状态转换实现自定义解决方案;如,一个流处理平台(Kafka、 AWS Kinesis 等) ,或者一个批处理解决方案(Apache NiFi、 AWS Glue、 Spark 等)。

Unifying multiple sources

合并多源

A bounded context may need to process data aggregates from multiple sources, including other bounded contexts. A typical example for this is the backend-for-frontend pattern, in which the user interface has to combine data originating from multiple services.

一个有界上下文可能需要处理来自多个源(包括其他有界上下文)的数据聚合。这种情况的一个典型例子是后端为前端模式(backend-for-frontend pattern),在这种模式中,用户界面需要组合来自多个服务的数据。

Another example is a bounded context that must process data from multiple other contexts and implement complex business logic to process all the data. In this case, it can be beneficial to decouple the integration and business logic complexities by fronting the bounded context with an anticorruption layer that aggregates data from all other bounded contexts, as shown in Figure 9-10.

另一个例子是一个有界上下文,它必须处理来自多个其他上下文的数据并实现复杂的业务逻辑来处理所有数据。在这种情况下,通过在有界上下文前面放置一个防腐层来聚合来自所有其他有界上下文的数据,可以将集成和业务逻辑复杂性解耦,如图9-10所示,这是有益的。

Figure 9-10. Simplifying the integration model using the anticorruption layer pattern

图9-10.  使用防腐层模式简化集成模型

Integrating Aggregates

集成聚合

In Chapter 6, we discussed that one of the ways aggregates communicate with the rest of the system is by publishing domain events. External components can subscribe to these domain events and execute their logic. But how are domain events published to a message bus?

在第6章中,我们讨论了聚合与系统其他部分通信的方式之一是通过发布领域事件。外部组件可以订阅这些领域事件并执行它们的逻辑。但是领域事件是如何发布到消息总线的呢?

Before we get to the solution, let’s examine a few common mistakes in the event publishing process and the consequences of each approach. Consider the following code:

在了解解决方案之前,让我们先探讨事件发布过程中的一些常见错误以及每种方法的后果。思考以下代码:

01 public class Campaign
02 {
03 ...
04 List<DomainEvent> _events;
05 IMessageBus _messageBus;
06 ...
07
08 public void Deactivate(string reason)
09 {
10 for (l in _locations.Values())
11 {
12 l.Deactivate();
13 }
14
15 IsActive = false;
16
17 var newEvent = new CampaignDeactivated(_id, reason);
18 _events.Append(newEvent);
19 _messageBus.Publish(newEvent);
20 }
21 }

On line 17, a new event is instantiated. On the following two lines, it is appended to the aggregate’s internal list of domain events (line 18), and the event is published to the message bus (line 19). This implementation of publishing domain events is simple but wrong. Publishing the domain event right from the aggregate is bad for two reasons. First, the event will be dispatched before the aggregate’s new state is committed to the database. A subscriber may receive the notification that the campaign was deactivated, but it would contradict the campaign’s state. Second, what if the database transaction fails to commit because of a race condition, subsequent aggregate logic rendering the operation invalid, or simply a technical issue in the database? Even though the database transaction is rolled back, the event is already published and pushed to subscribers, and there is no way to retract it.

在第17行,创建了一个新的事件实例。在接下来的两行中,它被添加到聚合的内部领域事件列表中(第18行),并且该事件被发布到消息总线(第19行)。这种发布领域事件的实现很简单,但却是错误的。直接从聚合中发布领域事件是错误的,这有两个原因。首先,在聚合的新状态被提交到数据库之前,事件就会被发布。订阅者可能会收到该活动已停止的通知,但这将与该活动的状态相矛盾。其次,如果由于竞态条件、后续的聚合逻辑导致操作无效或仅仅是数据库中的技术问题导致数据库事务提交失败怎么办?即使数据库事务被回滚,事件已经被发布并推送给订阅者,也无法撤销它。

Let’s try something else:

让我们尝试其他方法:

01 public class ManagementAPI
02 {
03 ...
04 private readonly IMessageBus _messageBus;
05 private readonly ICampaignRepository _repository;
06 ...
07 public ExecutionResult DeactivateCampaign(CampaignId id,string reason)
08 {
09 try
10 {
11 var campaign = repository.Load(id);
12 campaign.Deactivate(reason);
13 _repository.CommitChanges(campaign);
14
15 var events = campaign.GetUnpublishedEvents();
16 for (IDomainEvent e in events)
17 {
18 _messageBus.publish(e);
19 }
20 campaign.ClearUnpublishedEvents();
21 }
22 catch(Exception ex)
23 {
24 ...
25 }
26 }
27 }

In the preceding listing, the responsibility of publishing new domain events is shifted to the application layer. On lines 11 through 13, the relevant instance of the Campaign aggregate is loaded, its Deactivate command is executed, and only after the updated state is successfully committed to the database, on lines 15 through 20, are the new domain events published to the message bus. Can we trust this code? No.

在上面的清单中,发布新领域事件的责任转移到应用层。在第11行到第13行,加载 Campaign 聚合的相关实例,执行其 Deactive 命令,只有在更新的状态成功提交到数据库之后,在第15行到第20行,新的领域事件才会发布到消息总线。我们能信赖此代码吗?不能。

In this case, the process running the logic for some reason fails to publish the domain events. Perhaps the message bus is down. Or the server running the code fails right after committing the database transaction, but before publishing the events the system will still end in an inconsistent state, which means that the database transaction is committed, but the domain events will never be published.

在这种情况下,运行逻辑的进程由于某种原因未能发布领域事件。可能是消息总线出故障了。或者运行代码的服务器在提交数据库事务后立即失败,但在发布事件之前系统仍然会处于不一致的状态,这意味着数据库事务已经提交,但领域事件永远不会被发布。

These edge cases can be addressed using the outbox pattern.

这些边缘情况可以使用发件箱模式(Outbox Pattern)来解决。

Outbox

发件箱

The outbox pattern (Figure 9-11) ensures reliable publishing of domain events using the following algorithm:

发件箱模式(图9-11)使用以下算法确保可靠地发布领域事件:

  • Both the updated aggregate’s state and the new domain events are committed in the same atomic transaction.    更新的聚合状态和新的领域事件都在同一个原子事务中被提交。
  • A message relay fetches newly committed domain events from the database.    消息中继从数据库获取新提交的领域事件。
  • The relay publishes the domain events to the message bus.    消息中继将领域事件发布到消息总线。
  • Upon successful publishing, the relay either marks the events as published in the database or deletes them completely.    成功发布后,消息中继要么在数据库中将这些事件标记为已发布,要么完全删除它们。

Figure 9-11. Outbox pattern

图9-11.  发件箱模式

When using a relational database, it’s convenient to leverage the database’s ability to commit to two tables atomically and use a dedicated table for storing the messages, as shown in Figure 9-12.

当使用关系型数据库时,利用数据库的原子性将更改提交到两个表,并使用专用表来存储消息是很方便的,如图9-12所示。

Figure 9-12. Outbox table

图9-12.  发件箱表

When using a NoSQL database that doesn’t support multi-document transactions, the outgoing domain events have to be embedded in the aggregate’s record. For example:

当使用不支持多文档事务的 NoSQL 数据库时,传出的领域事件必须内嵌到聚合的记录中。例如:

 1 {
 2  "campaign-id": "364b33c3-2171-446d-b652-8e5a7b2be1af",
 3  "state": {
 4  "name": "Autumn 2017",
 5  "publishing-state": "DEACTIVATED",
 6  "ad-locations": [
 7  ...
 8  ]
 9  ...
10  },
11  "outbox": [
12  {
13  "campaign-id": "364b33c3-2171-446d-b652-
14 8e5a7b2be1af",
15  "type": "campaign-deactivated",
16  "reason": "Goals met",
17  "published": false
18  }
19  ]
20 }

In this sample, you can see the JSON document’s additional property, outbox, containing a list of domain events that have to be published.

在这个示例中,您可以看到JSON文档的一个额外属性outbox,它包含了一个待发布的领域事件列表。

Fetching unpublished events

获取未发布的事件

The publishing relay can fetch the new domain events in either a pull-based or push-based manner:

发布中继可以以拉取(pull-based)或推送(push-based)的方式获取新的领域事件:

Pull: polling publisher    拉:轮询发布者

       The relay can continuously query the database for unpublished events. Proper indexes have to be in place to minimize the load on the database induced by the constant polling.

       中继可以持续不断地查询数据库以获取未发布的事件。必须设置适当的索引,以最小化由持续轮询造成的数据库负载。

Push: transaction log tailing    推:事务日志跟踪

       Here we can leverage the database’s feature set to proactively call the publishing relay when new events are appended. For example, some relational databases enable getting notifications about updated/inserted records by tailing the database’s transaction log. Some NoSQL databases expose committed changes as streams of events (e.g., AWS DynamoDB Streams).

       我们可以利用数据库的功能集,当有新事件添加时主动调用发布中继。例如,一些关系型数据库允许通过跟踪数据库的事务日志来获取关于更新/插入记录的通知。一些NoSQL数据库将已提交的更改作为事件流进行公开(例如,AWS DynamoDB Streams)。

It’s important to note that the outbox pattern guarantees delivery of the messages at least once: if the relay fails right after publishing a message but before marking it as published in the database, the same message will be published again in the next iteration.

重要的是要注意,发件箱模式(Outbox Pattern)保证消息至少被传递一次:如果中继在发布消息后,但在数据库中将其标记为已发布之前失败,那么相同的消息将在下一次迭代中再次被发布。

Next, we’ll take a look at how we can leverage the reliable publishing of domain events to overcome some of the limitations imposed by aggregate design principles.

接下来,我们将探讨如何利用领域事件的可靠发布来克服聚合设计原则带来的一些限制。

Saga

分布式事务:Saga模式

One of the core aggregate design principles is to limit each transaction to a single instance of an aggregate. This ensures that an aggregate’s boundaries are carefully considered and encapsulate a coherent set of business functionality. But there are cases when you have to implement a business process that spans multiple aggregates.

聚合设计的核心原则之一是限制每个事务仅针对一个聚合实例。这确保了聚合的边界被仔细斟酌,并封装了一组连贯(相干)的业务功能。但是,有些情况下您必须实现跨越多个聚合的业务流程。

Consider the following example: when an advertising campaign is activated, it should automatically submit the campaign’s advertising materials to its publisher. Upon receiving the confirmation from the publisher, the campaign’s publishing state should change to Published. In the case of rejection by the publisher, the campaign should be marked as Rejected.

思考下面的例子:当一个广告活动被激活时,它应该自动将活动的广告素材提交给它的发行商。在收到发行商的确认后,广告活动的发布状态应改为“已发布”。如果发布者拒绝,广告活动应该被标记为“已拒绝”。

This flow spans two business entities: advertising campaign and publisher. Co-locating the entities in the same aggregate boundary would definitely be overkill, as these are clearly different business entities that have different responsibilities and may belong to different bounded contexts. Instead, this flow can be implemented as a saga.

此流程跨越两个业务实体: 广告活动和发行商。将这些实体放在同一个聚合边界内肯定是过度的,因为它们显然是不同的业务实体,具有不同的职责,可能属于不同的有界上下文。相反,这个流程可以作为一个“saga”(编排)来实现。

A saga is a long-running business process. It’s long running not necessarily in terms of time, as sagas can run from seconds to years, but rather in terms of transactions: a business process that spans multiple transactions. The transactions can be handled not only by aggregates but by any component emitting domain events and responding to commands. The saga listens to the events emitted by the relevant components and issues subsequent commands to the other components. If one of the execution steps fails, the saga is in charge of issuing relevant compensating actions to ensure the system state remains consistent.

“saga”(编排)是一个长期运行的业务流程。这里的“长期运行”不一定是指时间上的长短(就像sagas(长篇故事)可能从几秒钟到几年不等),而是指跨多个事务的业务流程。这些事务不仅可以由聚合来处理,还可以由任何发出领域事件和响应命令的组件来处理。saga监听相关组件发出的事件,并向其他组件发出后续命令。如果其中一个执行步骤失败,saga将负责发出相关的补偿操作,以确保系统状态保持一致。

Let’s see how the advertising campaign publishing flow from the preceding example can be implemented as a saga, as shown in Figure 9-13.

让我们看看前面的示例中的广告活动发布流程如何作为saga来实现,如图9-13所示。

Figure 9-13. Saga

图9-13.  Saga

To implement the publishing process, the saga has to listen to the CampaignActivated event from the Campaign aggregate and the PublishingConfirmed and PublishingRejected events from the AdPublishing bounded context. The saga has to execute the SubmitAdvertisement command on AdPublishing, and the TrackPublishingConfirmation and TrackPublishingRejection commands on the Campaign aggregate. In this example, the TrackPublishingRejection command acts as a compensation action that will ensure that the advertising campaign is not listed as active. Here is the code:

要实现发布流程,saga需要监听来自Campaign(广告活动)聚合的CampaignActivated(活动激活)事件以及来自AdPublishing(广告发布)有界上下文的PublishingConfirmed(发布确认)和PublishingRejected(发布拒绝)事件。saga需要在AdPublishing(广告发布)上执行SubmitAdvertisement(提交广告)命令,并在Campaign(广告活动)聚合上执行TrackPublishingConfirmation(跟踪发布确认)和TrackPublishingRejection(跟踪发布拒绝)命令。在这个例子中,TrackPublishingRejection(跟踪发布拒绝)命令作为补偿操作,确保Campaign(广告活动)不会被列为活动状态。以下是代码示例:

 1 public class CampaignPublishingSaga
 2 {
 3  private readonly ICampaignRepository _repository;
 4  private readonly IPublishingServiceClient _publishingService;
 5  ...
 6  public void Process(CampaignActivated @event)
 7  {
 8  var campaign = _repository.Load(@event.CampaignId);
 9  var advertisingMaterials = campaign.GenerateAdvertisingMaterials();
11  _publishingService.SubmitAdvertisement(@event.CampaignId, advertisingMaterials);
12  }
13  public void Process(PublishingConfirmed @event)
14  {
15  var campaign = _repository.Load(@event.CampaignId);
16  campaign.TrackPublishingConfirmation(@event.ConfirmationId);
17  _repository.CommitChanges(campaign);
18  }
19  public void Process(PublishingRejected @event)
20  {
21  var campaign = _repository.Load(@event.CampaignId);
22  campaign.TrackPublishingRejection(@event.RejectionReason);
23  _repository.CommitChanges(campaign);
24  }
25 }

The preceding example relies on the messaging infrastructure to deliver the relevant events, and it reacts to the events by executing the relevant commands. This is an example of a relatively simple saga: it has no state. You will encounter sagas that do require state management; for example, to track the executed operations so that relevant compensating actions can be issued in case of a failure. In such a situation, the saga can be implemented as an event-sourced aggregate, persisting the complete history of received events and issued commands. However, the command execution logic should be moved out of the saga itself and executed asynchronously, similar to the way domain events are dispatched in the outbox pattern:

前面的示例依赖于消息传递基础设施来传递相关事件,并通过执行相关命令来响应这些事件。这是一个相对简单的saga的示例:它没有状态。您将遇到确实需要状态管理的saga;例如,为了跟踪已执行的操作,以便在失败(发生故障)时发出相关的补偿操作。在这种情况下,saga可以作为事件源聚合来实现,持久化接收到的事件和发出的命令的完整历史。但是,命令执行逻辑应该从saga中移出并异步执行,类似于在发件箱模式中分派领域事件的方式:

 1 public class CampaignPublishingSaga
 2 {
 3  private readonly ICampaignRepository _repository;
 4  private readonly IList<IDomainEvent> _events;
 5  ...
 6  public void Process(CampaignActivated activated)
 7  {
 8  var campaign = _repository.Load(activated.CampaignId);
 9  var advertisingMaterials = campaign.GenerateAdvertisingMaterials();
10  var commandIssuedEvent = new CommandIssuedEvent(target: Target.PublishingService, command: newSubmitAdvertisementCommand(activated.CampaignId, advertisingMaterials));  
11  _events.Append(activated);
12  _events.Append(commandIssuedEvent);
13  }
14  public void Process(PublishingConfirmed confirmed)
15  {
16  var commandIssuedEvent = new CommandIssuedEvent(target: Target.CampaignAggregate, command: new TrackConfirmation(confirmed.CampaignId, confirmed.ConfirmationId));
17  _events.Append(confirmed);
18  _events.Append(commandIssuedEvent);
19  }
20  public void Process(PublishingRejected rejected)
21  {
22  var commandIssuedEvent = new CommandIssuedEvent(target: Target.CampaignAggregate, command: new TrackRejection(rejected.CampaignId, rejected.RejectionReason));
23  _events.Append(rejected);
24  _events.Append(commandIssuedEvent);
25  }
26 }

In this example, the outbox relay will have to execute the commands on relevant endpoints for each instance of CommandIssuedEvent. As in the case of publishing domain events, separating the transition of the saga’s state from the execution of commands ensures that the commands will be executed reliably, even if the process fails at any stage.

在这个示例中,发件箱中继必须针对每个CommandIssuedEvent实例在相关端点上执行命令。与发布领域事件一样,将saga的状态转换与命令执行分开,可以确保即使进程在任何阶段失败,命令也能可靠地执行。

Consistency

一致性

Although the saga pattern orchestrates a multicomponent transaction, the states of the involved components are eventually consistent. And although the saga will eventually execute the relevant commands, no two transactions can be considered atomic. This correlates with another aggregate design principle:

虽然 saga 模式协调了一个多组件事务,但是所涉及的组件的状态最终是一致的。尽管saga最终将执行相关的命令,但没有任何两个事务可以被认为是原子的。这与另一个聚合设计原则相关:

Only the data within an aggregate’s boundaries can be considered strongly consistent. Everything outside is eventually consistent.

只有聚合边界内的数据才能被认为是强一致的。除此之外的所有数据都是最终一致的。

Use this as a guiding principle to make sure you are not abusing sagas to compensate for improper aggregate boundaries. Business operations that have to belong to the same aggregate require strongly consistent data.

以此为指导原则,确保您不会滥用sagas来弥补不适当的聚合边界。要求数据强一致性要求的业务操作必须属于相同的聚合。

The saga pattern is often confused with another pattern: process manager. Although the implementation is similar, these are different patterns. In the next section, we’ll discuss the purpose of the process manager pattern and how it differs from the saga pattern.

saga模式常常与另一种模式混淆:流程管理器。尽管实现方式是相似的,但是不同的模式。在下一节中,我们将讨论流程管理器模式的用途以及它与saga模式的区别。

Process Manager

流程管理器

The saga pattern manages simple, linear flow. Strictly speaking, a saga matches events to the corresponding commands. In the examples we used to demonstrate saga implementations, we actually implemented simple matching of events to commands:

saga模式管理简单的线性流程。严格来说,saga将事件与相应的命令进行匹配。在我们用来演示saga实现的示例中,我们实际上实现了事件与命令的简单匹配:

  • CampaignActivated event to PublishingService.SubmitAdvertisement command    CampaignActivated(活动激活) 事件和 PublishingService.SubmitAdvertisement (提交广告)命令
  • PublishingConfirmed event to Campaign.TrackConfirmation command    PublishingConfirmed(发布确认)事件和Campaign.TrackConfirmation(跟踪确认)命令
  • PublishingRejected event to Campaign.TrackRejection command    PublishingRejected(发布拒绝) 事件和Campaign.TrackRejection(跟踪拒绝)命令

The process manager pattern, shown in Figure 9-14, is intended to implement a business-logic-based process. It is defined as a central processing unit that maintains the state of the sequence and determines the next processing steps.

流程管理器模式(如图9-14所示)旨在实现基于业务逻辑的流程。它被定义为一个中央处理单元,维护序列的状态并确定下一个处理步骤。

Figure 9-14. Process manager

图9-14.流程管理器

As a simple rule of thumb, if a saga contains if-else statements to choose the correct course of action, it is probably a process manager.

作为一个简单的经验法则,如果一个saga包含 if-else 语句来选择正确的操作流向,那么它很可能是一个流程管理器。

Another difference between a process manager and a saga is that a saga is instantiated implicitly when a particular event is observed, as in CampaignActivated in the preceding examples. A process manager, on the other hand, cannot be bound to a single source event. Instead, it’s a coherent business process consisting of multiple steps. Hence, a process manager has to be instantiated explicitly. Consider the following example:

流程管理器与saga之间的另一个区别是,当观察到特定事件(如前面示例中的CampaignActivated)时,saga会被隐式实例化。而流程管理器则不能与单个源事件绑定。相反,它是一个由多个步骤组成的连贯业务流程。因此,流程管理器必须被显式实例化。请思考以下示例:

Booking a business trip starts with the routing algorithm choosing the most cost-effective flight route and asking the employee to approve it. In case the employee prefers a different route, their direct manager needs to approve it. After the flight is booked, one of the preapproved hotels has to be booked for the appropriate dates. If no hotels are available, the flight tickets have to be canceled.

预订出差从路由算法开始,选择最具成本效益的航班路线,并请求员工核准。如果员工喜欢不同的路线,他们的直接领导需要批准。在预订航班后,需要在适当的日期预订其中一家预先批准的酒店。如果没有旅馆可入住,机票就必须取消。

In this example, there is no central entity to trigger the trip booking process. The trip booking is the process and it has to be implemented as a process manager (see Figure 9-15).

在这个例子中,没有中央实体来触发行程预订流程。行程预订是一个流程,它必须作为流程管理器来实现(见图9-15)。

Figure 9-15. Trip booking process manager

图9-15.  行程预订流程管理器

From an implementation perspective, process managers are often implemented as aggregates, either state based or event sourced. For example:

从实现的角度来看,流程管理器通常被实现为聚合,可以是基于状态的,也可以是基于事件溯源的。例如:

 1 public class BookingProcessManager
 2 {
 3  private readonly IList<IDomainEvent> _events;
 4  private BookingId _id;
 5  private Destination _destination;
 6  private TripDefinition _parameters;
 7  private EmployeeId _traveler;
 8  private Route _route;
 9  private IList<Route> _rejectedRoutes;
10  private IRoutingService _routing;
11  ...
12  public void Initialize(Destination destination, TripDefinition parameters, EmployeeId traveler)
13  {
14  _destination = destination;
15  _parameters = parameters;
16  _traveler = traveler;
17  _route = _routing.Calculate(destination, parameters);
18  var routeGenerated = new RouteGeneratedEvent(BookingId: _id, Route: _route);
19  var commandIssuedEvent = new CommandIssuedEvent(command: new RequestEmployeeApproval(_traveler, _route));
20  _events.Append(routeGenerated);
21  _events.Append(commandIssuedEvent);
22  }
23  public void Process(RouteConfirmed confirmed)
24  {
25  var commandIssuedEvent = new CommandIssuedEvent(command: new BookFlights(_route, _parameters));
26  _events.Append(confirmed);
27  _events.Append(commandIssuedEvent);
28  }
29  public void Process(RouteRejected rejected)
30  {
31  var commandIssuedEvent = new CommandIssuedEvent(command: new RequestRerouting(_traveler, _route));
32  _events.Append(rejected);
33  _events.Append(commandIssuedEvent);
34  }
35  public void Process(ReroutingConfirmed confirmed)
36  {
37  _rejectedRoutes.Append(route);
38  _route = _routing.CalculateAltRoute(destination, parameters, rejectedRoutes);
39  var routeGenerated = new RouteGeneratedEvent(BookingId: _id, Route: _route);  
40  var commandIssuedEvent = new CommandIssuedEvent(command: new RequestEmployeeApproval(_traveler, _route));
41  _events.Append(confirmed);
42  _events.Append(routeGenerated);
43  _events.Append(commandIssuedEvent);
44  }
45  public void Process(FlightBooked booked)
46  {
47  var commandIssuedEvent = new CommandIssuedEvent(command: new BookHotel(_destination, _parameters));  
48  _events.Append(booked);
49  _events.Append(commandIssuedEvent);
50  }
51  ...
52 }

In this example, the process manager has its explicit ID and persistent state, describing the trip that has to be booked. As in the earlier example of a saga pattern, the process manager subscribes to events that control the workflow (RouteConfirmed, RouteRejected, ReroutingConfirmed, etc.), and it instantiates events of type CommandIssuedEvent that will be processed by an outbox relay to execute the actual commands.

在这个例子中,流程管理器有其明确的ID和持久化状态,描述了需要预订的行程。与前面的saga模式示例一样,流程管理器订阅了控制工作流的事件(如RouteConfirmed(路由确认)、RouteRejected(路由拒绝)、ReroutingConfirmed(重新路由确认)等),并实例化了CommandIssuedEvent类型的事件,这些事件将由发件箱中继处理以执行实际的命令。

Conclusion

总结

In this chapter, you learned the different patterns for integrating a system’s components. The chapter began by exploring patterns for model translations that can be used to implement anticorruption layers or open-host services. We saw that translations can be handled on the fly or can follow a more complex logic, requiring state tracking.

在本章中,您学习了集成系统组件的不同模式。本章首先探讨了可用于实现防腐层或开放主机服务的模型翻译(转换)模式。我们看到,翻译(转换)可以即时处理,也可以遵循更复杂的逻辑,需要状态跟踪。

The outbox pattern is a reliable way to publish aggregates’ domain events. It ensures that domain events are always going to be published, even in the face of different process failures.

发件箱模式是发布聚合的领域事件的可靠方式。它确保领域事件总是会被发布,即使面对不同的进程失败也是如此。

The saga pattern can be used to implement simple cross-component business processes. More complex business processes can be implemented using the process manager pattern. Both patterns rely on asynchronous reactions to domain events and the issuing of commands.

Saga 模式可用于实现简单的跨组件业务流程。更复杂的业务流程可以使用流程管理器模式来实现。这两种模式都依赖于对领域事件的异步响应和命令的发出。

Exercises

练习

1. Which bounded context integration pattern requires implementation of model transformation logic?    哪种有界上下文集成模式需要实现模型转换逻辑?

a. Conformist    遵奉者

b. Anticorruption layer    防腐层

c. Open-host service    开放主机服务

d. B and C    B和C

答案:d。

2. What is the goal of the outbox pattern?    发件箱模式的目标是什么?

a. Decouple messaging infrastructure from the system’s business logic layer    将消息传递基础设施与系统的业务逻辑层分离

b. Reliably publish messages    可靠地发布消息

c. Support implementation of the event-sourced domain model pattern    支持实现基于事件源的领域模型模式

d. A and C    A和C

答案:b。

3. Apart from publishing messages to a message bus, what are other possible use cases for the outbox pattern?    除了将消息发布到消息总线之外,发件箱模式还有哪些其他可能的用例?

答案:The outbox pattern can be used to implement asynchronous execution of external components. For example, it can be used for sending email messages.    发件箱模式可用于实现外部组件的异步执行。例如,它可以用来发送电子邮件消息。

4. What are the differences between the saga and process manager patterns?    saga和流程管理器模式之间的区别是什么?

a. A process manager requires explicit instantiation, while a saga is executed when a relevant domain event is published.    流程管理器需要显式实例化,而saga在相关领域事件发布时执行。

b. Contrary to a process manager, a saga never requires persistence of its execution state.    与流程管理器相反,saga从不需要持久化其执行状态。

c. A saga requires the components it manipulates to implement the event sourcing pattern, while a process manager doesn’t.    saga要求它所操作的组件实现基于事件源的模式,而流程管理器则不需要。

d. The process manager pattern is suitable for complex business workflows.    流程管理器模式适用于复杂的业务流程。

e. A and D are correct.    A 和 D 是正确的。

答案:e。


1 Richardson, C. (2019). Microservice Patterns: With Examples in Java. New York: Manning Publications.    《微服务模式: Java 中的例子》纽约: 曼宁出版社。

2 Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Boston: Addison-Wesley.    企业集成模式: 设计、建造和部署消息解决方案。波士顿: Addison-Wesley。

 
posted @ 2023-01-29 11:17  菜鸟吊思  阅读(113)  评论(0)    收藏  举报