是否有一个优雅的解决方案来处理干净架构中来自外部设备的回调?

Is there an elegant solution for handling callbacks from external devices in clean architecture?

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

让我们考虑一个场景:

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

in order to use a callback API, one or more of our application classes must implement the callback method(s), and must therefore conform to some abstraction defined by the API’s provider. So our classes must depend on the API. Which means that the API can’t be easily mocked or stubbed. We have to treat our callback objects as being part of the adapter for that API, and test the rest of the application by mocking or stubbing them.

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

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

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

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

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

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

我重新考虑了我之前的回答。也许有更好的解决方案,因为 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();
}

In this case we connect 'external layer' Terminal API using adapter (Terminal API progress entities to Presenter progress entities) de facto bypassing the use case layer. Whether this would cause some sort of architectural breach? Anyway, personally, this solution seems more reasonable to me.

因为我不知道你用的终端机 api 我只能猜测...

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

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

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