【问题标题】:Clean architecture cascaded deletion of resources清理架构级联删除资源
【发布时间】:2021-10-15 15:10:24
【问题描述】:

目前,我一直在尝试使用“干净架构”转换代码库,以尝试分离许多事物,创建定义更好的接口,并使代码更易于测试。

现在,我们拥有以分层方式相关的资源。我们可以这样想:

A
|-B
| |
| C
|
|-D
  |-F
  |-G
    |-H
    |-I

这是一个非常粗略的示例,其目的不仅仅是准确描述我正在使用的内容。

在这种情况下,我可能有一些“用例”,我想在其中“删除 A”。在我们的例子中,当我们删除这个层次结构中的一个类型时,我们想要删除所有关联的数据,包括子类型的数据。这是我不确定如何准确处理事情的地方。我已经考虑过几种方法,正在寻找更好的选择。

第一个是有一个“UseCase”,它具有所有这些功能(删除所有类型),因此可以引用自己来调用其他删除方法。我真的不喜欢这样,因为在我看来它违反了“开放/封闭原则”,而且似乎很难维护。

第二种方法是让“UseCase”“DeleteA”依赖于“DeleteB”和“DeleteD”(例如)。然后“DeleteA”将具有“DeleteA & DeleteB & DeleteD”的依赖关系。这似乎很难测试,因为现在它实际上取决于每个子类型“删除”“用例”的接口。

我正在努力解决这个问题。我考虑过一种我也不太喜欢的服务定位器模式。似乎它使依赖关系不清楚,它们只会在运行时得到解决(没有编译时检查)。

欢迎提出任何建议。我发现很难找到有关此类案件的信息。我发现的示例似乎与“DoSomethingWithX”之类的东西相当孤立,其中结果不会跨多种类型级联。

【问题讨论】:

    标签: typescript dependency-injection architecture clean-architecture


    【解决方案1】:

    我假设您拥有的资源是用例/应用程序逻辑层知道的实体。我对Clean Architecture 有所了解,但不足以知道我要说的话是否违反了它的任何特定规则。

    你是如何接触这些实体的?他们是像 DTO 一样“愚蠢”还是有一些聪明才智?例如。实体是否知道它的直系子女是谁?例如。在您的示例中,D 会知道 F 和 G。

    想到的选项:

    哑实体 - 如果您的实体的智能为零,App-Logic 将通过获取所有可能相关的实体来强制进行补偿,然后管理它们的删除。优点/缺点:

    • 删除逻辑/智能都干净地保存在一层(应用逻辑层 - 忽略所有应该在其适当层中的基础 CRUD 代码)。
    • 性能可能会有所不同,并且部分取决于您获取实体树并对其进行处理的效率。
    • 实体可以保持超级愚蠢。 (也许太笨了?)
    • 逻辑可以执行它需要的任何操作,以确保可以安全地删除实体。例如。确保逻辑的其他部分不会试图改变任何要删除的东西的状态;确保删除符合ACID

    “我知道我的直系子女”实体 - 如果实体的智能非常有限:他们知道他们的直系子女是谁,但其他的不多。这将允许递归。逻辑层可以循环,询问第一个实体:“嘿,有孩子吗?”如果为真,则获取这些内容,将其添加到实体的平面列表中,并询问相同的问题,直到您只得到错误信息,然后退出循环。然后逻辑可以删除平面列表上的所有实体。优点/缺点:

    • 删除逻辑/智能都干净地保存在一个位置(应用逻辑层 - 忽略应该在其适当层中的所有基础 CRUD 代码)。
    • 实体树越大,性能可能会受到影响,您需要执行的递归越多。
    • 假设实体树没有被外部逻辑部分改变(例如,将子对象添加到尚未删除的实体中 - 但只是告诉逻辑它没有子对象)。我想你可以研究一些标志系统,这样逻辑就不会违反自己。可能有针对该特定问题的设计模式,但我想不出任何想法或快速搜索。
    • 实体可以保持相对笨拙,但仍有一些基本有用的方法。确保您建立清晰一致的设计规则,说明什么是足够“愚蠢”且足够有用,以便添加到实体中而不会使它们膨胀并意外地将它们变成逻辑(不干净的架构)。

    删除机制应该能够处理各种类型,只要它们实现给定的接口,例如可删除。

    最后,考虑一下哪种方法最适合您的情况:基于递归(第二个选项)或基于集合(第一个选项),您可以在其中建立所有内容的列表,然后删除。考虑正在做出哪些运行时假设(任何东西都可以作用于您要删除的实体),并平衡运行时需求与设计时需求。

    不要忘记您可以“软”删除内容。例如。递归设置一个标志,在对范围内的所有软删除项进行基于集合的删除之前进行最后一分钟检查。

    更新 RE 软删除/OP 评论

    好吧,如果您有一个仅代表其他地方的物理记录的实体(数据库中的行、文件等),您可以软删除该实体,并将其用作数据访问/文件系统代码的信号继续执行实际的删除操作。

    软删除可以是一个布尔值,或者您可以定义一个具有更复杂状态的标志,例如NormalMarkedForDeletionDeletionInProgressDeleted 或其他。

    【讨论】:

    • 请注意,软删除不一定适用于所有事情。在我的情况下,实际上其中一些不在数据库中,实际上是二进制文件,其中包含大小可能为 GB+ 的数据。这实际上是在我们的案例中需要删除关联资源的原因之一。回顾你的整个反应并考虑它。如果我没有其他问题,我会标记它。我非常感谢您的回复??
    【解决方案2】:

    我猜“删除”是指持久层中的删除。如果是这样,则应将级联删除放在此处而不是在用例中。

    您可能有一个 CMS 系统,用户可以在其中为网络创建页面。这些页面包含部分、图像,可能有附件等。用例“删除页面”可能会在请求存储库删除页面之前执行一些验证逻辑,但随后存储库负责删除。存储库的作用很大程度上取决于您使用的存储类型。例如。如果“页面”是您的聚合根并且存储库使用面向文档的 nosql db,它只会删除文档,并且所有包含的内容(例如图像、附件)都消失了。如果持久层使用关系数据库,它可能会从不同的表中删除行,或者它也只删除页面行,其余的由数据库的级联删除配置完成。

    因此我认为它应该放在持久层中。

    编辑

    就我而言,数据不一定只存储在数据库中,还可以通过不同的存储介质存储(例如某些文件存储中的二进制文件)。

    ....

    您的建议似乎需要引用其他存储库的存储库。这是你的意思吗?

    UserRepo 可以在 DocumentRepository 上调用 delete,但您也可以使用接口反转依赖关系,例如一个UserChangedHandler,它有一个方法onUserDeleted()

     +------------+        +--------------------+
     | DBUserRepo | -----> | UserChangedHandler |
     +------------+        +--------------------+
                                     ^
                                     |
                   +-----------------+--------------+ 
                   |                                |
     +------------------------+        +---------------------+ 
     | FileDocumentRepository |        | RestImageRepository |
     +------------------------+        +---------------------+
    

    如您所见,ChangeHandler 可以由任何其他存储库实现。它可能会删除存储在本地文件系统上的Documents 和存储在远程服务器上的Images。

    存储库实现UserChangeHandler,或者您创建一个适配器以将处理程序逻辑与存储库分开。

     +------------+        +--------------------+
     | DBUserRepo | -----> | UserChangedHandler |
     +------------+        +--------------------+
                                     ^
                                     |
                    +---------------------------+        +------------------------+ 
                    | DocumentUserChangeAdapter |  --->  | FileDocumentRepository |
                    +---------------------------+        +------------------------+
    

    在分布式环境中,ChangeHandler 也可能向队列发送消息,其他系统将异步删除其他数据。

    您的描述似乎还意味着“用户回购”将了解整个数据层次结构(用户 -> SomeData -> SomeOtherData),因此每当层次结构中发生任何变化时,都会影响“用户回购” .

    正如我在上面试图展示的,这并不一定意味着UserRepo 实现知道所有其他数据结构和持久性类型。持久性管理不能驻留在同一台服务器上,也不能在同一进程中运行。

    如何构建系统由您决定。有许多不同的方法可以解决它,具体取决于功能和非功能需求。

    PS:您使用 typescript 标记您的问题,因此您可能希望像这样定义UserChangeHandler

    export type UserChangeHandler = (userId : string) => void;
    

    如果更改处理程序应支持异步,则返回 Promise<void>

    【讨论】:

    • 就我而言,数据不一定只存储在数据库中,还可以通过不同的存储介质(例如某些文件存储中的二进制文件)存储。还有其他存储库可以处理这些不同类型数据的访问/管理。您的建议似乎需要引用其他存储库的存储库。你是这个意思吗?例如,如果我有“用户”和“文档”。 “User Repo”将引用“Doc Repo”,以便它可以告诉“Doc Repo”来管理删除?您的描述似乎假设所有数据都在一个地方。
    • 您的描述似乎还意味着“用户回购”将了解整个数据层次结构(用户 - > SomeData - > SomeOtherData),因此每当层次结构中发生任何变化时,它都会影响“用户回购”。
    • 这并不一定意味着UserRepo 实现知道所有其他数据结构。 UserRepo 可以在也委托的其他存储库上调用删除。您甚至可以使用UserDeletionEventHandler 之类的接口或更好的名称来将依赖关系反转到其他存储库。
    猜你喜欢
    • 2017-06-24
    • 2020-09-14
    • 2012-04-27
    • 1970-01-01
    • 2014-12-20
    • 1970-01-01
    • 2011-03-01
    • 1970-01-01
    相关资源
    最近更新 更多