【问题标题】:Are collections of inner aggregates valid?内部聚合的集合是否有效?
【发布时间】:2019-07-07 19:27:35
【问题描述】:

假设我有一个名为 Folder 的 AggregateRoot,其中包含如下子文件夹的集合:

class Folder : AggregateRoot
{
  string Name { get; }
  ICollection<Folder> Folders { get; }
}

这里的内部集合实际上只是其他文件夹聚合的聚合 ID 列表,在枚举时会延迟解析。

此构造在聚合不仅引用其他聚合而且还将其文件夹属性定义为其他文件夹聚合的集合的域建模中有效吗?

为什么?上面的例子可能不是特别好,但我的目标主要是有一种自然的方式来处理聚合集合,并隐藏 agg-references 是通过表面下的存储库解决的事实。我想像使用实体集合一样轻松地使用聚合。

我在这里的想法也是,子文件夹在某种程度上归父聚合所有,集合确实是一种表示存储聚合的地方的方式,即使存储聚合并不是真的如此更一般地通过存储库。

递归的例子并不重要。重点是聚合“似乎”拥有其他聚合的事实。当在两个文件夹中进行更改时,当然只能一个一个地保存它们,但这应该没问题。我还必须包含一些规则,即文件夹只能在适当的位置创建而不是手动添加,以便它们可以出现在多个 agg-collection 中。

【问题讨论】:

    标签: domain-driven-design aggregateroot


    【解决方案1】:

    子结构是域建模中的有效用例,并且经常在递归概念中遇到,如组、标签等,就像在您的文件夹示例中一样。而且我喜欢将它们作为域层中的纯集合对象来处理,没有任何持久性逻辑的暗示。在编写此类域逻辑时,我喜欢想象我正在处理对象,就好像它们将永久保存在 RAM 上一样。

    我将考虑您的递归示例进行解释,但同样的概念适用于子对象的任何“集合”,而不仅仅是递归关系。

    以下是伪代码的示例实现,使用 cmets 进行注释。我提前道歉,代码在结构上更接近 Python。我想准确地传达这个想法,而不是担心如何用我不精通的 C# 来表示它。如果有不清楚的地方,请询问有关代码的问题。

    伪代码说明:

    1. 在域层中,您可以像处理另一个列表/集合一样简单地处理集合,而不必担心底层持久性的复杂性。
    2. FolderService 是一个 ApplicationService,通常由 API 调用。该服务负责组装基础设施服务、与领域层交互以及最终的持久性。
    3. FolderTableFolder 对象的虚构数据库表示。 FolderRepository 知道这个类及其实现细节
    4. 从 DB 中保存和检索文件夹对象的复杂性仅存在于 FolderRepository 类中。
    5. load_by_name 存储库方法急切地加载所有子文件夹并将其填充到父文件夹中。我们可以将其转换为延迟加载,仅在访问时加载,除非我们正在遍历,否则永远不会加载它(甚至可以根据要求进行分页,特别是如果对子文件夹的数量没有特定限制)
    class Folder(AggregateRoot):
        name: str
        folders: List[Folder]
    
        @classmethod
        def construct_from_args(cls, params):
            # This is a factory method
            return Folder(**params)
    
        def add_sub_folder(self, new_folder: Folder) -> None:
            self.folders.append(new_folder)
    
        def remove_sub_folder(self, existing_folder: Folder) -> None:
            # Dummy implementation. Actual implementation will be more involved
            for folder in self.folders:
                if folder.name == existing_folder.name:
                    self.folders.remove(existing_folder)
    
    
    class FolderService(ApplicationService):
    
        def add_sub_folder(parent_folder_name: str, new_folder_params: dict) -> None:
            folder_repo = _system.get_folder_repository()
            parent_folder = folder_repo.load_by_name(parent_folder_name)
    
            new_sub_folder = Folder.construct_from_args(new_folder_params)
            parent_folder.add_sub_folder(new_sub_folder)
    
            folder_repo.save(parent_folder)
    
    
    class FolderTable(DBObject):
        # A DB representation of the folder domain object
        #   `parent_id` will be empty for the root folder
        name: str
        parent_id: integer
    
    
    class FolderRepository(Repository):
        # Constructor for Repository
        #   that has `connection`s to the database, for later use
    
        # FolderRepository works exclusively with `FolderTable` class
    
        def load_by_name(self, folder_name: str) -> Folder:
            # Load a folder, including its subfolders, from database
            persisted_folder = self.connection.find(name=folder_name)
    
            parent_identifier = persisted_folder.id
            sub_folders = self.connection.find(parent_identifier)
            for sub_folder in sub_folders:
                persisted_folder.folders.append(sub_folder)
    
            return persisted_folder
    
        def save(self, folder: Folder) -> None:
            persisted_folder = self.connection.find(name=folder.name)
            parent_identifier = persisted_folder.id
    
            # Gather the persisted list of folders from database
            persisted_sub_folders = self.connection.find(parent_identifier)
    
            for sub_folder in folder.folders:
                # The heart of the persistence logic, with three conditions:
    
                # If the subfolder is already persisted,
                #   Do Nothing
    
                # If there is a persisted subfolder that is no longer a part of folders,
                #   Remove it
    
                # If the subfolder is not among those already persisted,
                #   Add it
    

    如果您在此实现或我的思考过程中发现漏洞,请务必指出。

    【讨论】:

    • 感谢您的回答!但问题不是关于如何实现它,而是更多关于从 DDD 角度的建模选择,特别是如果它是一种常见的方法,或者它是不好的做法,或者只是奇怪和错误?我担心的是,内部聚合看起来好像是父聚合的一部分,就像正常的内部实体一样,它可能会让“消费者”感到困惑,或者在某种程度上我还没有预见到真的很糟糕.
    • 明白。这是我反复用于递归关系和子对象的一种方法。从来没有觉得它会在理解或建模方面造成问题。构建后非常直观。
    【解决方案2】:

    当一个聚合包含另一个聚合时,应避免直接引用,因为通常会带来一些不必要的痛苦。

    任何 延迟加载 都表明您可以重新设计一些东西,因为您也应该避免这种情况。

    最常见的模式是只有一个 id 列表或一个值对象列表。后者似乎更适合您的情况。然后,您始终可以拥有一个包含所有相关文件夹的完全加载的 AR。为了导航,您需要检索相关文件夹。

    这个特殊的例子有一些特殊性,因为它代表了一个层次结构,但你必须根据具体情况来处理这些。

    简而言之:无论是通过集合还是其他方式,对另一个进行聚合引用都是不明智的。

    【讨论】:

      猜你喜欢
      • 2020-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-03
      • 2023-03-31
      • 1970-01-01
      相关资源
      最近更新 更多