【问题标题】:DDD - clean architecture vs clear workflowDDD - 干净的架构与清晰的工作流程
【发布时间】:2020-10-29 22:54:59
【问题描述】:

我们以 DDD 方式写作,这意味着我们不会“将 DDD 视为圣经”,而是利用它提供的好处,并在它迫使我们做的事情时弯曲它对我们来说不是“常识”——毕竟 DDD 的主要观点之一是代码与现实生活中的用例保持接近。

这里有一个技术让我们有点偏离轨道的地方:

将项目添加到购物篮用户故事:(我在这里保持简短...)

"当客户将商品添加到购物篮时,如果销售额超过限制 (1000$),用户将收到“最大销售额为 1000$”的弹出窗口 如果此商品出现在购物篮中的次数超过允许的次数,用户将收到“产品超出一个订单中允许的最大数量”弹出窗口 并且该商品在库存中不存在用户将收到“产品 XXX 缺货”弹出窗口 如果所有条款都可以,请将商品添加到购物篮中”

应用程序代码可能如下所示:

func basketapp.AddItem(basketId, productId, quantity){

   myBasket := BasketRepo.LoadByID(basketId)      //load the basket
   Product := ProductService.GetByID(productId)   //get the requested product 
   myBasket.AddItem(Product, quantity)            //runs internal domain business logic to add the item
   BasketRepo.Save(basketId, myBasket)            //Persist/add event source
}

领域业务逻辑如下所示:

func model.basket.AddItem(product, quantity){
   if basket.total() > maxTotalPerBasket {
       return maxItemsPerBasketExceededError
   }

   if basket.CountItem(product.id) > product.MaxRepeatsPerSale {
       return productCountLimitExceededError
   }
   .....
   item := newBasketItem(product, quantity ....)
    basket.Items.Append(item)
}

当我希望在简单验证后验证产品是否有库存时,问题就开始了 对于人类(客户、产品经理开发人员)而言,这是另一个验证,因此按照用户故事的建议编写:

func model.basket.AddItem(product, quantity){
   if basket.total() > maxTotalPerBasket {
       return maxItemsPerBasketExceededError
   }
 
   if basket.CountItem(product.id) > product.MaxRepeatsPerSale {
       return productCountLimitExceededError
   }
        
   if !inventoryService.exists(product.Id,storeId ){
       return productNotInInventoryError
   }
        
   item := newBasketItem(product, quantity ....)

   basket.Items.Append(item)
}

但是根据 DDD 的概念,模型不会调用另一个服务(甚至不是注入方案),

所以额外的代码将成为应用层的一部分,我需要以这种方式拆分 model.AddItem:

func basketapp.AddItem(basketId, productId, quantity){

   myBasket := BasketRepo.LoadByID(basketId)        
   Product := ProductService.GetByID(productId)     
   //myBasket.AddItem(Product, quantity)            
        
        
   if !myBasket.ValidateItem(Product, quantity){    //Where ValidateItem is the model basket.total()+basket.CountItem code
      return ...
   } else if !StockLevelService.exists(productId, storeId ){
      return ...
   }
   myBasket.AddItem(Product, quantity)  //leaving lines from model.Additem after validation
        
          
   BasketRepo.Save(basketId, myBasket)         
        
}

它的代码更简洁,但现在用户故事需要更改(为此 - 编写此用户故事的人应该了解架构方面的 stockLevel 是在不同的服务/域中完成的事情”。

我们对这个想法进行了一些其他突变,但我们无法找到一种方式,一方面可以“按照技术规则行事”,另一方面“将作为用户故事阅读”(当然需要考虑到团队中的每个人都会很容易理解所有事情的正确位置,所以我们不会找到混合方法并且需要修复代码审查......以及在调试每个部分时快速知道去哪里等)

欢迎提出想法 :)

【问题讨论】:

  • 您可以将要检查的服务作为参数传递给 AR 方法。
  • !StockLevelService.exists 使用storedid:它来自架构的什么地方? (您的伪代码中缺少它)
  • @plalx 评论是一个好点,但是。不过,我看到了另一个问题:当您将商品添加到购物篮时,库存应该会更改其产品可用性状态。如果这可以异步发生,也许您不需要与库存交互,发送域事件(并在不可用的情况下接收一个)可以工作。否则,您可以实现域服务接口并将其注入 AR,如果可用,它将“提交”库存产品

标签: design-patterns domain-driven-design clean-architecture


【解决方案1】:

我们无法找到一种方式,一方面“按技术规则行事”,另一方面“将作为用户故事阅读”

不是你的错——文学作品很糟糕。

域模型主要是一种记账设备;他们的职责是处理信息,尤其是处理本地内存中的信息。

问题:您想要的信息并不总是本地的。有时您需要向其他人询问您需要的信息。

在您的具体情况下,本地信息将包括篮子模型的数据结构,以及已传递给应用程序的本地参数;问题来自于 inStock? 之类的东西,它需要您在本地没有的信息。

所以我们通常需要一条管道来获取这些信息。这里StockLevelService::exists 是一个例子——你向它传递一些本地信息,它会给你一个答案的本地副本,它存在于其他地方。

此时,您有两种可能的模式,可以让您获得所需信息的副本,而不会引入大量依赖项。

答案是将那条管道传递给模型,以便模型可以调用它来获取所需信息的副本

func model.basket.AddItem(product, quantity, inventoryService){ 
   // ...
   if !inventoryService.exists(product.Id,storeId ){
       return productNotInInventoryError
   }
   // ...

另一种方法是在应用程序代码中调用管道,并将答案传递给领域模型

func basketapp.AddItem(basketId, productId, quantity){

   myBasket := BasketRepo.LoadByID(basketId)      //load the basket
   Product := ProductService.GetByID(productId)   //get the requested product 


   myBasket.AddItem(Product, quantity, inventoryService.exists(Product, quantity))            //runs internal domain business logic to add the item
   BasketRepo.Save(basketId, myBasket)            //Persist/add event source
}

设计是我们所做的,是为了得到我们想要的东西,而不是仅仅这样做。

其中哪一个“更好”很大程度上取决于您想要更多的“我们想要什么”。第二种设计更简单(域模型与处理错误的问题隔离开来),但不一定更容易

当您事先不知道在获取远程信息时使用什么参数,或者何时/是否应该调用它等等时,这两种方法的区别就开始显露出来了。或者,在失败的情况下,您应该保存到目前为止所取得的进展,而不是仅仅爆炸和回滚所有内容。

回顾这两个演讲可能会有所帮助:

【讨论】:

  • 这有助于: 1. 了解没有我遗漏的“一个答案” 2. 映射选项的利弊:更简单的设计与更简单的设计(并不总是相同) b.传递准备使用值或依赖项作为参数 c.获取这些值是否总是必要的或仅基于其他逻辑(并且此逻辑可以在应用程序上还是必须在模型上,以免使事情过于复杂) d。如果您确实调用了对模型的依赖,您如何以标准方式处理错误 e。如果您确实处理了模型中的错误,那么单个应用程序如何进行?
猜你喜欢
  • 2019-04-08
  • 2020-02-03
  • 2018-03-12
  • 2019-03-28
  • 1970-01-01
  • 2015-02-13
  • 1970-01-01
  • 2016-06-24
  • 2013-05-24
相关资源
最近更新 更多