聚合与数据模型

时间:2016-01-06 14:50:23

标签: domain-driven-design aggregateroot

从整个存储中请求聚合并将其视为一个单元。建议设计小型聚合不影响性能。这部分对我来说非常具有挑战性。特别是在持久化数据时。

我有Activity DueDate属性。 活动Participants,可以提供活动Phases,但仅限于 DueDate 之前。
因此,每次用户参与阶段时,我都需要检查他是否是参与者Now < DueDate
我似乎不需要为每个参与者阶段贡献加载整个活动图表。< BR />

如果对 Phase 的贡献已经存在,我必须限制阶段内容更改。

除了来自不同参与者的贡献的并行交易之外,不会相互影响。

这给了我一个提示,ContributionToPhase必须是一个独立的聚合,并且可能通过身份引用 Activity 聚合。
虽然我仍然需要加载 Activity 聚合才能获得 DueDate 属性的值。说实话,这让我很担心。

数据模型如下:

Activity
------------
Id
Title
Description
DueDate
....

Phase
------------
Id
ActivityId
Order
Title
Description
....


ContributionToPhase
------------
Id
PhaseId
ParticipantId
....

可以看出,在数据模型中,ActivityContributionToPhase之间没有直接链接。如果我将其设计为事务脚本,我会创建一个ad hoc DTO,其中包含验证特定事务所需的所有数据(但不是更多):

ContributionRelatedDTO
    Id
    ActivtyId
    PhaseId
    UserId
    ActivityDueDate
    TimeStamp
    ....

PhaseContentsRelatedDTO
    Id
    ActivtyId
    HasContributions
    Timestamp
    ....

我应该如何使用DDD范例来解决它? 如果我将 ContributionToPhase 聚合模型为具有存储在 Activty 表中的只读属性 DueDate ,这样可以吗?或者它是一种错误的聚合设计的气味?

1 个答案:

答案 0 :(得分:0)

要解决DDD和ORM的这类问题,请尝试实施一些CQRS。我喜欢DDD,但我不认为你应该全心全意地遵循它,我认为DDD很好地让我们遵循良好的做法,但请记住它每天都被 gurus 改进,因为它还没有针对所有问题的解决方案。

对于每个交易,我们称之为命令。要执行命令,我们需要CommandHandlerCommandData。我看到CommandData,因为它是 DTO 。在那里,你把执行所述命令所需的所有东西都放了。 CommandHandler更像是一项小型服务,处理业务登录,因此它们属于。让我们创建一个简单的例子:

public interface ICommandHandler<T>
{

    T Handle(T command);

}

public class ContributeToPhaseCommandData
{

    public Guid ContributionToPhaseId { get; set; }
    public Guid ActivityId { get; set; }
    public Guid PhaseId { get; set; }
    public Participant Contributor { get; set; }
    public DateTime ActivityDueDate { get; set; }

    public bool Success { get; set; }
    public string CommandResultMessage { get; set; }


    public ContributeToPhaseCommandData( /* mandatory data in constructor */ ) { }

}

public class ContributeToPhaseCommandHandler : ICommandHandler<ContributeToPhaseCommandData>
{

    public ContributeToPhaseCommandHandler( /* inject other services, if needed */ )
    {

    }

    public ContributeToPhaseCommandData Handle(ContributeToPhaseCommandData command)
    {
        // do stuff here, you might set some response data in the 'command' and return it.
       // You might send a DomainEvent here, if needed.
        return command;
    }

}

它们通常由应用层调用,以响应某些用例(有人为某个阶段做出贡献)。在委托对域(命令处理程序)的调用之前,应用层应检查requester(也称为用户或其他系统)是否具有执行此类操作的授权。

现在,我们如何获取数据命令?如果您不需要,我认为您不应该强制加载完整聚合。只在您需要时加载它。

有时你有一个沉重的逻辑,需要完整的聚合,因此你可以把它放在域模型/实体中。虽然有时你有更复杂的逻辑,你很难把它放在模型/实体中,并且需要许多部分的一些信息,而且当你根本不需要所有东西时,加载重聚合是不切实际的。这使你的模型变得贫血。

看起来您不需要完整聚合来为此方案应用域逻辑。我只是认为创建替代lighter版本的聚合是没用的,除非有人证明相反(我可能错了)。

我会尝试尽可能在应用层中尽可能地创建( KISS ):

public SomeResponseToCaller ContributeToPhase(ICommandHandler<ContributeToPhaseCommandData> command, Guid phaseId, IPrincipal caller, IAuthorizationService authorizer)
{
    if (!authorizer.authorizes(caller))
        this.ExceptionHandler.Handle("Caller is not authorized! Shall we log this info?");
    using(var db = new ActivitiesContext())
    {
        ContributeToPhaseCommandData data = db.Phases
            .Select(p => new ContributeToPhaseCommandData()
                {
                    ActivityId = p.ActivityId,
                    PhaseId = p.Id,
                    Contributor = p.Activity.Participants.SingleOrDefault(part => part.Name == caller.Identity.Name)
                    ActivityDueDate = p.Activity.DueDate
                }).SingleOrDefault(p => p.Id == phaseId);

        if (data == null)
            this.ExceptionHandler.Handle("Phase not found");

        if (data.Contributor == null)
            this.ExceptionHandler.Handle("Caller is not a participant of this Activity!!!!");

        data.ContributionToPhaseId = Guid.NewGuid();

        var result = command.Handle(data);

        db.SaveChanges();

        return new SomeResponseToCaller() {
            Success = result.Success,
            ContributionId = result.ContributionToPhaseId,
            Message = result.CommandResultMessage
        };
    }
}

这个ExceptionHandler是某种实现IExcepionHandler的类,应该处理应用程序逻辑异常。它们可以在Application的类构造函数中注入。实际上,您甚至可以在构造函数中发送AuthorizationService,并在每次应用程序调用时重复使用它。

不仅仅在那里抛出异常的目的是使测试更容易。

现在让我们谈谈CQRS。简而言之,它的目的是将查询存储分开。来自Martin Fowler

  

......它的核心是,您可以使用不同的模型来更新信息,而不是用于读取信息的模型。在某些情况下,这种分离可能很有价值,但请注意,对于大多数系统而言,CQRS增加了风险的复杂性。

这种方法带来的好处是在执行命令后,您可以将调用委托给具有非规范化数据的 辅助存储,以用于只读目的。此辅助存储可能甚至不需要密钥和关系。这就像在某处存储你的DTO / ViewModels一样。

人们认为我们读取的数据超过了我们存储的数据,因此这允许您将数据存储在UI可以比以往更快地读取的状态中,从而“准备好呈现”数据。对于模型中的每个新更改,您可以插入除更新/删除之外的新注册表,因此可以更快,更轻松地获取历史数据,差异和其他内容。

由您和您的企业决定存储多少,非规范化程度。由于现在存储越来越便宜,你可以考虑在二级存储中存储更多东西,因为它是相关的。

它也可能是另一种存储,如NoSQL,缓存(它会让我们缓存失效),由您决定。我不是说实现这个很容易,我们应该定义这些层之间的事务级别,以及我现在不记得的其他东西。

所以我认为可以存储非规范化数据,因为您将它们用于只读目的,并且要小心使它们与您的域模型存储(可能是带有EF的SQL)同步。我希望这有助于对此主题进行重新研究,我的示例的目标是根据具体情况建议替代解决方案,您应该尝试结合良好的解决方案,在适合时使用CQRS,和聚合何时适合。允许将它们组合,直到有人证明相反(再次)。