干净的架构:结合交互器

44

我最近偶然发现了Uncle Bob的Clean Architecture,我很想知道Interactor能否执行其他Interactor。

例如,这是我的Interactor:getEmptyAlbums, getOtherAlbums。它们都有回调函数,分别返回一个专辑列表(Album模型的ArrayList)。

我可以创建一个名为getAllAlbums的Interactor,在其run块中执行前两个Interactor吗?

@Override
public void run() {
    getEmptyAlbums.execute();       
}

void onEmptyAlbumsReceived(ArrayList<Album> albums){
     getOtherAlbums.execute;
}
void onOtherAlbumsReceived(ArrayList<Album> albums){
     mMainThread.post(new Runnable() {
         callback.onAlbumsReceived(albums);
     }
});
6个回答

51

我一直在思考同样的问题,但发现关于这个主题的内容很少,我得出结论,“是的”,这 可能 是最好的选择。

我的理由如下:

  1. 单一职责原则:如果您无法聚合使用案例,则每个案例就无法真正做到单一职责。没有聚合,这意味着领域逻辑最终会出现在表示层中,从而违反了目的。
  2. DRY原则:可以共享用例,并且在有意义的情况下应该 共享。只要用例的意图相同即可。显然,在执行此操作之前,应仔细考虑。在我看来,除了下一个要点以外,很少有必要这样做。
  3. 协调器类:例如,如果您需要获取多个数据源并将其保存到存储库中,则需要运行所有这些子用例的用例,以确保正确实现操作顺序和并发等内容。我认为这是调用其他用例的最有力的原因。

为了保持单一职责原则,我建议限制汇总使用案例仅执行那些用例并进行任何最终转换。

鉴于这个问题的年代,我很想知道您采取了哪种方式以及遇到的问题。


3
我刚才为这个问题运行了一个搜索,由于DRY原则倾向于“是”。我可以看到一个 Interactor 对象(用例)创建一个新的 RequestModel 并将其传递给另一个单独的 Interactor 对象。但是,正如你所说,关于这个主题几乎没有什么信息。 - emeraldinspirations
4
我使用了执行其他交互器的交互器,以避免我的表现层过于混乱和庞大,并且没有遇到任何问题。 - Chris Rohit Brendan
1
我以同样的方式看待它。关于这个主题的更详细示例,请查看https://plainionist.github.io/Implementing-Clean-Architecture-UseCases/。 - plainionist
2
根据我的经验,永远不要这样做或者得到意大利面条代码 :-) 交互器应该独立改变,因为它们是应用程序的不同部分。如果你让交互器有机会使用另一个交互器 - 某些事情就会出错,你应该将逻辑从交互器转移到实体或网关。如果我们谈论这个具体的问题 - 它是完全不正确的。为什么?因为如果你有形容词词汇来描述专辑实体 - 那就是它自己的属性。 - O.O
1
我也是这种方法的粉丝,让用例使用其他用例确实会在它们之间创建依赖关系,但如果我们将这些用例视为编排器,则这些依赖关系实际上是有意义的。我曾经参与过复杂的项目,在没有这种方法的情况下,我们将不得不将业务逻辑移动到表示层,这显然是错误的,并且在业务逻辑发生变化时会产生更多问题。我们需要重构表示层,将逻辑放置在那里。 - Sylwester Szymanski
显示剩余2条评论

33

我的回答是否定的。让我解释一下原因:

  1. 这将打破边界

干净架构最重要的概念之一就是边界。每个用例都定义了一个边界,即系统的一个垂直层级。因此,没有理由让一个用例知道另一个用例的存在。这些垂直层使得各个用例可以独立地进行开发和部署。想象一下,如果我们在团队中合作,你负责开发GetEmptyAlbums用例,我则负责开发GetAllAlbums use case,如果我在我的用例中调用你的用例,那么我们就不是独立开发,也无法实现独立部署。垂直边界被打破了。详见《Clean Architecture》书的第152页和第16章。

  1. SRP原则也会被破坏

假设GetEmptyAlbums业务规则因某种原因而改变,你将需要重构该用例。现在,可能需要接受一些输入。如果GetAllAlbums调用GetEmptyAlbums,那么这个用例也必须进行重构。换句话说,通过耦合用例,您增加了更多的责任。因此,SRP原则被破坏。

  1. DRY原则仍然得到遵守

有两种重复:真实的重复和意外的重复。如果定义了非常相似的两个或更多用例,则会出现意外的重复。这是意外的,因为将来它们可能会因为不同的原因而变得不同(这才是最重要的)。请参见第154页。

  1. 测试变得更加脆弱

与SRP密切相关。如果您更改用例A的某些内容,并且C调用A,则不仅A的测试将失败,而且C的测试也将失败。

总之,答案是否定的,您不能从另一个用例交互器中调用一个用例交互器。但是,这个规则只适用于想要实现纯净的架构方法的情况,而这并不总是正确的决定。

另一个需要指出的事情是,用例必须声明输入和输出数据结构。我不确定您的Album类是否是Entity,但如果是这样,那么就存在问题。正如Bob大叔所说:“我们不想欺骗并传递Entity对象”在边界之间(第207页)。


2
我们可以在不同的用例中重复使用存储库吗?或者每个功能都应该独立于另一个功能? - sanevys
2
每个用例应该有自己的存储库。你会产生意外的重复。但你会得到一个完全隔离的领域、数据和表示层的垂直层。 然而,记住这不是终极软件架构。对于大团队来说确实非常有帮助,但对于小团队来说,在完美地应用它可能会过度冗长。当在另一个用例中重用存储库时,你必须问自己,这个存储库是否需要因为多个原因而更改?并根据此做出决策。你总是可以进行重构。 - GaboBrandX
1
@BenNeill 我同意你的观点,直接调用存储库可以避免中间人使用案例。现在,当我们谈论Uncle Bob的Clean Architecture时,应该将存储库封装起来,只能从它们的交互器中调用。一个原因是存储库返回实体,而展示器不应该使用它们(因为视图不会使用所有数据或调用实体方法)。此外,应该封装存储库以避免从交互器之外使用它们。就像我说的,这就是Clean Architecture建立的。这并不意味着它是每个人或每个时刻的最佳选择 :) - GaboBrandX
1
顺便提一下,关于DRY原则,《The Pragmatic Progammer》的20周年纪念版对该原则进行了一些澄清。它表明,“重复”的代码并不一定意味着违反DRY原则。请参考第31页的第25条提示(不要重复自己)。 - GaboBrandX
据我理解,干净架构中的用例都属于同一边界,因此一个用例依赖于另一个用例不可能会破坏边界。在一个稍微复杂的应用程序中,每个用例都彼此独立似乎是不现实的;从功能上看也不太合理。@GaboBrandX - Konrad
显示剩余6条评论

11
请参考《清晰架构》这本惊人的书籍的第16章。Bob大叔在名为“重复”的部分回答了这个问题。有两种类型的重复:
1. 真正的重复 —— 引入变化会影响多个存在重复代码的地方。 2. 意外的重复 —— 目前代码相似,但背后的思想不同,随着时间的推移,代码变得不同。
在出现真正的重复的情况下,您可以耦合用例,但要小心,因为在意外重复的情况下,随着软件的演变,将它们拆分开来会更加困难。

2
这是一个非常好的观点,只有在两个操作的意图相同的情况下,DRY才适用。 - Ben Neill

2

我对 Uncle Bob 的工作非常陌生,但我也遇到了同样的问题和疑问。

为了保持 SRP 和避免重复(DRY),我将用例与交互器分离。这可能有些过度,但对我来说确实非常有效。

我将用例放在它们自己的文件中,与交互器分开,以便所有单独的交互器都可以使用任何它想要并共享的用例。同时,交互器只需“使用”(导入、依赖等)任何它想要的用例。

这样做使我的交互器非常简单,真正只是一个容器,用于所需的依赖注入和一些类级成员变量。

因此,getAllAlbums、getEmptyAlbums 和 getOtherAlbums 用例变成它们自己的文件,并遵循 SRP。您会得到一个随心所欲地聚合和/或按顺序拼接用例的交互器类。

最近,我还使我的用例仅执行实际的业务逻辑,不包括从依赖注入网关获取的内容,比如数据库或网络调用。然后,我把处理这些依赖网关操作的代码放在操作用例的方法中...

现在,如果您只使用用例中的“黑盒”业务逻辑概念,您可以在不紧密耦合依赖项的情况下进行测试。例如,如果您正在制作游戏“井字游戏”,那么您的用例(快速查看)只会使用“井字游戏”语言,而不是“保存”、“提交”或“提取 [X]”。您可以将测试这些内容保存在交互器测试中或网关本身中。


我在我的实现中也得出了这个结论,@goredefex 的评论很好。 - Cory Robinson
5
用例和交互器不是同一件事吗?我认为 Uncle Bobo 将它们交替使用。 - IronHide
我很想了解你如何区分交互器和用例。此外,你如何在代码库中构建它们的结构(即文件夹结构)@GoreDefex。我以为它们是一样的。 - friartuck
根据我的经验,似乎是否使用强类型语言很重要。我可能错了,但在像Javascript或Python这样的脚本语言中,用例函数就是用例交互器。而在像C#或Java这样的强类型语言中,用例是一个接口,交互器是扩展许多这些用例并因此获得动作的类。 - GoreDefex

0
这个帖子有点旧了,但是我最近遇到了同样的问题,所以我想分享一下我的方法。
我不明白为什么我们不能这样做。当然,这取决于上下文,在多个用例之间创建依赖关系需要经过深思熟虑,但在我的情况下,我发现这种方法比将逻辑放在上层并可能重复代码要可靠得多。
首先,如果您标准化UseCase的输出,特别是在具有一些逻辑的复杂查询中,依赖关系可能不那么强。
其次,我还发现将领域UseCase和应用UseCase分开非常有价值。应用程序可能需要请求多个子用例来处理它们的逻辑,以提供完整的响应。在这种情况下,可以直接在控制器中完成,但是您可能希望在此层中保留一些总体逻辑,以避免重复。
最后,一个用例调用另一个用例与不同服务共同工作非常相似。如果我们真的想避免所有依赖关系,我们可以使用外观模式。但这真的取决于上下文,在我的情况下,我们还没有达到那个阶段,拥有这种“标准化”的用例之间的依赖是完全可以接受的。
关于测试问题,我也不明白为什么它应该更脆弱。每个用例仍然是单独测试的,而且由于你标准化了输出,通过它进行完全可靠的测试。 当然,这取决于应用程序的大小,但如果你处于与许多不同服务一起工作的情况下,也许你应该考虑使用外观模式等模式来避免依赖,并回到“标准化输出”的情况。

1
您的回答目前写得不清楚。请编辑以添加更多细节,以帮助他人理解如何回答所提出的问题。您可以在帮助中心中找到有关如何撰写良好答案的更多信息。 - Community

0

让我们参考主要来源,即书籍《Clean Architecture: A Craftsman's Guide to Software Structure and Design》。

用例是一个对象。它具有一个或多个实现应用特定业务规则的函数。它还包括输入数据、输出数据以及与其交互的适当实体的引用等数据元素。

用例是对自动化系统使用方式的描述。它指定用户提供的输入、返回给用户的输出以及生成该输出所涉及的处理步骤。

系统的用例将在系统结构中明显可见。这些元素将是在架构中占据重要位置的类、函数或模块,并且它们的名称清楚地描述了它们的过程。

在这本书中,有一些关于如何使用用例交互器的场景。但是,并没有提到任何需要调用另一个用例交互器来执行任务的场景。

现在是我谦虚的观点。

用例处于同一级别,彼此之间的通信不违反书中概述的任何原则。

通过遵循以下引用:

它指定了...处理步骤。
在我实现的用例交互器中,这些步骤是来自网关的相应方法。理想情况下,如果每个步骤都有网关实现,我们就不需要使用其他用例交互器,因为这样我们可以将每个用例封装起来,并保护其免受切换到另一个用例的影响。
然而,在我的经验中,有几个时刻我使用了一个用例交互器来调用另一个用例,当它更具常识时。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接