Domain driven design 聚合与数据模型
从整个存储中请求聚合,并将其视为单个单元。建议不要设计小型骨料,以免影响性能。这部分对我来说很有挑战性。特别是在持久化数据方面 我有一个带有Domain driven design 聚合与数据模型,domain-driven-design,aggregateroot,Domain Driven Design,Aggregateroot,从整个存储中请求聚合,并将其视为单个单元。建议不要设计小型骨料,以免影响性能。这部分对我来说很有挑战性。特别是在持久化数据方面 我有一个带有DueDate属性的活动。活动有参与者,这些参与者可以参与活动的阶段,但只能在截止日期之前参与。 因此,每次用户为阶段做出贡献时,我都需要检查他是否是参与者,现在代码>贡献者贡献者=参与者服务(PrimeDeFrimeFor)(PrimeID,SeaReNeDD)中使该域的规则PAR与THW类似。阶段.贡献(贡献者)//内部检查contributor.pha
DueDate
属性的活动
。活动有参与者
,这些参与者可以参与活动的阶段
,但只能在截止日期之前参与。因此,每次用户为阶段做出贡献时,我都需要检查他是否是参与者,
现在
似乎我不需要为每个参与者、阶段和贡献加载整个活动图。
同时,如果已经存在对此阶段的贡献,我必须限制阶段内容的更改 除此之外,不同参与者捐款的平行交易不会相互影响。
这给了我一个提示,
contributionPhase
必须是一个独立的聚合,并且可能根据身份引用活动聚合。虽然我仍然需要加载Activity aggregate才能获得DueDate属性的值。老实说,这让我很担心。
数据模型如下所示:
Activity
------------
Id
Title
Description
DueDate
....
Phase
------------
Id
ActivityId
Order
Title
Description
....
ContributionToPhase
------------
Id
PhaseId
ParticipantId
....
可以看出,在数据模型中,活动
和贡献阶段
之间没有直接联系。如果我将其设计为事务脚本,我会创建一个临时DTO,其中包含验证特定事务所需的所有数据(但不是更多):
ContributionRelatedDTO
Id
ActivtyId
PhaseId
UserId
ActivityDueDate
TimeStamp
....
或
那么,我应该如何使用DDD范式来处理它呢?
如果我将ContributionPhase聚合建模为具有存储在Activty表中的只读属性DueDate,可以吗?或者这是一种错误的聚合设计的味道?要用DDD和ORM解决此类问题,请尝试实现一些CQR。我喜欢DDD,但我不认为你应该全力以赴地遵循它,我认为DDD有助于我们遵循良好的实践,但请记住,大师们每天都在改进它,因为它还没有解决所有问题的方法 对于每个事务,我们称之为命令。要执行命令,我们需要一个
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;
}
}
这个ExceptionHandler
是实现IExcepionHandler
的类,它应该处理应用程序逻辑异常。它们可以被注入应用程序的类构造函数中。实际上,您甚至可以在构造函数中发送AuthorizationService
,并在每次应用程序调用中重用它
抛出异常的目的不仅仅是为了简化测试
现在我们来谈谈CQR。简单地说,它的目的是将查询和存储分离开来。发件人:
…其核心思想是,您可以使用不同的模型来更新信息,而不是使用不同的模型来读取信息。对于某些情况,这种分离可能是有价值的,但要注意,对于大多数系统,CQR会增加风险复杂性
这种方法带来的一个好处是,在执行命令后,您可以将调用委托给具有非规范化数据的辅助存储器,以实现只读目的。这个辅助存储甚至可能不需要密钥和关系。这就像将DTO/ViewModels存储在某个地方
人们认为我们读取的数据比存储的数据多,所以这允许您以UI读取速度比以往更快的状态存储数据,从而“准备好呈现”数据。对于模型中的每一个新更改,您都可以插入新的注册表,而不是更新/删除,因此获取历史数据、差异和其他内容更快更容易
由您和您的企业决定存储多少,以及非规范化的级别。因为现在存储越来越便宜,你可以考虑在辅助存储中存储更多的东西,因为它是相关的
它也可以是另一种存储,如NoSQL、cache(它将我们引入缓存失效),由您决定。我并不是说这很容易实现,我们应该定义这些层之间的事务级别,以及其他我现在记不起来的东西
因此我认为存储非规范化数据是可以的,因为您将它们用于只读目的,并且要小心使它们与域模型存储同步(可能是SQL和EF)。我希望这有助于研究这一主题,我的示例的目的是根据具体场景提出替代解决方案,您应该尝试组合好的解决方案,在合适的时候使用CQR,在合适的时候聚合。允许将它们合并,直到有人(再次)证明相反的情况。是否可以更改ActivityDueDate?如果是这样的话,那么你已经承认了一个事实,即规则最终将是一致的。您可以将ActivityDueDate复制到一个阶段,然后执行类似于
PhaseComtribution contribution=Phase.contribute(…);添加(贡献)代码>。使用域事件保持ActivityDueDate的同步,如果某个贡献最终被确定为非法,则可能会发出补偿操作。至于检查贡献者是否是参与者。您可以使用授权服务在应用程序服务中这样做。您也可以在应用程序服务>代码>贡献者贡献者=参与者服务(PrimeDeFrimeFor)(PrimeID,SeaReNeDD)中使该域的规则PAR与THW类似。阶段.贡献(贡献者)//内部检查contributor.phaseId==phase.id
@plax是,它可以更改并仅限制contribut的日期
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;
}
}
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
};
}
}