DDD: entity's collection and repositories

前端 未结 7 1168
隐瞒了意图╮
隐瞒了意图╮ 2021-01-30 12:04

Suppose I have

public class Product: Entity
{
   public IList Items { get; set; }
}

Suppose I want to find an item with max someth

7条回答
  •  北海茫月
    2021-01-30 12:28

    I believe in terms of DDD, whenever you are having problems like this, you should first ask yourself if your entity was designed properly.

    If you say that Product has a list of Items. You are saying that Items is a part of the Product aggregate. That means that, if you perform data changes on the Product, you are changing the items too. In this case, your Product and it's items are required to be transactionally consistent. That means that changes to one or another should always cascade over the entire Product aggregate, and the change should be ATOMIC. Meaning that, if you changed the Product's name and the name of one of it's Items and if the database commit of the Item's name works, but fails on the Product's name, the Item's name should be rolled back.

    This is the fact that Aggregates should represent consistency boundaries, not compositional convenience.

    If it does not make sense in your domain to require changes on Items and changes on the Product to be transactionally consistent, then Product should not hold a reference to the Items.

    You are still allowed to model the relationship between Product and items, you just shouldn't have a direct reference. Instead, you want to have an indirect reference, that is, Product will have a list of Item Ids.

    The choice between having a direct reference and an indirect reference should be based first on the question of transactional consistency. Once you have answered that, if it seemed that you needed the transactional consistency, you must then further ask if it could lead to scalability and performance issues.

    If you have too many items for too many products, this could scale and perform badly. In that case, you should consider eventual consistency. This is when you still only have an indirect reference from Product to items, but with some other mechanism, you guarantee that at some future point in time (hopefully as soon as possible), the Product and the Items will be in a consistent state. The example would be that, as Items balances are changed, the Products total balance increases, while each item is being one by one altered, the Product might not exactly have the right Total Balance, but as soon as all items will have finished changing, the Product will update itself to reflect the new Total Balance and thus return to a consistent state.

    That last choice is harder to make, you have to determine if it is acceptable to have eventual consistency in order to avoid the scalability and performance problems, or if the cost is too high and you'd rather have transactional consistency and live with the scalability and performance issues.

    Now, once you have indirect references to Items, how do you perform GetMaxItemSmth()?

    In this case, I believe the best way is to use the double dispatch pattern. You create an ItemProcessor class:

    public class ItemProcessor
    {
        private readonly IItemRepository _itemRepo;
        public ItemProcessor(IItemRepository itemRepo)
        {
            _itemRepo = itemRepo;
        }
    
        public Item GetMaxItemSmth(Product product)
        {
            // Here you are free to implement the logic as performant as possible, or as slowly
            // as you want.
    
            // Slow version
            //Item maxItem = _itemRepo.GetById(product.Items[0]);
            //for(int i = 1; i < product.Items.Length; i++)
            //{
            //    Item item = _itemRepo.GetById(product.Items[i]);
            //    if(item > maxItem) maxItem = item;
            //}
    
            //Fast version
            Item maxItem = _itemRepo.GetMaxItemSmth();
    
            return maxItem;
        }
    }
    

    And it's corresponding interface:

    public interface IItemProcessor
    {
        Item GetMaxItemSmth(Product product);
    }
    

    Which will be responsible for performing the logic you need that involves working with both your Product data and other related entities data. Or this could host any kind of complicated logic that spans multiple entities and don't quite fit in on any one entity per say, because of how it requires data that span multiple entities.

    Than, on your Product entity you add:

    public class Product
    {
        private List _items; // indirect reference to the Items Product is associated with
        public List Items
        {
            get
            {
                return _items;
            }
        }
    
        public Product(List items)
        {
            _items = items;
        }
    
        public Item GetMaxItemSmth(IItemProcessor itemProcessor)
        {
            return itemProcessor.GetMaxItemSmth(this);
        }
    }
    

    NOTE: If you only need to query the Max items and get a value back, not an Entity, you should bypass this method altogether. Create an IFinder that has a GetMaxItemSmth that returns your specialised read model. It's ok to have a separate model only for querying, and a set of Finder classes that perform specialized queries to retrieve such specialized read model. As you must remember, Aggregates only exist for the purpose of data change. Repositories only work on Aggregates. Therefore, if no data change, no need for either Aggregates or Repositories.

提交回复
热议问题