DDD:实体的集合和存储库

时间:2009-08-31 08:27:50

标签: c# domain-driven-design

假设我有

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

假设我想找到一个带有最大值的项目......我可以添加方法Product.GetMaxItemSmth()并使用Linq(from i in Items select i.smth).Max())或手动循环或其他任何方式。现在,问题是这会将完整的集合加载到内存中。

正确的解决方案是执行特定的数据库查询,但域实体无法访问存储库,对吧?所以要么我做

productRepository.GetMaxItemSmth(product)

(这很丑陋,没有?),或者即使实体有权访问存储库,我也使用实体中的IProductRepository

product.GetMaxItemSmth() { return Service.GetRepository<IProductRepository>().GetMaxItemSmth(); }

这也是丑陋的并且是代码的重复。我甚至可以去看看并做一个扩展

public static IList<Item> GetMaxItemSmth(this Product product)
{
   return Service.GetRepository<IProductRepository>().GetMaxItemSmth();
}

这更好,只是因为它并没有真正使存储库中的实体混乱......但仍然会重复方法。

现在,这是再次使用product.GetMaxItemSmth()还是productRepository.GetMaxItemSmth(product)的问题。我错过了DDD的东西吗?这里的正确方法是什么?只需使用productRepository.GetMaxItemSmth(product)?这是每个人都喜欢和满意的吗?

我觉得这不对...如果我无法从产品本身访问产品Items,为什么我需要在Product中收集此产品?然后,如果Product无法使用特定查询并在没有性能命中的情况下访问其集合,{{1}}可以执行任何有用的操作吗?

当然,我可以使用效率较低的方式而且从不介意,当它很慢时,我会将存储库调用作为优化注入实体......但即使这听起来也不对,是吗?

有一点要提一下,也许它不是DDD ......但我需要在Product中使用IList才能获得使用Fluent NHibernate生成的数据库模式。不过,请在纯DDD环境中自由回答。

更新:这里描述了一个非常有趣的选项:http://devlicio.us/blogs/billy_mccafferty/archive/2007/12/03/custom-collections-with-nhibernate-part-i-the-basics.aspx,不仅可以处理与数据库相关的集合查询,还可以帮助进行集合访问控制。

7 个答案:

答案 0 :(得分:5)

拥有Items集合具有GetXXX()方法都是正确的。

要纯粹,您的实体不应该直接访问存储库。但是, 可以通过查询规范进行间接引用。查看Eric Evans的书第229页。像这样:

public class Product
{
    public IList<Item> Items {get;}

    public int GetMaxItemSmth()
    {
        return new ProductItemQuerySpecifications().GetMaxSomething(this);
    }
}

public class ProductItemQuerySpecifications()
{
   public int GetMaxSomething(product)
   {
      var respository = MyContainer.Resolve<IProductRespository>();
      return respository.GetMaxSomething(product);
   }
}

如何获得对存储库的引用是您的选择(DI,服务定位器等)。虽然这消除了Entity和Respository之间的直接引用,但它并没有减少LoC。

一般来说,如果我知道GetXXX()方法的数量将来会引起问题,我只会提前介绍它。否则,我将把它留给将来的重构练习。

答案 1 :(得分:4)

我相信在DDD方面,无论何时遇到这样的问题,您应首先问自己,您的实体设计是否正确

如果您说产品有项目列表。您说项目是产品聚合的一部分。这意味着,如果您在产品上执行数据更改,您也将更改项目。在这种情况下,您的产品及其商品必须在交易上保持一致。这意味着对一个或另一个的更改应始终级联整个Product聚合,并且更改应为ATOMIC。这意味着,如果您更改了产品的名称及其中一个项目的名称,并且如果项目名称的数据库提交有效,但产品名称失败,则应回滚该项目的名称。

这是聚合应该代表一致性边界而不是构成方便的事实。

如果您的域中没有任何意义要求对项目进行更改并且产品上的更改在事务上保持一致,那么Product不应该保留对Items的引用。

您仍然可以对产品和商品之间的关系进行建模,您不应该直接引用。相反,您希望有一个间接引用,即Product将有一个Item ID列表。

直接引用和间接引用之间的选择应首先基于事务一致性问题。一旦您回答了这个问题,如果您似乎需要事务一致性,那么您必须进一步询问它是否会导致可伸缩性和性能问题。

如果您的产品太多而产品太多,这可能会导致规模扩大并且表现不佳。在这种情况下,您应该考虑最终的一致性。这时您仍然只有从Product到项目的间接引用,但是通过其他一些机制,您可以保证在将来某个时间点(希望尽快),Product和Items将处于一致状态。例如,随着项目余额的变化,产品总余额增加,而每个项目逐个改变,产品可能不完全具有正确的总余额,但只要所有项目完成更改,产品将自动更新以反映新的总余额,从而返回到一致的状态。

最后的选择很难做,你必须确定是否可以接受最终的一致性,以避免可扩展性和性能问题,或者如果成本太高而且你宁愿具有事务一致性和生活具有可扩展性和性能问题。

现在,一旦你对Items有间接引用,你如何执行GetMaxItemSmth()?

在这种情况下,我认为最好的方法是使用双重调度模式。您创建一个ItemProcessor类:

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

它是相应的界面:

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

负责执行您需要的逻辑,包括使用您的产品数据和其他相关实体数据。或者这可以托管任何类型的复杂逻辑,这些逻辑跨越多个实体并且不完全适合任何一个实体,因为它需要跨越多个实体的数据。

在您的产品实体上添加:

public class Product
{
    private List<string> _items; // indirect reference to the Items Product is associated with
    public List<string> Items
    {
        get
        {
            return _items;
        }
    }

    public Product(List<string> items)
    {
        _items = items;
    }

    public Item GetMaxItemSmth(IItemProcessor itemProcessor)
    {
        return itemProcessor.GetMaxItemSmth(this);
    }
}

注: 如果您只需查询Max项并获取值,而不是实体,则应完全绕过此方法。创建一个具有GetMaxItemSmth的IFinder,它返回您的专用读取模型。可以使用仅用于查询的单独模型,以及执行专门查询以检索此类专用读取模型的一组Finder类。您必须记住,聚合只存在于数据更改的目的。存储库仅适用于聚合。因此,如果没有数据更改,则不需要聚合或存储库。

答案 2 :(得分:3)

(免责声明,我刚刚开始掌握DDD。或者至少相信这样做:))

我将在第二部分马克第二,强调我花了一些时间才意识到的一点。

  • 根据聚合来考虑您的对象,这会导致
      

    重点是要么将孩子与父母一起加载,要么单独加载

困难的部分是考虑手头问题的汇总而不是集中支持它的数据库结构 一个强调这一点的例子我是customer.Orders。您真的需要所有客户的订单来添加新订单吗?通常不会。如果她有1毫克呢? 你可能需要像OutstandingAmount或AmountBuyedLastMonth这样的东西来实现像“AcceptNewOrder”或ApplyCustomerCareProgram这样的场景。

  • 该产品是您的sceanrio的真正聚合根吗?
      

    如果产品不是聚合根,该怎么办?

即。你打算操纵物品或产品吗? 如果是产品,您需要ItemWithMaxSomething还是需要MaxSomethingOfItemsInProduct?

  • 另一个神话:PI意味着你不需要考虑数据库

鉴于您的场景中确实需要带有maxSomething的项目,那么您需要知道数据库操作的含义,以便通过服务或属性选择正确的实现。
例如,如果产品具有大量项目,则解决方案可能是将项目的ID与产品一起记录在db中,而不是遍历所有列表。

DDD中的困难部分是定义正确的聚合。我觉得越来越多,如果我需要依赖延迟加载,那么我可能已经监督了一些上下文边界。

希望这有助于:)

答案 3 :(得分:2)

我认为这是一个难以回答的难题。

一个答案的关键是分析Domain-Driven Design中讨论的聚合和关联。关键是要么你将孩子加载到父你单独加载它们。

当您将它们与父项(示例中的产品)一起加载时,父项控制所有对子项的访问权限,包括检索和写入操作。这样做的一个证据是,孩子们必须没有存储库 - 数据访问由父级存储库管理。

所以回答你的一个问题:“为什么我在产品中需要这个系列呢?”也许你不这样做,但如果你这样做,那就意味着当你加载产品时,总是总是。您可以通过查看列表中的所有项目来实现Max方法,该方法只需查找Max。这可能不是最高性能的实现,但如果Product是Aggregate Root,那将是这样做的方法。

如果产品聚合根,该怎么办?好吧,首先要做的是从Product中删除Items属性。然后,您将需要某种可以检索与产品关联的项目的服务。这样的服务也可以有一个GetMaxItemSmth方法。

这样的事情:

public class ProductService
{
    private readonly IItemRepository itemRepository;

    public ProductService (IItemRepository itemRepository)
    {
        this.itemRepository = itemRepository;
    }

    public IEnumerable<Item> GetMaxItemSmth(Product product)
    {
        var max = this.itemRepository.GetMaxItemSmth(product);
        // Do something interesting here
        return max;
    }
}

这与您的扩展方法非常接近,但存在显着差异,即存储库应该是注入服务的实例。静态东西永远不适合建模目的。

就像它在这里一样,ProductService是一个非常薄的包装器本身,所以它可能是多余的。然而,通常情况下,它是一个添加其他有趣行为的好地方,因为我试图用我的代码注释暗示。

答案 4 :(得分:1)

您可以解决此问题的另一种方法是在聚合根目录中跟踪它。如果Product和Item都是同一聚合的一部分,而Product是根,那么对所有Items的访问都是通过Product控制的。因此,在您的AddItem方法中,将新项目与当前最大项目进行比较,并在需要时替换它。将其保留在Product中所需的位置,这样您就不必运行SQL查询。这是定义聚合促进封装的一个原因。

答案 5 :(得分:0)

请记住,NHibernate是数据库和对象之间的映射器。您的问题在我看来,您的对象模型不是一个可行的关系模型,这没关系,但您需要接受它。

为什么不将另一个集合映射到您的Product实体,该集合使用关系模型的强大功能以高效的方式加载。我是否正确地假设选择这个特殊集合的逻辑不是火箭科学并且可以很容易地在过滤的NHibernate映射集合中实现?

我知道我的答案含糊不清,但我只能概括地理解你的问题。我的观点是,如果以面向对象的方式处理关系数据库, 会出现问题。像NHibernate这样的工具可以弥合它们之间的差距,而不是以同样的方式对待它们。请随时让我澄清一些我没有说清楚的观点。

答案 6 :(得分:0)

您现在可以直接使用NHibernate 5直接执行此操作而无需特定代码! 它不会将整个集合加载到内存中。

请参阅https://github.com/nhibernate/nhibernate-core/blob/master/releasenotes.txt

Build 5.0.0
=============================

** Highlights
...
    * Entities collections can be queried with .AsQueryable() Linq extension without being fully loaded.
...