内部集合的集合是否有效?

时间:2019-07-07 19:27:35

标签: domain-driven-design aggregateroot

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

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

这里的内部集合实际上只是其他Folder-aggregates的聚合ID列表,在枚举时会延迟解决。

这种结构在域建模中是否有效?在域建模中,聚合不仅引用其他聚合,还将其Folders-Property定义为其他Folder-aggregates的集合?

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

我在这里的想法还在于,子文件夹在某种程度上归父集合所有,集合实际上是一种表示集合存储位置的方式,即使存储集合不是真的也是如此通常是通过存储库。

具有递归性的示例并不十分重要。重点在于以下事实:集合“似乎”拥有其他集合。而且,当在两个文件夹中进行更改时,当然只能一一保存,但这没关系。我还必须包含一些规则,即只能在适当位置创建文件夹,而不能手动添加文件夹,以便它们可以出现在多个agg集合中。

2 个答案:

答案 0 :(得分:0)

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

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

下面是伪代码的示例实现,并带有注释。我先向您道歉,该代码在结构上更接近于Python。我想准确地传达这个想法,而不用担心如何用C#来表达它,因为我对C#并不熟悉。如果不清楚,请询问有关代码的问题。

有关伪代码的注释:

  1. 在域层中,您像对待另一个集合/集合一样简单地处理集合,而不必担心底层的持久性复杂性。
  2. FolderService是通常由API调用的ApplicationService。该服务负责组装基础结构服务,与域层交互以及最终的持久性。
  3. FolderTableFolder对象的假想数据库表示。 FolderRepository一路了解此类及其实现细节
  4. 仅在FolderRepository类中存在从数据库保存和检索Folder对象的复杂性。
  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

如果您发现此实现或我的思想过程有漏洞,请指出。

答案 1 :(得分:0)

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

任何 lazy-loading 都会表明您可以重新设计一些内容,因为您也应该避免这种情况。

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

这个特定的示例具有某些特殊性,因为它表示层次结构,但是您必须根据具体情况来处理。

简而言之:不建议任何聚合引用另一个,无论是通过集合还是其他方式。