我正在对SOLID主体进行一些研究,并在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);
}
显然它违反了单一责任原则,因为当我们实现这个接口时,在单个类中我们都放置了Command和Query。这不是预期的。
问题2
存储库模式中断接口隔离原则( 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“没有客户应该被迫依赖它不使用的方法。”所以我们看到它显然也违反了ISP。
所以,我的理解是Repository模式不遵循SOLID主体。你怎么看?我们为什么要选择这种违反校长的模式呢?需要你的意见。
答案 0 :(得分:24)
显然它违反了单一责任原则,因为当我们实现这个接口时,在单个类中我们都放置了Command和Query。这不是预期的。
这不是单一责任原则的含义。 SRP意味着该类应该具有一个主要关注点。存储库的主要关注点是“使用类似于集合的接口来访问域对象,在域和数据映射层之间进行调解”({{3 }})。这就是这门课的作用。
存储库模式中断接口隔离原则
如果这让您感到烦恼,那么只需提供另一个不包含您不会实施的方法的界面。不过,我个人不会这样做;它为边际收益提供了许多额外的接口,并且不必要地使API混乱。 NotImplementedException
非常不言自明。
你会发现计算中存在许多有例外的规则,法律或原则,有些是完全错误的。接受歧义,学习从更实际的角度编写软件,并停止以这样的绝对术语思考软件设计。
答案 1 :(得分:5)
显然它违反了单一责任原则
只有你对SRP的含义有一个非常狭隘的定义,这一点很清楚。事实是SOLID违反了SOLID。这些原则本身就是矛盾的。 SRP与DRY不一致,因为您经常需要重复自己以正确分离问题。在某些情况下,LSP与ISP不一致。 OCP经常与DRY和SRP发生冲突。这些原则并不是那么严格和快速的规则,而是指导你......尽量遵守它们,但不要将它们视为不能破坏的法则。
最重要的是,您将使用非常具体的通用存储库实现模式来混淆Repository体系结构模式。请注意,通用存储库与具体存储库不同。也没有要求Repository实现您提到的方法。
是的,您可以将命令和查询分开作为两个单独的问题,但不要求您这样做以使每个人都承担一项责任。命令查询分离是一个很好的原则,但不是SOLID所涵盖的内容,当然也没有就是否将问题分离出不同责任的问题达成共识。他们更像是同一责任的不同方面。如果您想要并且声称更新与删除不同,或者通过ID查询与按类型或其他方式查询不同,您可以将此设置为荒谬的级别。在某些时候,你必须画线和盒子,对大多数人来说,“阅读和写作一个实体”是一个单一的责任。
存储库模式中断接口隔离原则
首先,您将Liskov Substitution Principal与接口隔离原则混淆。 LSP是你的例子违反的。
正如我之前所说,除了“类似集合的接口”之外,不要求Repository实现任何特定的方法集。事实上,像这样实现它是完全可以接受的:
public interface IRepository<T> where...[...] {IEnumerable<T> List { get; }}
public interface CustRepository : IRepository<Customer>, IRepoAdd, IRepoUpdate, IRepoDelete, IRepoFind {}
现在它可以选择实现任何其他成员而不破坏LSP,虽然它是一个相当愚蠢的实现,我当然不会只是为了避免破坏LSP而实现。
事实上,您可能没有充分的理由想要一个没有删除的存储库。我能想到的唯一可能的原因是只读存储库,我将为使用只读集合接口定义一个单独的接口。
答案 2 :(得分:3)
我自己使用Repository模式,并使用该模式确保实现所有必需的接口。为此,我为所有操作(IEntityCreator,IEntityReader,IEntityUpdater,IEntityRemover)创建了单独的接口,并使repostiory继承了所有这些接口。这样我就可以在具体类中实现所有方法,并且仍然分别使用所有接口。我没有理由说明存储库模式违反了SOLID原则。您只需要正确定义存储库的“责任”:存储库的职责是促进对类型T数据的所有访问。这就是所有要说的。如上所述,我还有一个名为ReferenceRepository<T>
的只读存储库接口,它只包含IEntityReader<T>
接口。下面定义了所有接口以便快速复制:)最重要的是,我还创建了一些具体的类,包括缓存和/或日志记录。这将包含I
中SOLID
所述的任何进一步操作。类型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
{
}
答案 3 :(得分:1)
我认为它确实破坏了ISP。就是这样。
也许这是一个既定的模式,人们很难接受。
https://www.newyorker.com/magazine/2017/02/27/why-facts-dont-change-our-minds
接口隔离原则(ISP)指出,不应强迫任何客户端依赖其不使用的方法。[1] ISP将非常大的接口拆分为更小和更具体的接口,以便客户端只需要了解他们感兴趣的方法即可。
我正在实现API资源以获取订单。对于典型的存储库,我不得不依赖可以更新和删除内容的大型存储库。
我宁愿只依赖并嘲笑或伪造这种仅能获得订单的类型。
答案 4 :(得分:0)
我知道这是一条旧帖子,只想提供2美分。如果您想更好地遵循Solid,则需要将界面分为不同的版本。一种用于读取,一种用于编辑,删除等,用于接口隔离。但是理想情况下,我不会使用存储库模式,而是希望使用其自己的接口为每个实体或目的创建一个存储库。
答案 5 :(得分:0)
是的,这很古老,但是我认为其中一部分尚不清楚。 我同意提议的存储库模式显然会破坏SRP,但是这取决于SRP的定义。
关于某物仅应具有“ 单一责任”的定义是含糊的,因为在这种情况下,您总是可以辩称某物仅具有一种责任。是的,请确保存储库仅在您的应用程序和数据库之间进行中介。但是更深一层,它负责读取,写入,更新,删除...
我实施的最后一个CLI只是一个责任...处理与某些API的通信...但是更深一层...您明白了
我更喜欢罗伯特·C·马丁(Robert C. Martin)的定义,该定义指出SRP的意思是:“ 只有一个改变的理由。”在我看来,这是更为精确的。如果写入/更新发生更改(审核),读取发生更改(缓存)或删除发生更改(实际删除之前的挑战)等等,则存储库可能会发生更改。
接着,对于每个CRUD操作,建议的具有单个接口的答案将遵循SRP并且也遵循ISP,因为两者基本上彼此相关。