我有一个具有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,这样可以吗?或者它是错误的聚合设计的味道?
对于每个事务,我们称之为命令.要执行命令,我们需要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,并在适合时使用聚合.允许将它们组合,直到有人证明相反(再次).