如何在 DDD 中处理外部有状态 Web 服务?
How to deal with external stateful webservices in DDD?
场景#1
- 情况:我需要对网络服务中的一些数据执行业务操作。
- 解决方案:存储库使用 webservice 作为存储库,使用 webservice 数据构建聚合,稍后保存更改。
- 疑惑:这样可以吗?
场景 #2
- 情况:我需要对数据库中的一些数据执行业务操作,并且此类操作涉及用于计算值的网络服务。
- 解决方案:将webservice抽象为领域服务,因此将其作为参数传递给聚合来执行操作。
- 疑问:这有意义吗?我会说 web 服务应该是存储库的一部分,所以数据是一起持久化的。
场景 #3
- 情况:业务运营request/saves数据from/to两个不同的数据库,一个直接访问,另一个通过webservice访问。
- 解决:数据库连接和webservice都在版本库里,一起保存。聚合不知道此拆分。
场景 #4(问题开始...)
- 情况:我需要对数据库中的数据执行业务操作,这涉及到 webservice 操作,业务操作本身需要该结果。
- 问题:网络服务不是事务性的,也不在我的控制之下,尽管它是幂等的。对 webservice 的调用取决于聚合必须加载的一些数据,聚合逻辑取决于 webservice returns(想象一下在线支付)的结果代码。
- 解决方法:webservice作为domain service使用,如果请求失败则全部失败,稍后重试。如果保存时数据库失败,也会重试。
场景 #n [#4 的许多变体] ...
所以我无法就如何在 DDD 中更改状态时使用 Web 服务制定明确的标准。
理想情况下,我会在我的存储中进行更改,然后拥有某种集成层来复制外部数据库或服务中的这些更改,例如事件。但是,大多数情况下存在写后读一致性要求,如果您在操作 returns 后刷新,屏幕上会显示正确的数据(例如:来自同一网络服务的付款收据付款已完成)。
另一个例子:一个屏幕允许保存关于一本书的数据,但是书籍规格保存在数据库中,描述(多种语言)保存在外部服务中。操作必须一致,如果用户单击 "Save" 并刷新,屏幕需要一起显示,不能只显示带有旧翻译的规范或根本不显示翻译,因为它们正在被复制到另一个数据库。
哪个是可靠的决定标准?
答案是阅读Life Beyond Distributed Transactions。
简而言之,尝试可靠地协调不同位置的写入非常昂贵。在大多数情况下,最好在您的设计中承认您不能同时出现在所有地方,并投资于处理该事实的后果。
我一直遵循的规则是 Aggregate 应该是纯粹的,不依赖于进行 IO 调用的服务(即磁盘或网络)。每次在给定状态下执行命令时,它都应该给出相同的结果。注入服务或将其作为方法调用中的参数传递违反了此规则。抽象的想法是聚合永远不应该根据它不拥有的数据做出决定。
然而,每个复杂系统包含的不仅仅是聚合体。它还包含 Sagas/Process 对业务流程建模的管理器(从现在开始只是 Saga)。要设计一个完美的 Saga,只需要一个清晰的业务流程和幂等的端点。
一个 Saga 启动,然后监听域中的变化,通常是通过订阅域事件。它通过向相应的端点发送命令来对它们做出反应。请注意,我使用术语端点来指代以幂等方式处理命令的任何接收器。纯粹的聚合就是这样的端点。但是 Saga Endpoint 也可以是外部系统,比如支付网关。这样的网关不应该使用现有的PaymentID
(您的系统发送的不透明字段)发起新的支付。
结论:据我所知,你所有的场景都可以实现为Sagas。
场景#1
- 情况:我需要对网络服务中的一些数据执行业务操作。
- 解决方案:存储库使用 webservice 作为存储库,使用 webservice 数据构建聚合,稍后保存更改。
- 疑惑:这样可以吗?
场景 #2
- 情况:我需要对数据库中的一些数据执行业务操作,并且此类操作涉及用于计算值的网络服务。
- 解决方案:将webservice抽象为领域服务,因此将其作为参数传递给聚合来执行操作。
- 疑问:这有意义吗?我会说 web 服务应该是存储库的一部分,所以数据是一起持久化的。
场景 #3
- 情况:业务运营request/saves数据from/to两个不同的数据库,一个直接访问,另一个通过webservice访问。
- 解决:数据库连接和webservice都在版本库里,一起保存。聚合不知道此拆分。
场景 #4(问题开始...)
- 情况:我需要对数据库中的数据执行业务操作,这涉及到 webservice 操作,业务操作本身需要该结果。
- 问题:网络服务不是事务性的,也不在我的控制之下,尽管它是幂等的。对 webservice 的调用取决于聚合必须加载的一些数据,聚合逻辑取决于 webservice returns(想象一下在线支付)的结果代码。
- 解决方法:webservice作为domain service使用,如果请求失败则全部失败,稍后重试。如果保存时数据库失败,也会重试。
场景 #n [#4 的许多变体] ...
所以我无法就如何在 DDD 中更改状态时使用 Web 服务制定明确的标准。
理想情况下,我会在我的存储中进行更改,然后拥有某种集成层来复制外部数据库或服务中的这些更改,例如事件。但是,大多数情况下存在写后读一致性要求,如果您在操作 returns 后刷新,屏幕上会显示正确的数据(例如:来自同一网络服务的付款收据付款已完成)。
另一个例子:一个屏幕允许保存关于一本书的数据,但是书籍规格保存在数据库中,描述(多种语言)保存在外部服务中。操作必须一致,如果用户单击 "Save" 并刷新,屏幕需要一起显示,不能只显示带有旧翻译的规范或根本不显示翻译,因为它们正在被复制到另一个数据库。
哪个是可靠的决定标准?
答案是阅读Life Beyond Distributed Transactions。
简而言之,尝试可靠地协调不同位置的写入非常昂贵。在大多数情况下,最好在您的设计中承认您不能同时出现在所有地方,并投资于处理该事实的后果。
我一直遵循的规则是 Aggregate 应该是纯粹的,不依赖于进行 IO 调用的服务(即磁盘或网络)。每次在给定状态下执行命令时,它都应该给出相同的结果。注入服务或将其作为方法调用中的参数传递违反了此规则。抽象的想法是聚合永远不应该根据它不拥有的数据做出决定。
然而,每个复杂系统包含的不仅仅是聚合体。它还包含 Sagas/Process 对业务流程建模的管理器(从现在开始只是 Saga)。要设计一个完美的 Saga,只需要一个清晰的业务流程和幂等的端点。
一个 Saga 启动,然后监听域中的变化,通常是通过订阅域事件。它通过向相应的端点发送命令来对它们做出反应。请注意,我使用术语端点来指代以幂等方式处理命令的任何接收器。纯粹的聚合就是这样的端点。但是 Saga Endpoint 也可以是外部系统,比如支付网关。这样的网关不应该使用现有的PaymentID
(您的系统发送的不透明字段)发起新的支付。
结论:据我所知,你所有的场景都可以实现为Sagas。