仓储模式最佳实践

23

我正在应用程序中实现仓储模式,并遇到了两个模式理解上的“问题”:

  1. 查询 - 我读到IQueryable在使用仓储时不应使用。然而,显然你希望这样做,以便每次调用方法时都不返回完整的对象列表。它应该被实现吗?如果我有一个名为List的IEnumerable方法,则IQueryable的一般“最佳实践”是什么?应该/不应该具有哪些参数?

  2. 标量值 - 从使用仓储模式的角度来看,如何返回一个单一的标量值,而不必返回整个记录?从性能的角度来看,仅返回单个标量值是否更有效率?

3个回答

35

严格来说,仓库(Repository)提供了获取/存储领域对象的集合语义。它为您的材料化实现(ORM、手动编写、模拟)提供了一个抽象,以便领域对象的消费者与这些细节解耦。在实践中,仓库通常抽象访问实体,即具有标识和持久生命周期的领域对象。在DDD风格中,仓库提供对聚合根的访问。

仓库的最小接口如下:

void Add(T entity);
void Remove(T entity);
T GetById(object id);
IEnumerable<T> Find(Specification spec);

虽然你会看到命名差异以及Save/SaveOrUpdate语义的添加,但以上是“纯粹”的想法。你可以获得ICollection Add/Remove成员以及一些查找器。如果你不使用IQueryable,你还将在存储库上看到查找方法,如:

FindCustomersHavingOrders();
FindCustomersHavingPremiumStatus();

在这种情况下使用IQueryable存在两个相关问题。第一个问题是可能会泄露实现细节到客户端,以域对象的关系形式,即违反迪米特法则。第二个问题是存储库获得了可能不属于域对象存储库本身的查找职责,例如查找投影,这些投影与请求的域对象不太相关。
此外,使用IQueryable“打破”了模式:具有IQueryable的存储库可以或可以不提供对“域对象”的访问。IQueryable使客户端有很多选项,可以在最终执行查询时将要实例化的内容。这是关于使用IQueryable的争论的主要内容。
关于标量值,您不应该使用存储库返回标量值。如果需要标量,则通常从实体本身获取。如果听起来效率低下,那么它确实如此,但根据您的负载特性/要求,您可能不会注意到。在需要因性能原因或因为需要合并来自许多域对象的数据而需要域对象的替代视图的情况下,有两个选择。
1)使用实体的存储库查找指定的实体并将其投影/映射到扁平视图。
2)创建专用于返回封装所需扁平视图的新域类型的查找器接口。这不是存储库,因为没有集合语义,但它可能在底层使用现有的存储库。
如果您使用“纯”存储库访问持久实体,则需要考虑一件事情,即会损失ORM的某些好处。在“纯”实现中,客户端无法提供有关如何使用域对象的上下文,因此您无法告诉存储库:“嘿,我只会更改customer.Name属性,所以不要费力获取那些急切加载的引用。”另一方面,问题是客户端是否应该知道这些内容。这是一个双刃剑。
至于使用IQueryable,大多数人似乎都习惯于“打破”模式以获得动态查询组合的好处,特别是对于像分页/排序这样的客户端职责。在这种情况下,您可能会有:
Add(T entity);
Remove(T entity);
T GetById(object id);
IQueryable<T> Find();

当查询需求增加时,您可以放弃所有这些自定义的Finder方法,这真的会使存储库变得混乱。


1
我今天看到了jbogard关于这个主题的帖子:http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/09/02/ddd-repository-implementation-patterns.aspx - pfries
4
大多数人似乎乐于“打破”模式以获得动态查询组合的好处。 - Jim G.

10
作为对@lordinateur的回应,我并不喜欢指定存储库接口的默认方式。
因为你的解决方案中的接口要求每个存储库实现至少需要Add、Remove、GetById等方法。现在考虑一种场景,其中通过特定实例保存并没有意义,你仍然必须使用NotImplementedException或类似的方法来实现其余方法。
我更喜欢将存储库接口声明分开,如下所示:
interface ICanAdd<T>
{
    T Add(T entity);
}

interface ICanRemove<T>
{
    bool Remove(T entity);
}

interface ICanGetById<T>
{
    T Get(int id);
}

一个特定的 SomeClass 实体的仓库实现可能如下所示:
interface ISomeRepository
    : ICanAdd<SomeClass>, 
      ICanRemove<SomeClass>
{
    SomeClass Add(SomeClass entity);
    bool Remove(SomeClass entity);
}

让我们退一步,看看为什么我认为这比在一个通用接口中实现所有CRUD方法更好。
一些对象具有不同的要求。客户对象可能无法删除,采购订单无法更新,而购物车对象只能创建。当使用通用IRepository接口时,这显然会导致实现上的问题。
那些实现反模式的人通常会实现完整的接口,然后对他们不支持的方法抛出异常。除了与众多面向对象原则不符之外,这还破坏了他们希望能够有效使用其IRepository抽象的希望,除非他们还开始将方法放在其中,以确定是否支持给定对象,并进一步实现它们。
解决此问题的常见方法是转移到更细粒度的接口,例如ICanDelete、ICanUpdate、ICanCreate等等。虽然这样做可以解决许多面向对象原则方面出现的问题,但也大大减少了代码重用量,因为大多数情况下,人们将无法再使用Repository具体实例。
我们都不喜欢一遍又一遍地编写相同的代码。然而,作为架构接缝的存储库契约是扩展契约使其更通用的错误位置。
这些摘录已经无耻地从这篇文章中提取出来,您也可以在评论中阅读更多讨论。

如果您不通过“保存”来使用存储库,那么它会是什么样子?如果去掉添加/删除部分,您只剩下一个Finder界面。如果您只想查找内容,您不需要存储库,而是需要某种ICustomerFinder实现。也就是说,您的存储库可以实现Finder:interface ICustomerRepository:IRepository<Customer>,ICustomerFinder仅仅为了澄清,您不会真正通过存储库接口“保存”,这将是某种工作单元实现的责任。这样的会话最好在存储库之外进行管理。 - pfries
Greg关于组合的观点是很好的,即您可以使用内部通用存储库实现域对象存储库。当在ORM(如NHibernate)周围使用像ICustomerRepository这样的接口时(它替代了通用存储库),实际上就是这样工作的。他的主要观点是不要在应用程序的接缝处使用通用契约。这是非常合理的;如果您的契约是ICustomerRepository而不是IRepository,则可以避免他所描述的问题。尽管它们表达了意图,但我并不喜欢所有的查找器。查询对象怎么样? - pfries
froghammr:经过我的所有研究,我最终选择了查询对象。我正在使用MonoDroid/C#开发移动应用程序,没有LINQ、Entity或Hibernate,所以我必须从头开始编写所有内容。尽管如此,Finder/Specification的开销仍然很大。 - samus

4
关于第1点: 就我所看到的,从仓库返回的并不是IQueryable本身成为问题。仓库的目的是看起来像一个包含所有数据的对象,这样你就可以向仓库请求数据。如果有多个对象需要相同的数据,则仓库的工作是缓存数据,因此仓库的两个客户端将获得相同的实例 - 因此,如果一个客户端更改属性,则另一个客户端将看到该更改,因为它们指向同一实例。
如果仓库实际上是Linq提供程序本身,那就完全符合要求。但大多数人只是让Linq-to-sql提供程序的IQueryable直接通过,这实际上绕过了仓库的责任。因此,仓库根本不是仓库,至少根据我的理解和模式的使用方式。
关于第2点: 自然而然地,仅从数据库返回单个值比返回整个记录更有效率。但使用仓库模式,您根本不会返回记录,而是返回业务对象。因此,应用程序逻辑不应关注字段,而应关注域对象。
但是,与返回完整的域对象相比,返回单个值有多么有效?如果您的数据库架构合理定义,那么您可能无法测量差异。
拥有清晰易懂的代码比微观性能优化更重要。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接