【问题标题】:Performing simple query inside of domain service在域服务内部执行简单查询
【发布时间】:2018-02-02 22:55:51
【问题描述】:

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

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

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

我曾考虑将域服务传递给设置返回的 PeriodDate 的方法(或构造函数),但我不知道该怎么做。即使域服务引用了存储库,我也看不出在该存储库上放置“GetNextReturnDate()”是合适的。

对于背景,每个产品退货都与一个产品相关联。我不愿意让产品成为聚合根,因为加载所有产品返回只是为了添加一个似乎是一种极不高效的做事方式(考虑到这个库将与 RESTful Web API 一起使用)。

谁能提供关于我应该如何建模的建议?只是改变聚合根和处理性能的问题吗?域中是否有可以放置“查询”类型服务的地方?

例如,当前的构造函数 for product return 如下所示:

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(及其实现)位于域层,因此无法直接从那里调用 SQL(而且我觉得这不是我应该在这里做的)

同样,我很可能已经对这个进行了非常糟糕的建模,或者我在设计聚合时遗漏了一些东西,因此我们将不胜感激!

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

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

但是,我仍然担心我的设计遗漏了一些东西,所以我再次对建议持开放态度。

编辑 2:为了回应 VoiceOfUnreason 下面的回答,我想我会澄清一些关于 PeriodDate 属性的事情。

关于它的规则如下:

  1. 不能为空
  2. 它必须与其他产品退货顺序一致,如果不满足则不能处于有效状态

这是一个棘手的问题。我不能依赖传入的日期,因为它很可能是乱序的,但是如果没有注入服务,我无法确定日期。我要将构造函数转换为工厂上的方法以删除“构造函数工作”反模式。

我可能对当前代码的方式过于防御,但感觉必须注入 ReturnService 的次数是错误的。请注意,有很多情况下必须重新计算返回值,但感觉好像在保存之前只是这样做很容易(但我想不出一个干净的方法这样做)。

总的来说,我只是觉得这个类有点味道(注入服务和诸如此类的东西),但我可能不必要地担心。

【问题讨论】:

  • 您能否详细解释一下“每个产品退货必须是连续的”。我对 ProductReturn 的理解是:很多人从您的网站购买产品,如果有人想要退货,您正在创建一个 Product Return Agg。 ,在那种情况下,“每个产品退货必须是连续的”有什么意义。请澄清
  • 我认为你应该在软件工程论坛上问这个问题,你会在那里得到很好的回应,因为它看起来是一个设计问题。链接 - softwareengineering.stackexchange.com
  • @SahilAggarwal 在引用其他网站时,指出cross-posting is frowned upon 通常会有所帮助
  • @techagrammer 指的是金融——产品本质上是一种投资。作为一项投资,它每月都有回报。

标签: domain-driven-design


【解决方案1】:

我曾考虑将域服务传递给设置返回的 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 的规则属于 ProductReturn,那么您通常会在对象上使用方法来封装这些规则。另一方面,如果 periodDate 真的是在这个聚合之外决定的(就像你的例子中的 productID 一样),那么只需传递正确的答案。

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

用户无法输入日期,因为在任何给定时间,退货日期只能是上次退货后的下一个日期。

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

另一方面,如果“最后返回日期”属于域模型,那么传递域服务可能更有意义。

还要提一下——没有日期的return是无效的,所以无法构造实体,希望以后调用该方法

你确定吗?实际上,您在域模型上引入了排序约束 - 这些消息都是不允许的,除非首先收到该消息,这意味着您有一个竞争条件。见 Udi Dahan 的Race Conditions Don't Exist

更一般地说,一个实体是否有效取决于它是否能够满足其方法的后置条件,如果后置条件更广泛,您可以在构造过程中放松约束。

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

构造 ProductReturn 需要有效的 _periodDate 的想法并没有错误;但是需要权衡取舍,具体取决于您的操作环境。

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

如果您在此处存储的数据与存储在其他地方的数据之间存在严格限制,那么这可能表明存在建模问题。在您对设计投入过多之前,请确保您了解 Set Validation 的含义。

【讨论】:

  • 感谢您的回复,非常详细!我想在汇总而不是传入的原因是因为日期应该是(我觉得)的逻辑是业务逻辑。用户不能输入日期,因为在任何给定时间,退货日期只能是上次退货后的下一个日期。我确实注意到了构造函数的反模式,所以我打算创建一个工厂
  • 我想避免的另一件事是,当用户可以在创建对象时传入对构造对象有意义的数据时,他们必须调用多个方法来设置属性。关于价值计算,有关于如何计算以及何时计算的业务规则,这就是为什么我觉得使用服务更合适。 IE。有时该值将被硬编码,有时则需要根据其他属性进行计算。我在这里是不是完全错误的心态?
  • 还要提一下——没有日期的return是无效的,所以无法构造实体,希望以后调用该方法
  • (很抱歉评论垃圾邮件)但我也担心在整个设置方法中需要返回计算服务的次数。这些方法中的每一种都会影响回报的计算方式,因此确实需要重新计算,但处理这种方式的方式对我来说只是闻起来
  • 最后,如果数据库中保存的任何日期不是下一个连续日期,则后续收益的计算将失败,因为我们需要一个序列来正确进行计算。这是一个棘手的问题——return 不知道如何获取下一个日期,但不能直接传入,因为它可能不正确。这就是我选择域服务方式的原因!
【解决方案2】:

您的问题是查询多个汇总(产品退货)决策(创建新的产品退货汇总)。

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

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

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

ProductReturnManager 可以建模为一个 Saga,它是在第一个 CreateProductReturnCommand(对于 productId)上创建的,并且为进一步的 CreateProductReturn 命令(与 productId 相关)加载相同的 saga。它处理 ProductReturnCreatedEvent 以更新其状态。 Saga 创建逻辑将根据您的业务规则(例如,在 InvoiceRaisedForProduct 事件上完成 saga 创建并处理 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 关于跨多个聚合工作的精彩视频。

【讨论】:

  • 感谢您的回答。我想我已经开始思考这条路线(即从聚合中抽象出创建逻辑),但没有如此复杂的解决方案。不过,我担心必须将产品返回聚合标记为“正在处理”——这是否意味着该域对持久性机制的工作方式有所了解?除此之外,我可以看到这个解决方案是如何工作的,但我试图避免在创建另一个返回之前总是加载至少一个其他返回(并在内存中维护一个列表),但这也许是不可避免的
  • @Olorin 请告诉我们您解决问题的最终方法。
猜你喜欢
  • 1970-01-01
  • 2020-12-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-25
  • 2014-01-26
  • 1970-01-01
  • 1970-01-01
  • 2016-08-03
相关资源
最近更新 更多