存储库模式是否遵循 SOLID 原则?

Does Repository Pattern follow SOLID principles?

我正在对 SOLID principal 进行一些研究,发现了 Repository 模式实现中的一些问题。我将逐一解释,如有错误请指正。

问题1

存储库模式打破单一责任原则(S)

假设我们有一个定义为

的接口
public interface IRepository<T> where T: IEntity
{ 
    IEnumerable<T> List { get; }
    void Add(T entity);
    void Delete(T entity);
    void Update(T entity);
    T FindById(int Id);
}

显然它违反了单一责任原则,因为当我们实现这个接口时,在一个 class 中我们同时放置了命令和查询。这不是预期的。

问题2

Repository Pattern Breaks 接口隔离原则(I)

假设我们有 2 个上述接口的实现。

首次实施

CustomerRepository : IRepository<Customer>
{
   //All Implementation
}

第二次实现

ProductRepository : IRepository<Product>
{
   //All Implementation except Delete Method. So Delete Method Will be
   void Delete (Product product){
       throw Not Implement Exception!
   }
}

并且根据 ISP "No client should be forced to depend on methods it does not use." 所以我们清楚地看到它也违反了 ISP。

所以,我的理解是 Repository 模式不遵循 SOLID 原则。你怎么看?为什么要选择这种违背Principal的模式呢?需要你的意见。

Clearly it violates the single responsibility principle because when we implement this interface, In a single class we are putting Command and Query both. and this not expected.

这不是单一职责原则的意思。 SRP 意味着 class 应该 一个主要关注点。 存储库的主要关注点是 "mediate between the domain and data mapping layers using a collection-like interface for accessing domain objects" (Fowler)。这就是 class 的作用。

Repository Pattern Breaks Interface segregation principle

如果这让您感到困扰,那么只需提供另一个不包含您不打算实现的方法的接口。不过,我个人不会那样做;它有很多额外的接口以获得边际收益,并且不必要地使 API 混乱。 NotImplementedException 非常不言自明。

您会发现计算中有很多规则、法律或原则都有例外,有些甚至是完全错误的。拥抱歧义,学习从更实际的角度编写软件,不要再用绝对的术语来思考软件设计。

Clearly it violates the single responsibility principle

只有当您对 SRP 的定义非常狭窄时才会清楚。事实是 SOLID 违反了 SOLID。这些原则本身自相矛盾。 SRP 与 DRY 不一致,因为您经常必须重复自己才能正确分离关注点。 LSP 在某些情况下与 ISP 不一致。 OCP 经常与 DRY 和 SRP 发生冲突。这些原则在这里并不是一成不变的规则,而是为了指导你……尽量遵守它们,但不要把它们当作不能违背的法则。

最重要的是,您将存储库架构模式与非常具体的通用存储库实现模式混淆了。请注意,通用存储库不同于具体存储库。也没有要求存储库实现您提到的方法。

是的,您可以将命令和查询作为两个单独的关注点分开,但是没有要求您这样做以使每个单独的职责。命令查询分离是一个很好的原则,但不是 SOLID 涵盖的内容,而且对于分离关注点是否属于不同职责的前提,当然没有达成共识。它们更像是同一责任的不同方面。如果您愿意并声称更新与删除的责任不同,或者按 id 查询与按类型查询或其他任何责任不同,您可以把它带到一个荒谬的水平。在某些时候,您必须划清界限并将内容框入,对于大多数人来说 "reading and writing an entity" 是一个单一的责任。

Repository Pattern Breaks Interface segregation principle

首先,您混淆了里氏替换原则和接口隔离原则。 LSP 是你的例子所违反的。

正如我之前所说,除了 "collection-like interface" 之外,没有要求 Repository 实现任何特定的方法集。事实上,这样实现它是完全可以接受的:

public interface IRepository<T> where...[...] {IEnumerable<T> List { get; }}
public interface CustRepository : IRepository<Customer>, IRepoAdd, IRepoUpdate, IRepoDelete, IRepoFind {}

现在它可以在不破坏 LSP 的情况下选择性地实现任何其他成员,尽管这是一个相当愚蠢的实现,我当然不会为了避免破坏 LSP 而实现。

事实是,您可能没有充分的理由想要一个没有删除的存储库。我能想到的唯一可能的原因是只读存储库,我会定义一个单独的接口来使用只读集合接口。

我自己使用 Repository 模式,我使用该模式来确保实现所有必需的接口。为此,我为所有操作(IEntityCreator、IEntityReader、IEntityUpdater、IEntityRemover)创建了单独的接口,并使 repostiory 继承了所有这些接口。这样我就可以在一个具体的 class 中实现所有方法,并且仍然单独使用所有接口。我看不出有什么理由说 Repository 模式违反了 SOLID 原则。你只需要正确定义repository的'responsibility'即可:Repository的职责是方便所有对T类型数据的访问。如上所述,我还有一个名为 ReferenceRepository<T> 的只读存储库接口,它只包含 IEntityReader<T> 接口。下面定义了所有接口以便快速复制 :) 除此之外,我还创建了一些具体的 classes,包括缓存 and/or 日志记录。这是为了合并 SOLIDI 所要求的任何进一步行动。类型 IEntity 用作标记接口以仅允许实体而不允许某些其他类型的对象(您必须从某个地方开始)。

/// <summary>
/// This interface defines all properties and methods common to all Entity Creators.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public interface IEntityCreator<TEntity>
    where TEntity : IEntity
{
    #region Methods
    /// <summary>
    /// Create a new instance of <see cref="TEntity"/>
    /// </summary>
    /// <returns></returns>
    TEntity Create();
    #endregion
}

/// <summary>
/// This interface defines all properties and methods common to all Entity Readers.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public interface IEntityReader<TEntity>
   where TEntity : IEntity
{
    #region Methods
    /// <summary>
    /// Get all entities in the data store.
    /// </summary>
    /// <returns></returns>
    IEnumerable<TEntity> GetAll();

    /// <summary>
    /// Find all entities that match the expression
    /// </summary>
    /// <param name="whereExpression">exprssion used to filter the results.</param>
    /// <returns></returns>
    IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> whereExpression);
    #endregion
}

/// <summary>
/// This interface defines all properties and methods common to all Entity Updaters. 
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public interface IEntityUpdater<TEntity>
    where TEntity : IEntity
{
    #region Methods
    /// <summary>
    /// Save an entity in the data store
    /// </summary>
    /// <param name="entity">The entity to save</param>
    void Save(TEntity entity);
    #endregion
}

/// <summary>
/// This interface defines all properties and methods common to all Entity removers.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public interface IEntityRemover<TEntity>
    where TEntity : IEntity
{
    /// <summary>
    /// Delete an entity from the data store.
    /// </summary>
    /// <param name="entity">The entity to delete</param>
    void Delete(TEntity entity);

    /// <summary>
    /// Deletes all entities that match the specified where expression.
    /// </summary>
    /// <param name="whereExpression">The where expression.</param>
    void Delete(Expression<Func<TEntity, bool>> whereExpression);
}

/// <summary>
/// This interface defines all properties and methods common to all Repositories.
/// </summary>
public interface IRepository { }

/// <summary>
/// This interface defines all properties and methods common to all Read-Only repositories.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public interface IReferenceRepository<TEntity> : IRepository, IEntityReader<TEntity>
   where TEntity : IEntity
{

}

/// <summary>
/// This interface defines all properties and methods common to all Read-Write Repositories.
/// </summary>
public interface IRepository<TEntity> : IReferenceRepository<TEntity>, IEntityCreator<TEntity>,
    IEntityUpdater<TEntity>, IEntityRemover<TEntity>
    where TEntity : IEntity
{

}

我认为它确实破坏了 ISP。就是这样。

也许这是一种让人难以接受的既定模式。

https://www.newyorker.com/magazine/2017/02/27/why-facts-dont-change-our-minds

The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.[1] ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.

我正在实施 API 资源来获取订单。对于一个典型的存储库,我不得不依赖一个可以更新和删除内容的巨大存储库。

我宁愿只依赖,模拟或伪造,一种只得到订单的类型。

我知道这是旧的 post 只是想提供我的 2 美分。如果你想更好地遵循 solid,你需要将界面分成不同的版本。一个用于读取,一个用于编辑,删除等接口隔离。但理想情况下,我不会使用存储库模式,我宁愿为每个实体或目的创建一个存储库,它有自己的界面。

是的,这是旧的,但我认为有一部分仍然不清楚。 我同意所提议的存储库模式明显破坏了 SRP,但是它取决于 SRP 的定义

某些东西应该只具有“单一责任”的定义是模糊的,因为在那种情况下你总是可以争辩说某物只有一个责任。是的,确保存储库仅在您的应用程序和数据库之间进行调解。但更深一层,它负责读取、写入、更新、删除...

我实现的最后一个 CLI 也只有一个职责...处理与某些人的通信 API...但更深一层...您明白了

我更喜欢 Robert C. Martin 的定义,其中指出 SRP 的意思是:“只有一个改变的理由。”我认为这更准确。如果 write/update 更改(审核)、读取更改(缓存)或删除更改(实际删除前的挑战)等,则存储库可能会更改。

接下来,针对每个 CRUD 操作的单一接口的建议答案将遵循 SRP 并遵循 ISP,因为两者基本上是相互关联的。