DDD:聚合根

时间:2010-04-01 06:13:02

标签: domain-driven-design aggregate aggregateroot aggregates

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

我有3个实体:Plan,PlannedRole和PlannedTraining。每个计划都可以包含许多PlannedRoles和PlannedTraining。

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

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

解决方案2:我刚刚意识到用户还需要2个搜索页面,他们可以搜索计划角色或计划培训。这让我意识到他们正试图独立地访问这些对象并“脱离”Plan的背景。所以我认为我的初始设计错了,这就是我想出这个解决方案的方法。所以,我认为这里有3个聚合,每个实体有1个聚合。

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

还有另一个不变量,表明计划只有在具有特定状态时才能更改。因此,我不能将任何PlannedRoles或PlannedTrainings添加到不处于该状态的计划中。同样,我不能用第二种方法强制执行这种不变量。

任何建议都将不胜感激。

干杯, MOSH

3 个答案:

答案 0 :(得分:9)

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

DDD - How to implement high-performing repositories for searching

当涉及到搜索时,我不使用'模型',而是我有专门的搜索存储库,它们返回'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存在于Infrastructure.Interface库中,基本ReportRepositoryBase也是如此。所以我的系统中有两种不同类型的存储库 - 聚合存储库和报告存储库......

然后在另一个库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);
        }
    }

答案 1 :(得分:4)

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

坚持使用第一个解决方案(Plan ==聚合根)。人为地推广实体(甚至是价值对象)来聚合根会扭曲整个领域模型并破坏一切。


您可能希望查看所谓的CQRS(命令查询责任隔离)架构,该架构非常适合修复此特定问题。 Mark Nijhof Here's an example app。这是很好的'getting-started'列表。

答案 2 :(得分:3)

这是 CQRS 架构的重点:隔离命令 - 修改域 - 来自查询 - 只是给出了域状态的视图,因为对命令和查询的要求是如此不同。 / p>

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

以及许多其他博客(包括mine