在域服务中执行简单查询

时间:2018-02-02 03:51:09

标签: domain-driven-design

我在尝试重新设计现有业务对象时采用了更多以域驱动的设计方法,遇到了问题。

目前,我有一个Product Return聚合根,它处理与特定产品的退货相关的数据。作为此汇总的一部分,需要提供一个日期来说明目前的回报月份(和年份)。

每个产品退货必须是顺序的,因此每个产品退货必须是前一个月后的下个月。尝试创建不遵循此模式的产品退货应该会导致异常。

我曾考虑将域服务传递给为返回设置PeriodDate的方法(或构造函数),但我不知道如何执行此操作。即使域服务具有对存储库的引用,我也看不到放置" GetNextReturnDate()"在该存储库中。

对于后台,每个产品退货都与产品相关联。我不愿意将产品作为聚合根,因为加载所有产品返回只是为了添加一个似乎是一种非常不高效的处理方式(考虑到这个库将与RESTful Web API一起使用)。 / p>

任何人都可以提供关于我应该如何建模的建议吗?这只是改变聚合根并处理性能的问题吗?域名中是否有某个地方可以查询'可以放置类型服务吗?

例如,产品返回的当前构造函数如下所示:

public ProductReturn(int productID, int estimateTypeID, IProductService productService)
{
     // This doesn't feel right, and I'm not sure how to implement it...
     _periodDate = productService.GetNextReturnDate(productID);

    // Other initialization code here...
}

IProductService(以及它的实现)位于Domain层,因此无法直接从那里调用SQL(我觉得这不是我应该在这里做的)反正)

同样,我极有可能模仿这个,或者我在设计聚合时错过了一些东西,所以任何帮助都会受到赞赏!

我认为我这里更广泛的问题是理解如何在域实体内部实现约束(无论是外来的,唯一的等),而不是在简单的SQL中通过域服务获取整个返回列表查询会提供所需的信息

编辑:我确实看到了另一个问题的答案:https://stackoverflow.com/a/48202644/9303178,这表明有“域名查询”问题。域中的接口,听起来像是可以返回我正在寻找的那种数据。

然而,我仍然担心我的设计中遗漏了一些东西,所以我再次接受建议。

编辑2:为了回应下面的VoiceOfUnreason的回答,我想我澄清了一些RE周期属性。

关于它的规则如下:

  1. 不能为空
  2. 必须与其他产品退货同步,如果没有履行,则不能处于有效状态
  3. 这是一个棘手的问题。我不能依赖于传入的日期,因为它很可能会失序,但我无法在没有注入服务的情况下找出日期。我将把构造函数转换为Factory上的方法,以删除正在进行工作的构造函数'反模式。

    我可能对代码当前的方式过于防守,但感觉ReturnService必须注入的次数是错误的。请注意,有很多场景必须重新计算返回值,但感觉好像在保存之前很容易做到只是(但我无法想到干净的方式来做到这一点)。

    总的来说,我觉得这堂课有一点气味(带注入的服务和诸如此类的东西),但我可能会不必要地担心。

2 个答案:

答案 0 :(得分:0)

  

我曾考虑将域服务传递给为返回设置PeriodDate的方法(或构造函数),但我不知道如何执行此操作。

我强烈怀疑将域服务传递给方法是正确的方法。

一种思考方式:从根本上说,聚合根是一包缓存数据,以及用于更改数据包内容的方法。在任何给定的函数调用期间,它对世界的全部知识就是那个包,以及传递给方法的参数。

因此,如果你想告诉汇总它还不知道的东西 - 当前没有包含的数据 - 那么你必须将这些数据作为参数传递。

反过来又有两种形式;如果您可以在不查看聚合包的情况下知道 要传递哪些数据,您只需将其作为参数传递。如果您需要隐藏在聚合包中的一些信息,那么您传递域服务,并让聚合(可以访问包的内容)传入必要的数据。

public ProductReturn(int productID, int estimateTypeID, IProductService productService)
{
    // This doesn't feel right, and I'm not sure how to implement it...
    _periodDate = productService.GetNextReturnDate(productID);
    // Other initialization code here...
}

这个拼写有点奇怪; constructor does work通常是反模式,当你可以计算值并将其传入时,传递服务来计算值有点奇怪。

如果您觉得决定如何计算_periodDate是业务逻辑的一部分,也就是说您认为选择periodDate 的规则属于< / em> ProductReturn,那么你通常会在对象上使用一个方法来封装这些规则。另一方面,如果在这个聚合之外真的决定了periodDate(就像你的例子中的productID那样),那么只需传入正确的答案。

可能会让你感到困惑的一个想法是:时间不是聚合包中存在的东西。时间是输入;如果业务规则需要知道当前执行某些工作的时间,那么您将把该时间作为参数传递给聚合(再次,作为数据或域服务)。

  

用户无法传递日期,因为在任何给定时间,返回日期只能是上次返回的下一个日期。

通常,您在用户和域模型之间有一个层 - 应用程序;它是决定传递给域模型的参数的应用程序。例如,通常是应用程序将“当前时间”传递给域模型。

另一方面,如果“最后一次返回的日期”是域模型所拥有的东西,那么传递域名服务可能更有意义。

  

我还应该提一下 - 如果没有日期,返回无效,所以我无法构造实体,然后希望稍后调用该方法

你确定吗?实际上,您正在对域模型引入排序约束 - 除非首先收到这些消息,否则不允许这些消息,这意味着您已经遇到竞争条件。见Udi Dahan的Race Conditions Don't Exist

更一般地说,实体是否有效或基于其是否能够满足其方法的后置条件而有效,如果后期条件更广,您可以在施工期间放松约束。

Scott Wlaschin的Domain Modeling Made Functional详细描述了这一点;总之,_periodDate可能是或类型,与它进行交互有明确的选择:如果有效则执行此操作,如果无效则执行此操作。

构建ProductReturn需要有效_periodDate的想法不是错误;但需要考虑的权衡取决于您所处的背景。

  

最后,如果任何日期保存到数据库而不是下一个连续日期,则后续返回的计算将失败,因为我们需要一个序列来正确地进行计算。

如果此处存储的数据与存储在其他地方的数据之间存在强约束,则可能表示存在建模问题。在您过度投入设计之前,请确保了解Set Validation的含义。

答案 1 :(得分:0)

您的问题查询多个聚合(产品退货)以便做出决策(创建新的产品退货汇总)。

基于使用存储库查询聚合的决策总是错误的;我们永远无法保证一致性,因为从存储库读取的状态总是有点旧。(聚合是事务边界。从存储库读取的状态只会在那个瞬间正确。在下一个瞬间聚合&# 39;国家可能会改变。)

在您的域中,我要做的是创建一个ProductReturnManager AggregateRoot,它管理特定产品的退货,以及ProductReturn Aggregate,它指定产品的一个特定退货。 ProductReturnManager AggregateRoot管理ProductReturnAggregate的生命周期以确保一致性。

为ProductReturn分配下个月顺序日期的逻辑在ProductReturnManager中(基本上ProductReturnManager充当构造函数)。产品退货的行为将在ProductReturnAggregate中。

可以将ProductReturnManager建模为Saga,它在第一个CreateProductReturnCommand(对于productId)上创建,并且为进一步的CreateProductReturn命令(由productId关联)加载相同的saga。它处理ProductReturnCreatedEvent以更新其状态。 Saga创建逻辑将根据您的业务规则(例如,saga创建在InvoiceRaisedForProduct事件上完成并处理CreateProductReturn命令。)

示例代码:

ProductReturnManagerSagaState{

ProductId productId;
//can cache details about last product return  
ProductReturnDetails lastProductReturnDetails;

}

ProductReturnManagerSaga : Saga<ProductReturnManagerSagaState>,IAmStartedByMessages<CreateProductReturn>{

Handle(CreateProductReturn message){

//calculate next product return date
Date productReturnDate = getNextReturnDate(Data.lastProductReturnDetails.productReturnDate);

//create product return 
ProductReturnAggregateService.createProductReturn(Data.productId, productReturnDate);
}

Handle(ProductReturnCreatedEvent message){
//logic for updating last product return details in saga state

}

}

ProductReturnAggregate{

ProductId productId;
Date productReturnDate;
ProductPayment productPayment;
ProductReturnState productReturnState;

//commands for product return 
markProductReturnAsProcessing(); 
}

This是Udi Dahan在多个聚合中进行合作的精彩视频。