域驱动设计 – 聚合与数据模型

从存储整体请求聚合并作为单个单元处理.建议设计小型聚合不影响性能.这部分对我来说非常具有挑战性.特别是在持久化数据方面.

我有一个具有DueDate属性的Activity.活动的参与者可以参与活动的阶段,但仅限于DueDate之前.
因此,每次用户为Phase做出贡献时,我都需要检查他是否是参与者,而现在<截止日期.
似乎我不需要为每个参与者,阶段和贡献加载整个活动图.

如果已经存在对此阶段的贡献,我必须同时限制阶段内容更改.

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

这给了我一个提示,即ContributionToPhase必须是一个独立的聚合,并且可能通过标识引用Activity聚合.
虽然我仍然需要加载Activity聚合只是为了获取DueDate属性的值.说实话,这让我很担心.

数据模型如下:

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

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


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

可以看出,在数据模型中,Activity和ContributionToPhase之间没有直接的联系.如果我将其设计为事务脚本,我将创建一个特殊的DTO,其中包含验证特定事务所需的所有数据(但不是更多):

ContributionRelatedDTO
    Id
    ActivtyId
    PhaseId
    UserId
    ActivityDueDate
    TimeStamp
    ....

要么

PhaseContentsRelatedDTO
    Id
    ActivtyId
    HasContributions
    Timestamp
    ....

那么我应该如何使用DDD范式来处理它呢?
如果我将ContributionToPhase聚合模型化为具有存储在Activty表中的只读属性DueDate,这样可以吗?或者它是错误的聚合设计的味道?

解决方法

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

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

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;
    }

}

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

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

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

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

我试着尽可能地在应用程序层中尽可能地创建(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开始:

…its heart is the notion that you can use a different model to update information than the model you use to read information. For some situations,this separation can be valuable,but beware that for most systems CQRS adds risky complexity.

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

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

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

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

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

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...