清晰的架构边界

Clean architecture boundaries

我对 clean architecture 比较陌生,我对使用 CA 进行后端 spring 项目有一些疑问。

  1. application 必须(对 CA 来说是一个好习惯)是 modularized 还是可以是一个文件夹中的模块拆分?

  2. 关于业务逻辑,应该只在用例上吗?

    我可以在任何图层中使用 "Validators" 吗?
      例如
        关于大小、可为空、可选字段的验证可以在 InputRequest 对象上吗?或者在 Controller 本身中调用 PaginationValidator 例如?
        存储库中的验证检查当我有以下业务规则时是否应该更新字段:"If the field is null, does not update, otherwise update it"。
        如果我有一个调用网关的用例,网关也可以调用一个验证器吗?或者验证必须发生在用例中?
  1. 存储库

      存储库是网关吗?如果可以,它适合哪一层?
      假设我在数据库上有两个实体,部门和员工。业务规则为:"to create a Department I MUST create its employees too, and only through Department"。
        遇到这种情况我该怎么办?
        我是否应该创建两个存储库,因为我只能在创建部门时创建员工,我是否应该通过部门存储库从员工的存储库调用 create()?
        即使在创建部门时只能创建员工,我是否应该有两个用例(一个用于创建部门,一个用于创建员工)?
        如果我有两个用例,我应该通过部门用例调用员工用例还是创建另一个用例来编排它们?
        我应该只创建一个用例并创建一个网关来编排存储库吗?
        还有其他选择吗?
      另外关于版本库,我可以通过版本库调用Usecase吗?这让我很疑惑,因为理论上Usecase和repository会有接口,它的实现可以在同一层,但是箭头是向前向后的不是吗?
  2. DTO

      它可以有第二个构造函数期望 "Input" 并且这个构造函数填充相应的字段吗?例如具有构造函数的部门需要 DepartmentRequest
      如果我必须创建三个 "layers" 域,是否有任何规则?比如 DepartmentRequest、Department 和 DepartmentEntity?如果不能,我可以只为一个实体设置两个或一个对象吗?
      所有转换都应该通过 assembler/converter 吗?


我已经在互联网上搜索过,但是所有这些信息似乎都过于抽象,有时甚至是模棱两可的,如果有人可以通过示例给我解释,我将不胜感激! 谢谢!!

Does the application must be modularized or can it be one module splitted in folders?

我可以模块化,但一定不能。模块化在 Java 或 C++ 等编译语言中具有优势,因为您不能在模块之间引入循环。例如。如果在 Java 中,JAR B 依赖于 JAR A,则不能引入从 JAR A 到 JAR B 的依赖关系,因为这会引入一个循环,然后在构建 JAR B 之前无法构建 JAR A,反之亦然.

根据您的需要,拆分为文件夹、包或命名空间(取决于您的语言如何命名)是有意义的。看看我对Who must create services with package modifier if I implement architecture from “Clean Architecture” book.

问题的回答

About the Business Logic, should it be only on Usecases?

所有业务规则都应该放在用例层或以下,因为干净架构的目标是让这部分易于测试。应用程序特定的业务逻辑应放在用例中,实体中的应用程序不可知规则。

About inputs, could it be be on controllers or in domain(request) it self?

将特定于控制器的验证与业务验证分开。控制器特定应放在控制器级别,业务特定在用例 and/or 实体层。

例如如果你有一个消耗 JSON 的休息控制器并且这个 JSON 包含一个日期字符串。然后控制器负责将字符串解析为日期对象,例如作为 ISO 日期或者它可能会尝试解析不同的格式。当您将解析的日期对象传递给用例时,用例负责验证此日期在用例上下文中是否有效。例如。如果您有一个用例“ScheduleMeeting”,日期应该是未来的日期。

用例层应该抛出像 ScheduledTimeElapsedException 这样的业务异常。控制器层可以捕获此异常并决定要做什么,例如休息服务可能 return HTTP 状态代码 and/or 带有更详细描述的 JSON,可能是国际化描述,具体取决于请求客户端的语言设置。

Repository

Is a repository a gateway? if so which layer it fits?

存储库是用例需要从中获取实体以及将实体放入其中的接口。

网关是知道如何联系外部服务或数据库以从中获取数据和将数据存储到其中的实现。

Let's suppose I have two entities on DB, Department and Employee. The business rule is: "to create a Department I MUST create its employees too, and only through Department". In this situation what should I do?

Should I create two repositories and as I can only create employees when I create the Department, should I call the create() from Employee's repository through the Department repository?

您应该应用接口隔离原则并创建一个用例特定的存储库接口。这个接口应该只定义用例需要的方法。由于业务规则是同时创建两者,因此该界面可能如下所示。

public void MyUseCaseRepository {
    public void persistDepartment(Department dep, List<Employee> employees);
}

如果应用接口隔离原则,还应回答以下其他问题。

Should I create just one Usecase and create a gateway to orchestrate the repositories?

然后实现可以负责存储两者或 none。

您现在可能会想“但是后来我将业务逻辑移到了网关中。”。阅读我对问题 .

的回答

Also about repository, can I call a Usecase through a repository? This makes me very confused, because theorically, the Usecase and the repository will have interfaces, and its implementations could be on the same layer, so is there any rule about it?

clean architecture 并没有真正给出关于这个的声明。存储库实现与控制器一样位于接口适配器层中。因此理论上他们可以访问用例。但我认为这不是作者的本意。我认为 Bob 大叔认为它更像是六边形架构,存储库是适配器,而控制器是端口。我不会从存储库实现中调用用例。最后这个问题只能由作者来回答了

DTOs

May it have a second constructor expecting a "Input" and this constructor fill the respective fields?

我猜你的意思是 ResponseModel 当你说“输入”时 return 由用例编辑。

从体系结构的角度来看,这是可以的,因为 DTO 属于控制器或接口适配器层,因此依赖方向是正确的。

但是当 DTO 的构造函数读取响应模型并填充它的字段时,它还会将响应模型的值转换为其 DTO 表示形式。也许您想将此转换与 DTO 本身分开,并将其放在可以单独测试的演示器实现中。

Is there any rule if I have to create three "layers" of domains? Like DepartmentRequest, Department and DepartmentEntity? If not can I have only have two or one object for an entity?

您可以在干净的架构中看到规则。依赖箭头从外层开始,到下一个内层结束。没有箭头指向例如从控制器层到实体层。因此,如果您对架构非常严格,则控制器不应访问实体。

如果您将所有内容都放在一个对象中,例如Department 几乎所有东西都使用它,例如作为 DTO,作为数据库实体和域实体,然后你将所有这些不同的职责结合起来。

因此,如果您修改 DTO 表示在数据集中的存储方式,则很容易破坏它,反之亦然。如果您想修改业务规则,您也可以轻松地破坏数据库表示,反之亦然。最后,如果您修改业务规则,您还可以打破 DTO 表示。

这些对象看起来很相似,但由于服务于不同的客户端,它们的职责不同。因此,如果将所有内容都放在一个对象中,就会违反单一职责规则。

我不能告诉你单一责任违规的所有影响,因为这远远超出了这个问题。

  1. 想想Bob,如果软件environment/Lang没有模块的概念,他不会用这个模式吗?

架构就是架构,文件夹和模块不是架构,它们是权衡。模块可能有助于设计它,因此业务层无法访问诸如 jdbc 之类的东西,代价是更多的模块定义。

  1. 用例是应用程序的 public api,因此验证就在那里。用例应仅实现桥接模式中的桥接,桥接的另一端是例如一个存储库。

桥梁(例如接口和边界类)充满了需求。用例非常苛刻,可能需要同一操作的 5 或 10 个不同版本。它可以表示为 en Enum 或存储库中的 10 个方法,这样的决定是交易 offs/style/etc.

控制器将验证请求是否有效(将它们想象成超市中的钞票检查器)。但是业务逻辑(POS)将验证优惠券是否有效以及价格的折扣是多少。

硬币分发器的存储库可以决定它是否应该分发许多小硬币以及可能以何种货币分发。投币器可以验证机器中是否有足够的硬币。

不同层有不同的验证。在类型语言中,许多验证是自动的,例如你声明了一个 UUID,如果它实际上不是,那么它将在运行时出错。

  1. 您正在混合来自其他模式的抽象。存储库实现了用例的桥梁。来自用例的需求。在业务应用中,repository中的publicapi是它强制实现的所有bridges的总和。如果存储库选择在 API 层中实现所有桥接,则需要权衡取舍。

通常,Department 和 User 是直接从您的心智模型中映射出来的,但这并不是您设计软件时的最佳方式。

用例应该在 NewDepartment 和 NewDepartment 存储库等上运行。用户和部门甚至可能不存在于软件中。相反,存在 20 种不同的属性组合,它们会一起变化。你可以用复仇者联盟的名字或一些有意义的名字来称呼他们。应该尊重无处不在的语言,但如果不适用,“Thanos”可能是模块或概念的好名字。大家知道Thanos在你的项目中做了什么,就是清除数据的模块或者w/e。或者,也许您将“欢迎”作为新部门等的概念。同样,尝试与无处不在的语言相协调,并尝试使自己摆脱事物不会一起改变的思维模式。例如。客户状态和客户名称通常在完全不同的时间更改。

每个用例通常都有一个定义明确的桥,因此有一个专门的存储库。

  1. 权衡。不要将您的 public API 耦合到数据库,例如不要 return 数据库元组。将元组转换为对象并没有改变这一事实,因此通常至少需要一次转换。当业务逻辑中有很多代码时,通常还需要进行二次转换,以允许更改数据库。