23

所以我在一个应用程序中实现存储库模式,在我对模式的理解中遇到了两个“问题”:

  1. 查询 - 我已经阅读了使用存储库时不应使用 IQueryable 的响应。但是,很明显,您希望在每次调用方法时都不会返回完整的对象列表。是否应该实施?如果我有一个名为 List 的 IEnumerable 方法,那么 IQueryable 的一般“最佳实践”是什么?它应该/不应该有哪些参数?

  2. 标量值 - 返回单个标量值而不必返回整个记录的最佳方式是什么(使用存储库模式)?从性能的角度来看,在整行上只返回一个标量值不是更有效吗?

4

3 回答 3

35

严格来说,存储库提供了用于获取/放置域对象的集合语义。它围绕您的物化实现(ORM、手动、模拟)提供了一个抽象,以便域对象的消费者与这些细节分离。在实践中,存储库通常抽象对实体的访问,即具有身份的域对象,并且通常具有持久的生命周期(在 DDD 风格中,存储库提供对聚合根的访问)。

存储库的最小接口如下:

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

尽管您会看到命名差异和 Save/SaveOrUpdate 语义的添加,但以上是“纯粹”的想法。您将获得 ICollection 添加/删除成员以及一些查找器。如果您不使用 IQueryable,您还会在存储库中看到 finder 方法,例如:

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 方法,这些方法会随着查询需求的增长而使存储库变得混乱。

于 2009-08-28T16:43:17.460 回答
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 方法更好。

有些对象与其他对象有不同的要求。不能删除客户对象,不能更新 PurchaseOrder,只能创建 ShoppingCart 对象。当使用通用 IRepository 接口时,这显然会导致实现问题。

那些实现反模式的人通常会实现他们的完整接口,然后会为他们不支持的方法抛出异常。除了不同意许多 OO 原则之外,这打破了他们能够有效使用其 IRepository 抽象的希望,除非他们也开始在其上放置方法来确定是否支持给定对象并进一步实现它们。

解决此问题的一个常见解决方法是迁移到更精细的接口,例如 ICanDelete、ICanUpdate、ICanCreate 等。这在解决许多在 OO 原则方面出现的问题的同时,也大大减少了代码重用量,即被视为大多数时候将无法再使用存储库具体实例。

我们都不喜欢一遍又一遍地编写相同的代码。然而,作为架构接缝的存储库合同是扩大合同以使其更通用的错误位置。

这些摘录无耻地取自这篇文章,您还可以在评论中阅读更多讨论。

于 2009-08-28T19:06:38.127 回答
4

关于 1:据我所知,从存储库返回的问题不是 IQuerable 本身。存储库的意义在于它应该看起来像一个包含所有数据的对象。因此,您可以向存储库索取数据。如果您有多个对象需要相同的数据,则存储库的工作是缓存数据,因此存储库的两个客户端将获得相同的实例 - 因此,如果一个客户端更改了属性,另一个将看到,因为它们指向同一个实例。

如果存储库实际上是 Linq-provider 本身,那么这将很合适。但大多数人只是让 Linq-to-sql 提供程序的 IQuerable 直接通过,这实际上绕过了存储库的责任。所以存储库根本不是存储库,至少根据我对模式的理解和使用。

关于 2:自然地,仅从数据库返回单个值比返回整个记录更有效。但是使用存储库模式,您根本不会返回记录,而是返回业务对象。所以应用程序逻辑不应该关注域,而应该关注域对象。

但是与完整的域对象相比,返回单个值的效果如何呢?如果您的数据库模式定义合理,您可能无法衡量差异。

拥有干净、易于理解的代码比预先进行微观性能优化更为重要。

于 2009-08-28T15:50:42.417 回答