从整个存储中请求聚合并将其视为一个单元。建议设计小型聚合不影响性能。这部分对我来说非常具有挑战性。特别是在持久化数据时。
我有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
....
可以看出,在数据模型中,Activity
和ContributionToPhase
之间没有直接链接。如果我将其设计为事务脚本,我会创建一个ad hoc DTO,其中包含验证特定事务所需的所有数据(但不是更多):
ContributionRelatedDTO
Id
ActivtyId
PhaseId
UserId
ActivityDueDate
TimeStamp
....
或
PhaseContentsRelatedDTO
Id
ActivtyId
HasContributions
Timestamp
....
我应该如何使用DDD范例来解决它? 如果我将 ContributionToPhase 聚合模型为具有存储在 Activty 表中的只读属性 DueDate ,这样可以吗?或者它是一种错误的聚合设计的气味?
答案 0 :(得分:0)
要解决DDD和ORM的这类问题,请尝试实施一些CQRS。我喜欢DDD,但我不认为你应该全心全意地遵循它,我认为DDD很好地让我们遵循良好的做法,但请记住它每天都被 gurus 改进,因为它还没有针对所有问题的解决方案。
对于每个交易,我们称之为命令。要执行命令,我们需要CommandHandler
和CommandData
。我看到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,和聚合何时适合。允许将它们组合,直到有人证明相反(再次)。