清晰架构图

4
以下是经典的干净架构图,来自于叔叔鲍勃的原始博客文章。原始博客文章

Clean Architecture

我对为什么网关比用例更加外向感到困惑,这在所有包含网关或其他形式的数据访问机制的图表中都存在。我理解用例不应绑定到特定的数据访问机制,因为这些应该被隐藏在架构边界后面。同时,形成此边界的服务的唯一用户应该是用例。这与图表相冲突,因为按照视觉规则,用例不能使用网关(而它们需要使用),框架可以使用网关(而它不应该)。我是否遗漏了什么?如果没有,有没有更正确的方式在视觉上表示清洁架构规则?我之所以问是因为我正在创建一个显示清洁架构+DDD有界上下文的图表。

enter image description here

我对DB和服务部分不满意,因为它们位于UseCase圆圈外部,这样就不清楚UseCases是Bounded Context的公共接口,而持久性也无法直接从外部访问。类似这样的解决方案可以解决这两个问题,但会引入一个新问题:没有数据访问层边界的指示。实际上,按照视觉规则,图表暗示Bounded Context中的所有内容都可以直接访问持久性。

enter image description here

我相信我遇到了和原始图表中导航网关“错位”相同的问题,并正在寻找解决方法。


我最终将数据库放在了UC圈之外。这样做并不是非常明显的UC形成了BC的公共接口,但至少依赖关系是清晰的。https://www.entropywins.wtf/blog/2018/09/09/clean-architecture-bounded-contexts-diagram/ - Jeroen De Dauw
5个回答

7
该图显示的是实现的位置,而不是接口。网关接口与用例一起存在。

这解决了“UseCases需要能够访问gateways”的问题,但仍然存在“Gateway实现不应该能够使用UseCases”的问题。或者他们应该可以使用吗? - Jeroen De Dauw
2
@JeroenDeDauw 您可以将接口分离到它们自己的模块中,以避免用例可见。然而,在您的应用程序中,您始终可以访问所有公共类型,因此您有编写“意大利面条代码”的选项。但是,您应该这样做吗?在我看来,某些“封装”必须通过约定强制执行,而不是编译器支持。 - Lars-Erik
@Lars-Erik,我的问题纯粹是关于架构规则的图表和可视化表示,而不是关于如何在代码库中遵循这些规则。 - Jeroen De Dauw
这是来自罗伯特·C·马丁的书《Clean Architecture》的一句引文:“在用例交互器和数据库之间是数据库网关。这些网关是多态接口,包含应用程序对数据库执行的每个创建、读取、更新或删除操作的方法。...这些网关由数据库层中的类实现。” 在“用例和数据库”之间是“接口适配器”。因此,书中明确指出网关接口位于“接口适配器”层,并在“框架和驱动程序”层中实现。 - undefined

1
这张图是Alistair Cockburn的六边形架构的更详细版本,特别是在CDI方面对适配器的使用更加精确。
基本上,这个想法是将依赖关系向内转移:用例可能依赖于实体(即导入与域相关的包并使用域数据类型;Cockburn没有将它们描述为单独的圆圈,而是作为“应用程序”)。左侧的适配器可以调用用例(即执行或中止它们,例如使用命令模式),而用例反过来可以调用接口(如服务或存储库门面,由右侧的适配器实现),使用依赖反转原则来保留入站方向。
如果你对“左”和“右”感到困惑:在Cockburn的图片中,六边形在左侧有适配器(输入/交互机制),在右侧也有适配器(持久性、外部系统、服务)。你可以将其视为一个将域层不依赖于持久性层,但反过来的3层架构图表旋转90度。

再次,Cockburn的解释可能因为不寻常的六边形图像而有些难以理解,但它更加精确,因为它不试图包含所有内容,而是专注于依赖性基础知识。


1
你能解释一下它在非服务器端Web应用程序方面的不足之处吗?(或者提供一个解释的链接) - Jeroen De Dauw
1
回答后续问题:1)在高度分布式系统中,信息流通常是多向的,并且需要在所描述的圆圈之间“跳进跳出”。可能涉及交互者或演示者,但不一定。系统中还经常存在只读服务和视图(读取模型)的组成部分,并且状态是从事件(重新)计算的。当然,您会发现一些原则嵌入了“清洁架构”中,但现实情况会有所不同。 - weltraumpirat
1
在企业环境中,你会发现领域知识、展示逻辑和视图逻辑通常都存在高度分区的遗留系统中,这些系统涵盖了一些信息流程,但并不能像“调用API的适配器”那样干净利落地分离。再次强调,应该应用基本原则(例如,努力实现领域分离和定义清晰的反腐层与干净的接口),但需要更细致的方法来使其奏效。 - weltraumpirat
1
在单页应用程序和移动应用程序中,马丁方便地将其抽象为“设备”,您通常会有本地域逻辑和状态,必须与此图中的Web服务进行同步。一些交互可能看起来像描述的那样,但涉及更复杂的数据流场景。再次强调,底层原则是有用的,但是此图所描述的解决方案不足。 - weltraumpirat
1
我认为假设只有一种真正的“干净架构”设计方式,因此其他任何方式都是“不干净”的,这是危险的。对于不同的系统组件、不同的要求和不同的环境,将适用不同的规则。在一个场景中明智的做法,在另一个场景中可能会成为阻碍。识别有用的模式和原则是好事情 - 但永远记住,架构是权衡和妥协的游戏,本质上是非二元的。 - weltraumpirat
显示剩余2条评论

1
在您的BC图纸中,您缺少一个围绕UC外部的周围圆圈。那个圆圈是基础设施,在那里您应该放置域模型的存储库接口的存储库实现。该实现使用技术(SQL等)访问数据库。
因此,圆圈从内到外依次为:
- 域模型(实体、值对象、存储库(接口)等) - 应用程序层(用例) - 基础设施(UI、存储库(实现)等)
依赖关系向内指向,而不仅仅是下一个圆圈内部(基础设施依赖于应用程序层和域模型)。
我使用六边形架构。

考虑到在我的图表中,Bounded Context 内部不应该有任何 UI 代码,我不确定为什么基础设施会依赖于 UseCases。实际上,在我看来,存储库的实现永远不应该使用 UseCase。你有不同的看法吗? - Jeroen De Dauw
1
如果您的 BC 中没有 UI,则不要在基础架构圆圈中放置任何 UI 组件。基础架构取决于用例,因为一些应用层问题是由基础架构实现的(事务、安全性以及通常不特定于 BC 域的任何问题)。除了这些问题之外,在基础架构圆圈中还存在特定领域接口的实现(如存储库)。架构的依赖规则允许您从存储库的实现中访问用例,但不应该这样做。 - choquero70
1
这取决于开发人员是否会做这些事情。但是,我告诉过你的架构(洋葱架构)允许这样做。这就是为什么我更喜欢端口和适配器(也称六边形架构)。 - choquero70

0

网关接口必须位于用例层,并在接口适配器层中实现,以确保依赖规则。

  Interface Adapters Layer    || Use Cases Layer
                              ||
  +-----------------+         || <implements>    +-------------+   <uses>  +---------+
  |JDBCEntityGateway| --------++-------------->  +EntityGateway|  <------  | UseCase |
  +-----------------+         ||                 +-------------+           +---------+
                              ||

这个模式可以应用于每个架构边界。更高级别的层次定义一个接口,告诉它需要什么而不是如何完成任务。较低级别的层次实现该接口,从而定义其完成方式。也许这就是为什么该层被命名为接口适配器。由此产生的结果是,通过提供另一种实现,您可以改变某些事情的完成方式。现在你可能会认识到这是开闭原则。

但请记住,接口应该是用例需求的稳定抽象。不要在接口中加入特定于实现的内容,例如,如果您像这样指定查找方法List<Eintity> find(String where),因为where字符串是一个细节,可能是SQL或JPQL字符串的一部分。您应该引入一个EntityCriteria,以一种实现无关的方式描述选择标准。


0
我觉得这个问题和下面的讨论非常有趣,因为我们有一个来自用户@RobertMartin的评论,而且他的回答增加了更多的困惑而不是清晰度。
为了保持一致性,只保留一个真实的来源,我不会使用互联网上其他文章中对清晰架构的解释,我也会忽略用户@RobertMartin在这个问题下的回答,他说:
“图表显示的是实现的位置,而不是接口。网关接口与用例一起存在。”
因为这也不符合书籍《Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin (2017)》中所写的内容。
考虑到上述所有内容,如果我们只有这本书,我将从我的角度回答这个问题,根据书中描述的清晰架构的解释以及我个人成功使用“清晰架构”实现的经验。
让我们从这个开始:
“……根据视觉规则,用例不能使用网关(尽管它们需要),而框架相关的东西可以使用它们(尽管它不应该这样做)。”
你知道我思考了同样的问题一个星期,当我试图尽可能地实现干净架构的最接近实现时,我逐字逐句地遵循书中的描述,因为通常情况下,直到你尝试解耦层之后,你才会注意到问题。
让我们看一下书中的一些相关引用,看看它是如何描述的:
“用例是对自动化系统使用方式的描述。它指定了用户提供的输入,返回给用户的输出以及生成该输出所涉及的处理步骤。”
“用例是一个对象。它有一个或多个实现应用特定业务规则的函数。它还有包括输入数据、输出数据以及与之交互的适当实体的引用在内的数据元素。实体对控制它们的用例一无所知。”
数据库网关
在用例交互器和数据库之间是数据库网关。这些网关是多态接口,包含了应用程序对数据库执行的每个创建、读取、更新或删除操作的方法。例如,如果应用程序需要知道昨天登录的所有用户的姓氏,那么UserGateway接口将有一个名为getLastNamesOfUsersWhoLoggedInAfter的方法,该方法以日期作为参数并返回姓氏列表。请记住,我们不允许在用例层中使用SQL,而是使用具有适当方法的网关接口。这些网关由数据库层中的类实现。
“在用例交互器和数据库之间”是什么意思?当然是“接口适配器”层,所以书中给出了一个关于实现网关接口的指导,现在我们知道它应该在接口适配器层中实现,并在外部框架和驱动程序层中进行实现。
但是,如何在不从外部接口适配器层导入网关接口的情况下实现用例呢?
为了缓解这个问题,我决定采用一种叫做“端口和适配器”(也被称为“六边形架构”或“端口和适配器架构”)的技术,这是一种可以帮助实现内核和外层之间更高程度分离的架构模式。在这种模式中,内核定义了接口(端口),由外层(适配器)来实现这些接口,并且依赖关系从内核流向适配器,而内核不需要导入外层的任何特定接口。
我担心如果没有清晰的可视化,很难描述我所指的意思,所以这里有一个在Flutter项目中使用端口和适配器模式的干净架构示例:
应用业务规则
用例:
import 'package:entities/entities.dart';

/// [GetDogsResourceUseCase] is a class with functions that have prominent
/// positions within the architecture, and they will have names that clearly
/// describe their function.
/// From the [GetDogsResourceUseCase], it is impossible to tell whether the
/// application is delivered on the web, on a thick client, or a console,
/// or is a pure service.
abstract class GetDogsResourceUseCase {
  const GetDogsResourceUseCase();

  Stream<Resource<List<Dog>>> callAsStream([Params params]);

  Future<Resource<List<Dog>>> callAsFuture([Params params]);
}

接口适配器
用例适配器:
    import 'package:dogs_app_interface_adapters/dogs_app_interface_adapters.dart';
    import 'package:dogs_app_use_cases/dogs_app_use_cases.dart';
    import 'package:entities/entities.dart';
    import 'package:injectable/injectable.dart';
    
    /// A [GetDogsResourceUseCaseAdapter] specifies the [Params]
    /// to be provided by the user, the [Resource] to be returned to the user, and
    /// the processing steps involved in producing that output [Resource].
    @Injectable(as: GetDogsResourceUseCase)
    class GetDogsResourceUseCaseAdapter implements GetDogsResourceUseCase {
      const GetDogsResourceUseCaseAdapter(this._dogsGateway);
    
      final DogsGateway _dogsGateway;
    
      @override
      Stream<Resource<List<Dog>>> callAsStream([
        Params input = const Params(),
      ]) {
    //TODO: implement
    // step 1
    //_paramsGateway.getSavedDogsAsStream,
    // step 2
    //_dogsGateway.requestDogs,
    // step 3
    //_dogsGateway.saveDogs,
}
    
      @override
      Future<Resource<List<Dog>>> callAsFuture([
        Params input = const Params(),
      ]) {
    //TODO: implement
    // step 1
    //_dogsGateway.getSavedDogsAsFuture,
    // step 2
    //_dogsGateway.requestDogs,
    // step 3
    //_dogsGateway.saveDogs,
}

网关:

import 'package:entities/entities.dart';

abstract class DogsGateway {
  Future<List<Dog>> requestDogs([
    Params params = const Params(),
  ]);

  Stream<List<Dog>> getSavedDogsAsStream([
    Params params = const Params(),
  ]);

  Future<List<Dog>> getSavedDogsAsFuture([
    Params params = const Params(),
  ]);

  Future<void> saveDogs(List<Dog> dogs);
}

框架和驱动程序

网关实现:

@Injectable(as: DogsGateway)
class DogsGatewayImpl implements DogsGateway {
  const DogsGatewayImpl(this._restClient, this._dogsDao);

  final RestClient _restClient;
  final DogsDao _dogsDao;

  @override
  Stream<List<Dog>> getSavedDogsAsStream([
    Params params = const Params(),
  ]) {
     //TODO: implement
     //_dogsDao.streamDogs
  }

  @override
  Future<List<Dog>> getSavedDogsAsFuture([
    Params params = const Params(),
  ]) {
    //TODO: implement
    //_dogsDao.getDogs
  }

  @override
  Future<List<Dog>> requestDogs([
    Params params = const Params(),
  ]) {
      //TODO: implement
      //_getDogsResponse(params)
  }

  @override
  Future<void> saveDogs(List<Dog> dogs) {
    // TODO: Save remote dogs in a local database
    //_dogsDao.updateDogs
  }

如你所见,我成功地实现了书中所呈现的相同图表,而且没有违反依赖规则。
我们来看看你的下一个问题:
“我有什么遗漏吗?”
恐怕是书本或图表有所遗漏。由于我在书籍出版后的6年内写下这个答案,而罗伯特·C·马丁尚未出版《Clean Architecture》的修订版,我们将不得不动用想象力,思考对我们的项目最为适合的方式,以及如何根据我们自己对清晰架构的理解来进行创造性的解读。
接下来是你的最后一个问题:
“如果没有遗漏,有没有更正确的方式来直观地表示清晰架构的规则?”
事实上,是有的。我最终找到了更正确的方式来表示图表,至少在实际实现和概念验证方面,它可以在真实项目中被实施,消除了混淆,并在对象表示抽象接口时添加了标记。

enter image description here


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