DDD:聚合根

发布于 2024-08-26 23:59:26 字数 862 浏览 11 评论 0 原文

我需要帮助来找到我的聚合根和边界。

我有 3 个实体:计划、计划角色和计划培训。每个计划可以包括许多 PlannedRoles 和 PlannedTrainings。

解决方案 1:起初我认为 Plan 是聚合根,因为脱离 Plan 的上下文,PlannedRole 和 PlannedTraining 没有意义。他们总是在计划之内。此外,我们还有一条业务规则,规定每个计划最多可以有 3 个 PlannedRoles 和 5 个 PlannedTrainings。所以我想通过指定该计划作为聚合根,我可以强制执行这个不变量。

但是,我们有一个搜索页面,用户可以在其中搜索计划。结果显示了计划本身的一些属性(并且没有显示其 PlannedRoles 或 PlannedTrainings)。我想如果我必须加载整个聚合,则会产生很大的开销。有近 3000 个计划,每个计划可能有几个子项。将所有这些对象加载在一起,然后忽略搜索页面中的 PlannedRoles 和 PlannedTrainings 对我来说没有意义。

解决方案 2:我刚刚意识到用户还需要 2 个搜索页面,他们可以在其中搜索计划的角色或计划的培训。这让我意识到他们正在尝试独立地访问这些对象并且“脱离”计划的上下文。所以我认为我最初的设计是错误的,这就是我想出这个解决方案的原因。因此,我认为这里有 3 个聚合,每个实体 1 个。

这种方法使我能够独立搜索每个实体,并且还解决了解决方案 1 中的性能问题。但是,使用这种方法我无法强制执行我之前提到的不变量。

还有另一个不变量,规定只有当计划处于某种状态时才可以更改。因此,我不应该能够将任何 PlannedRoles 或 PlannedTrainings 添加到不处于该状态的计划中。同样,我无法用第二种方法强制执行这个不变量。

任何建议将不胜感激。

干杯, 莫什

I need help with finding my aggregate root and boundary.

I have 3 Entities: Plan, PlannedRole and PlannedTraining. Each Plan can include many PlannedRoles and PlannedTrainings.

Solution 1: At first I thought Plan is the aggregate root because PlannedRole and PlannedTraining do not make sense out of the context of a Plan. They are always within a plan. Also, we have a business rule that says each Plan can have a maximum of 3 PlannedRoles and 5 PlannedTrainings. So I thought by nominating the Plan as the aggregate root, I can enforce this invariant.

However, we have a Search page where the user searches for Plans. The results shows a few properties of the Plan itself (and none of its PlannedRoles or PlannedTrainings). I thought if I have to load the entire aggregate, it would have a lot of overhead. There are nearly 3000 plans and each may have a few children. Loading all these objects together and then ignoring PlannedRoles and PlannedTrainings in the search page doesn't make sense to me.

Solution 2: I just realized the user wants 2 more search pages where they can search for Planned Roles or Planned Trainings. That made me realize they are trying to access these objects independently and "out of" the context of Plan. So I thought I was wrong about my initial design and that is how I came up with this solution. So, I thought to have 3 aggregates here, 1 for each Entity.

This approach enables me to search for each Entity independently and also resolves the performance issue in solution 1. However, using this approach I cannot enforce the invariant I mentioned earlier.

There is also another invariant that states a Plan can be changed only if it is of a certain status. So, I shouldn't be able to add any PlannedRoles or PlannedTrainings to a Plan that is not in that status. Again, I can't enforce this invariant with the second approach.

Any advice would be greatly appreciated.

Cheers,
Mosh

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

草莓味的萝莉 2024-09-02 23:59:26

我在设计模型时遇到了类似的问题,并提出了这个问题,我认为这可能对您有帮助,特别是关于您的第一点。

DDD - 如何实现高性能搜索存储库

当谈到搜索时,我不使用“模型”,而是有专门的搜索存储库返回“Summary”对象......即“PlanSummary”。这些只不过是信息对象(可以认为更像是报告)并且不用于事务意义上 - 我什至没有在我的模型类库中定义它们。通过创建这些专用存储库和类型,我可以实现高性能搜索查询,其中可以包含分组数据(例如 PlannedTraining 计数),而无需在内存中加载聚合的所有关联。一旦用户在 UI 中选择这些摘要对象之一,我就可以使用该 ID 来获取实际的模型对象并执行事务操作并提交更改。

因此,对于您的情况,我将为所有三个实体提供这些专门的搜索存储库,当用户希望针对其中一个实体执行操作时,您始终会获取它所属的计划聚合。

这样,您就可以进行高性能搜索,同时仍然保持单个聚合具有所需的不变量。

编辑 - 示例:

好的,所以我猜实现是主观的,但这就是我在应用程序中处理它的方式,使用“TeamMember”聚合作为示例。用 C# 编写的示例。我有两个类库:

  • 模型
  • 报告

模型库包含聚合类,强制执行所有不变量,报告库包含这个简单的类:

public class TeamMemberSummary
{
    public string FirstName { get; set; }

    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }

    public bool IsAvailable { get; set; }

    public string MainProductExpertise { get; set; }

    public int ExperienceRating { get; set; }
}

报告库还包含以下接口:

public interface ITeamMemberSummaryRepository : IReportRepository<TeamMemberSummary>
{

}

这是应用程序层(在我的情况恰好是 WCF 服务)将消耗并通过我的 IoC 容器(Unity)解决实现。 IReportRepository 与基础 ReportRepositoryBase 一样,存在于 Infrastructure.Interface 库中。所以我的系统中有两种不同类型的存储库 - 聚合存储库和报告存储库...

然后在另一个库 Repositories.Sql 中,我有实现:

public class TeamMemberSummaryRepository : ITeamMemberSummaryRepository
{
    public IList<TeamMemberSummary> FindAll<TCriteria>(TCriteria criteria) where TCriteria : ICriteria
    {
        //Write SQL code here

        return new List<TeamMemberSummary>();
    }

    public void Initialise()
    {

    }
}

那么,在我的应用程序层中:

    public IList<TeamMemberSummary> FindTeamMembers(TeamMemberCriteria criteria)
    {
        ITeamMemberSummaryRepository repository 
            = RepositoryFactory.GetRepository<ITeamMemberSummaryRepository>();

        return repository.FindAll(criteria);

    }

然后在客户端中,用户可以选择这些对象之一,并对应用程序层中的一个对象执行操作,例如:

    public void ChangeTeamMembersExperienceRating(Guid teamMemberID, int newExperienceRating)
    {
        ITeamMemberRepository repository
            = RepositoryFactory.GetRepository<ITeamMemberRepository>();

        using(IUnitOfWork unitOfWork = UnitOfWorkFactory.CreateUnitOfWork())
        {
            TeamMember teamMember = repository.GetByID(teamMemberID);

            teamMember.ChangeExperienceRating(newExperienceRating);

            repository.Save(teamMember);
        }
    }

I was having similar problems with this when designing my model and asked this question which I think might help you, especially regarding your first point.

DDD - How to implement high-performing repositories for searching.

When it comes to searching I don't work with the 'model', instead I have specialised search repositories that return 'Summary' objects... i.e. 'PlanSummary'. These are nothing more than information objects (could be thought of more like reporting) and are not used in a transactional sense - I don't even define them in my model class library. By creating these dedicated repositories and types I can implement high performing search queries that can contain grouped data (such as a PlannedTraining count) without loading all of the associations of the aggregate in memory. Once a user selects one of these summary objects in the UI, I can then use the ID to fetch the actual model object and perform transactional operations and commit changes.

So for your situation I would provide these specialised search repositories for all three entities, and when a user wishes to perform and action against one, you always fetch the Plan aggregate that it belongs to.

This way you have the performant searches whilst still maintaining your single aggregate with the required invariants.

Edit - Example:

OK, so I guess implementation is subjective, but this is how I have handled it in my application, using a 'TeamMember' aggregate as an example. Example written in C#. I have two class libraries:

  • Model
  • Reporting

The Model library contains the aggregate class, with all invariants enforced, and the Reporting library contains this simple class:

public class TeamMemberSummary
{
    public string FirstName { get; set; }

    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }

    public bool IsAvailable { get; set; }

    public string MainProductExpertise { get; set; }

    public int ExperienceRating { get; set; }
}

The Reporting library also contains the following interface:

public interface ITeamMemberSummaryRepository : IReportRepository<TeamMemberSummary>
{

}

This is the interface that the application layer (which in my case happens to be WCF services) will consume and will resolve the implementation via my IoC container (Unity). The IReportRepository lives in an Infrastructure.Interface library, as does a base ReportRepositoryBase. So I have two different types of repository in my system - Aggregate repositories, and reporting repositories...

Then in another library, Repositories.Sql, I have the implementation:

public class TeamMemberSummaryRepository : ITeamMemberSummaryRepository
{
    public IList<TeamMemberSummary> FindAll<TCriteria>(TCriteria criteria) where TCriteria : ICriteria
    {
        //Write SQL code here

        return new List<TeamMemberSummary>();
    }

    public void Initialise()
    {

    }
}

So then, in my application layer:

    public IList<TeamMemberSummary> FindTeamMembers(TeamMemberCriteria criteria)
    {
        ITeamMemberSummaryRepository repository 
            = RepositoryFactory.GetRepository<ITeamMemberSummaryRepository>();

        return repository.FindAll(criteria);

    }

Then in the client, the user can select one of these objects, and perform an action against one in the application layer, for example:

    public void ChangeTeamMembersExperienceRating(Guid teamMemberID, int newExperienceRating)
    {
        ITeamMemberRepository repository
            = RepositoryFactory.GetRepository<ITeamMemberRepository>();

        using(IUnitOfWork unitOfWork = UnitOfWorkFactory.CreateUnitOfWork())
        {
            TeamMember teamMember = repository.GetByID(teamMemberID);

            teamMember.ChangeExperienceRating(newExperienceRating);

            repository.Save(teamMember);
        }
    }
猛虎独行 2024-09-02 23:59:26

这里真正的问题是违反 SRP。您的应用程序输入部分与输出发生冲突。

坚持第一个解决方案(计划==聚合根)。人为地提升实体(甚至价值对象)来聚合根会扭曲整个领域模型并毁掉一切。


您可能想查看所谓的 CQRS (命令查询职责隔离)架构非常适合解决这个特定问题。 这是 Mark Nijhof 的一个示例应用。这是很好的'入门' 列表。

Real problem here is SRP violation. Your input part of app goes in conflict with output.

Stick with first solution (Plan==aggregate root). Artificially promoting entities (or even value objects) to aggregate roots distorts whole domain model and ruins everything.


You might want to check out so called CQRS (command query responsibility segregation) architecture which would fit perfectly to fix this particular issue. Here's an example app by Mark Nijhof. Here's nice 'getting-started' list.

小草泠泠 2024-09-02 23:59:26

这是CQRS架构的全部要点:将命令(修改域)与查询(仅提供域状态视图)分开,因为命令和查询的要求非常不同。

您可以在这些博客上找到很好的介绍:

以及许多其他博客(包括我的

This is the whole point of CQRS architectures: segregate Commands - that modify the domain - from Queries - that simply give a view of domain state, because requirement for Commands and Queries are so different.

you can find a good introductions on these blogs :

and on many other blogs (including mine)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文