Home > Back-end >  What is the right way to build an entity with relationships
What is the right way to build an entity with relationships

Time:05-20

I have some factory that's responsible to build Product entity. To build the Product it's necessary to retrieve all entities from a data source that should be associated with 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())
    }
}

Personally I don't like the code above because of interface segregation principle violation at least. ProductFactory can access to all methods of the repositories but should not supposed to do this.

I have a thought to create some kind of DAL called Storage that could be used for specific business operation such as product creation. For example:

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)
}

Any suggestions?

CodePudding user response:

You are confusing instantiation and insertion concepts here:

  • Insertion is a logical operation which happens when you add a new entity into your system, like adding a new product to the product catalog.
  • Instantiation is a technical operation which happens when you want to create a new object in memory, like when you convert a DTO into an entity, or you whant to retrieve an existing product from the database.

In DDD, a factory is an object responsible for an entity instantiation. Entity insertion is the responsibility of the repository. Most of the time, you don't need to write a factory because you can write some simple code like Product product = new Product(); or use some mapping library to project a data persistence object (DPO) into an entity.

Sometimes though, object instantiation can become a complex task, and you can write a factory to factor this code and reuse it from different classes. Instead of writing Product product = new Product(); you would write something like Product product = productFactory.MakeProduct(...); and inject the factory.

This means your ProductFactory should actually be your 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)
    }
}

CodePudding user response:

The interface segregation principle that you want to apply is a good idea. It makes testing much easier, because you only need one mock and and not a whole bunch of mocks.

I would name the interface after the client that it is dedicated to, e.g. ProductFactoryRepository.

The code that your ProductRepository implements seems to be code that I usually would write in an interactor (aka. use case). Of cource you can extract it to an own class if you want.

But there is one thing that might break the architecture (if I understand your code). It is this function.

 fun build(action: CreateProductDTO): Product {

As far as I understand you. The ProductFactory is part of the entity (or domain) layer. The CreateProductDTO seems to belong to the controller (web or transport) layer, because DTO usually stands for data transfer object.

But this would mean that you have a dependency from the entity layer to the transport layer, which breaks the architecture rules of the clean architecture.

The clean architecture proposes to pass plain data structures into an InputPort. These data structures are often called RequestModel and ResponseModel.

The interactor should implement an input port such as this:

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

The RequestModel can either be a simple data like

data class CreateProductRequestModel(val shopId: Int, val tags: Array<String>, val categoryId: Int)

or you can declare an interface

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

and let CreateProductDTO implement it.

You should decouple the interactor (use case) from the transport layer in order to apply the single responsibility principle, because the DTO changes for other reasons than the use cases input model.

PS: I hope the Kotlin code I wrote is correct. I usually code in Java.

  • Related