【问题标题】:Is there an elegant solution for handling callbacks from external devices in clean architecture?在干净的架构中处理来自外部设备的回调是否有一个优雅的解决方案?
【发布时间】:2021-11-15 14:17:10
【问题描述】:

我正在构建一个基于简洁架构 (https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 的复杂系统,使用许多外部组件,例如支付终端。提到的支付终端有一个包含基本功能的库,包括一个传递回调的地方,以便终端可以通知交易的进度,例如。 (插入密码或取出支付卡)。

让我们考虑一个场景:

  1. 用户按下“执行事务”按钮,使用某种适配器系统通知用户想要执行事务。
  2. 使用其他类型适配器的系统调用外部 API 并告诉它“执行事务”。
  3. 现在我们想通过外部 API 回调通知用户进度。

为了使用回调 API,我们的一个或多个应用程序类必须实现回调方法,因此必须符合 API 提供者定义的一些抽象。所以我们的类必须依赖于 API。这意味着 API 不能被轻易地模拟或存根。我们必须将回调对象视为该 API 适配器的一部分,并通过模拟或存根它们来测试应用程序的其余部分。

一方面,我不希望架构依赖于外部 API,但创建专门用于处理 API 事件的新实体对我来说似乎是过度设计的。

【问题讨论】:

  • 您能否就您想要/需要什么做一个具体明确的问题?您的系统图也可以提供帮助。谢谢。

标签: callback architecture external clean-architecture hexagonal-architecture


【解决方案1】:

我会在用例层创建一个进度接口,因为它会报告用例的进度。

然后我会实现一个进度展示器,并在从控制器调用它时将其传递给用例。

然后,用例可以通过其接口将进度传递给支付终端适配器。

+------------+     +-------------------+  <<create>>  +------------+
| View Model | <-- | ProgressPresenter | <----------  | Controller |
+------------+     +---------+---------+              +------------+
                             |                              |
UI                           |                              |
=============================|==============================|==========
Use Case                     V                              V
                       +----------+                    +----------+
                       | Progress |                    | Use Case |
                       +----------+                    +----------+
                             ^                              |
                             |                              V
                             |                     +-----------------+
                             |                     | PaymentTerminal |   
                             |                     +-----------------+
                             |                              | 
=============================|==============================|==========
External                     |                              V
                             |                  +------------------------+
                             +------------------| PaymentTerminalAdapter |
                                                +------------------------+

在富客户端应用程序中,ViewModel 通常通过观察者关系连接到 UI 组件,并且会在 ViewModel 更改时导致 UI 更新。在 Web 应用程序中,您要么必须通过某种 websocket 执行此操作,要么将进度保存在某种存储(会话或数据库)中并让客户端轮询值。

【讨论】:

  • 感谢您的回复。参考您的图表,“PaymentTerminalAdapter”必须将外部 API 转换为域对应项。例如,如果终端在描述的枚举中有 3 个回调状态(ENTER_CARD、ENTER_PIN、REMOVE_CARD),那么现在我们必须创建与终端状态等效的域实体。我理解正确吗?
  • 没错。或者你创建一个TerminalProgress,它提供了enterCard()enterPin()removeCard()方法,并且presenter实现完成了更新视图模型所需的工作。
  • @akkolyth 我发布了另一个答案,可能会帮助您做出正确的决定。
【解决方案2】:

我重新思考了我之前的答案。也许有更好的解决方案,因为Terminal 看起来就像另一个 I/O 设备。此方案是否适合您,取决于终端 api 的设计方式。

由于我不知道你的 api 是如何工作的,我会假设一些事情。也许它符合您的要求或给您新的想法。

在终端会话启动时创建一个终端适配器并连接它 给演示者。 您可以调用用例并将其传递给PaymentTerminal 接口。

+-----------+                    
| ViewModel |
+-----------+
      ^
      |
+-----------+     +-----------------+      +------------+
| Presenter | <-- | TerminalAdapter | <--- | Controller |
+-----------+     +-----------------+      +------------+
                           ^                     |
===========================|=====================|===============
                           |                     V
                     +----------+          +----------+
                     | Terminal | <------- | Use Case |
                     +----------+          +----------+

一些伪控制器代码:

public void startPaymentSession(){
    PaymentModel paymentModel = .. // get the model
    PaymentPresenter paymentPresenter = new PaymentTerminalPresenter(paymentModel);

    this.paymentTerminalAdapter = new PaymentTerminalAdapter();
    this.paymentTerminalAdapter.setCallbacks(paymentPresenter);

    UseCase uc = new UseCase(this.paymentTerminalAdapter);
    uc.execute();
}

在这种情况下,我们使用适配器(终端 API 进度实体到 Presenter 进度实体)连接“外部层”终端 API,实际上绕过了用例层。这是否会导致某种架构破坏?无论如何,就我个人而言,这个解决方案对我来说似乎更合理。

由于我不知道您使用的终端 api 我只能猜测...

在我看来,终端 api 正在混合视图和应用程序逻辑。有某种回调注册用于通知用户和处理应用程序逻辑的其他方法。因此,我会将 termial api UI 部分连接到演示者并将应用程序逻辑部分(作为接口)传递给用例。在这种情况下,我认为这不是架构漏洞,因为终端适配器的一部分是 I/O 设备。

这可能是错误的,因为它是基于我的假设。尽管如此,我还是想为我的假设正确的情况展示另一种方法。

最后,我认为将终端 api 视为外部服务还是 I/O 设备并不重要。对我来说,架构的一个目标更为重要——用例、演示者等的可测试性。

【讨论】:

  • 有趣,一方面感谢这个解决方案,我们不必定义更多的域实体,这将对应于终端 API(不幸的是它是一个强制样板),所以这很好。然而,在这种情况下,我们使用适配器(终端 API 进度实体到 Presenter 进度实体)连接“外部层”终端 API,事实上绕过了用例层。这是否会导致某种架构破坏?无论如何,就我个人而言,这个解决方案对我来说似乎更合理。
猜你喜欢
  • 1970-01-01
  • 2015-07-03
  • 1970-01-01
  • 2021-03-20
  • 2022-11-30
  • 1970-01-01
  • 2010-11-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多