建立具有关系的实体的正确方法是什么

What is the right way to build an entity with relationships

我有一些工厂负责构建 Product 实体。要构建 Product,必须从应与 Product.

关联的数据源中检索所有实体
class ProductFactory(
    private val productRepository: ProductRepository,
    private val shopRepository: ShopRepository,
    private val categoryRepository: CategoryRepository,
    private val tagRepository: TagRepository
) {
    fun build(action: CreateProductDTO): Product {
         val product = Product.Builder()
         val shop = shopRepository.findById(action.shopId)
         product.setShop(shop)

         val tags = tagRepository.findAllById(action.tags)
         product.setTags(tags)

         val category = categoryRepository.findById(action.categoryId)
         product.setTaxon(taxon)

         return productRepository.save(builder.build())
    }
}

我个人不喜欢上面的代码,至少因为违反了接口隔离原则。 ProductFactory 可以访问存储库的所有方法,但不应该这样做。

我想创建一种名为 Storage 的 DAL,它可用于特定的业务操作,例如产品创建。例如:

interface Storage {
    fun findShopById(id: Long): Optional<Shop>
    fun findCategoryById(id: Long): Optional<Category>
    fun findAllTagsById(ids: Iterable<Long>): List<Tag>
    fun save(product: Product)
}

有什么建议吗?

你在这里混淆了实例化插入概念:

  • 插入 是一种逻辑操作,当您将新实体添加到系统时发生,例如将新产品添加到产品目录。
  • Instantiation 是一种技术操作,当您想要在内存中创建一个新对象时,例如当您将 DTO 转换为实体时,或者您想要检索现有对象时来自数据库的产品。

在DDD中,工厂是负责实体实例化的对象。实体 插入 存储库 的责任。大多数时候,您不需要编写工厂,因为您可以编写一些简单的代码,例如 Product product = new Product(); 或使用一些映射库将数据持久性对象 (DPO) 投射到实体中。

但有时,对象实例化可能会成为一项复杂的任务,您可以编写一个工厂来分解此代码并从不同的 classes 中重用它。而不是写 Product product = new Product(); 你会写类似 Product product = productFactory.MakeProduct(...); 的东西并注入工厂。

这意味着您的 ProductFactory 实际上应该是您的 Builder class:

class ProductRepository(
    private val productFactory: ProductFactory,
    private val shopRepository: ShopRepository,
    private val categoryRepository: CategoryRepository,
    private val tagRepository: TagRepository
) {
    fun insert(action: CreateProductDTO): Product {
         val shop = shopRepository.findById(action.shopId)
         val tags = tagRepository.findAllById(action.tags)
         val category = categoryRepository.findById(action.categoryId)
         val product = productFactory.MakeProduct(shop, tags, category)
         product.Name = action.productName
         return this.save(product)
    }
}

你想应用的接口隔离原则是个好主意。它使测试更容易,因为您只需要一个模拟而不是一大堆模拟。

我会在它专用于的客户端之后命名接口,例如ProductFactoryRepository.

您的 ProductRepository 实现的代码似乎是我通常在交互器(又名用例)中编写的代码。当然,如果需要,您可以将其提取到自己的 class。

但是有一件事可能会破坏架构(如果我理解你的代码)。就是这个功能。

 fun build(action: CreateProductDTO): Product {

据我了解。 ProductFactory 是实体(或域)层的一部分。 CreateProductDTO好像属于controller(web或transport)层,因为DTO通常代表data transfer object.

但这将意味着您具有从实体层到传输层的依赖性,这打破了干净架构的架构规则。

干净的架构建议将普通数据结构传递到 InputPort。这些数据结构通常称为 RequestModelResponseModel.

交互器应该实现这样的输入端口:

interface CreateProductInputPort {
    fun createProduct(requestModel: CreateProductRequestModel, outputPort: CreateProductOutputPort)
}

RequestModel可以是像

这样的简单数据
data class CreateProductRequestModel(val shopId: Int, val tags: Array<String>, val categoryId: Int)

或者你可以声明一个接口

interface CreateProductRequestModel {
   fun getShopId(): Int
   fun getTags(): Array<String>
   fun getCategoryId(): Int
}

CreateProductDTO实施它。

您应该将交互器(用例)与传输层分离,以便应用单一责任原则,因为 DTO 因用例输入模型以外的其他原因而更改。

PS:希望我写的Kotlin代码是正确的。我通常在 Java.

中编码

遵循 DDD 教条,两个聚合应该使用 id 相互引用,而不是将整个 Shop/Tag/Category 设置在 Product 聚合中 :) 这就是为什么你觉得这段代码很奇怪,打破这个规则创造了必要完全使用存储库。

我想,使用整个 Shop 是由您用于持久性的技术驱动的。 您在工厂中感兴趣的是检查具有该 ID 的商店是否确实存在。

这应该通过向您的 ProductFactory 执行查询(传递服务)来处理,这样它将与 Shop 的细节分离并且只能访问 API由您的域公开(通过服务 query/commands)