从存储整体上请求聚合,并将其视为单个单元。建议设计较小的骨料不影响性能。这部分对我来说非常具有挑战性。特别是在持久化数据方面。
我有一个Activity
和DueDate
属性。活动的Participants
可以对活动的Phases
起作用,但只能在DueDate之前。
因此,每次用户为Phase做出贡献时,我都需要检查他是否是参与者和Now < DueDate
。
看来我不需要每个参与者,阶段和贡献都加载整个活动图。
如果已经存在对此阶段的贡献,那么我必须同时限制阶段内容的更改。
除此以外,来自不同参与者的贡献的并行事务不会相互影响。
这给了我一个提示,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,我认为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;
}
}
它们通常由应用层调用,以响应某些用例(有人参与某个阶段)。在委派对域的调用(命令处理程序)之前,应用程序层应检查
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
的某种类,该类应处理应用程序逻辑异常。可以将它们注入到应用程序的类构造函数中。实际上,您甚至可以在构造函数中发送AuthorizationService
,并将其重新用于每个应用程序调用。不仅在此处引发异常的目的是使测试更加容易。
现在让我们谈谈CQRS。简而言之,其目的是将查询与存储分开。从Martin Fowler:
…其内心的想法是,您可以使用与用于读取信息的模型不同的模型来更新信息。在某些情况下,这种分离可能很有价值,但请注意,对于大多数系统,CQRS会增加风险。
这种方法带来的好处是,在执行命令之后,您可以将调用委派给具有非规范化数据的辅助存储,以实现只读目的。该二级存储甚至可能不需要键和关系。这就像将您的DTO / ViewModels存储在某个地方。
人们认为我们读取的数据要比存储的更多,因此,这使您可以在UI读取比以往更快的状态下存储数据,从而“准备好呈现”数据。对于模型中的每个新更改,除了更新/删除之外,您还可以插入其他注册表,因此可以更快,更轻松地获取历史数据,差异和其他内容。
由您和您的企业决定存储多少存储量,非规范化级别。由于当今存储越来越便宜,因此您可以考虑在该辅助存储中存储更多内容。
它也可以是另一种类型的存储,例如NoSQL缓存(由我们来进行缓存失效),由您决定。我并不是说要实现它很容易,我们应该在这些层之间定义事务级别,以及我现在不记得的其他内容。
因此,我认为存储非规范化数据是很好的,因为您将它们用于只读目的,并且要小心使其与域模型存储(可能是带有EF的SQL)同步。我希望这对研究该主题有帮助,我的示例旨在根据具体情况建议替代解决方案,您应尝试结合良好的解决方案,在适合时使用CQRS,并在适合时聚合。允许组合它们,直到有人再次证明相反。