Является ли шаблон хранилища следовать принципам SOLID?

Я занимаюсь исследованием принципала SOLID и обнаружил некоторые проблемы в реализации шаблона репозитория. Я собираюсь объяснить каждую проблему, пожалуйста, исправьте меня, если я ошибаюсь.

Задача 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!
   }
}

И согласно провайдеру "Ни один клиент не должен быть вынужден зависеть от методов, которые он не использует". Итак, мы увидели, что это также явно нарушает провайдера.

Итак, я понимаю, что шаблон репозитория не следует принципу SOLID. Как вы думаете? Почему мы должны выбирать этот тип паттерна, который нарушает Принципал? Нужно ваше мнение.

Ответ 1

Очевидно, что это нарушает принцип единой ответственности, потому что когда мы реализуем этот интерфейс, в одном классе мы помещаем Command и Query оба. и этого не ожидалось.

Это не тот принцип единственной ответственности. SRP означает, что класс должен иметь одну основную проблему. Первичная задача репозитория заключается в том, чтобы "посредничать между слоями отображения домена и данных с использованием интерфейса, подобного коллекции, для доступа к объектам домена" (Fowler). Это то, что делает этот класс.

Принцип разложения схем хранилища репозитория

Если это вас беспокоит, просто укажите другой интерфейс, который не включает метод, который вы не собираетесь внедрять. Я лично этого не сделал бы; у него много дополнительных интерфейсов для предельной выгоды, и он излишне загромождает API. A NotImplementedException очень понятен.

Вы обнаружите, что в вычислениях есть много правил, законов или принципов, которые имеют исключения, а некоторые - совершенно неправильные. Примите двусмысленность, научитесь писать программное обеспечение с более практической точки зрения и переставайте думать о разработке программного обеспечения в таких абсолютных терминах.

Ответ 2

Очевидно, что это нарушает принцип единой ответственности

Это только ясно, если у вас очень узкое определение того, что такое СРП. Дело в том, что SOLID нарушает SOLID. Сами принципы противоречат сами себе. SRP находится в противоречии с DRY, так как вам часто приходится повторять себя, чтобы правильно разделить проблемы. В некоторых ситуациях LSP противоречит ISP. OCP часто конфликтует с DRY и SRP. Эти принципы здесь не такие жесткие и быстрые правила, но чтобы вести вас... старайтесь придерживаться их, но не относитесь к ним как к законам, которые нельзя сломать.

Кроме того, вы смешиваете шаблон архитектуры хранилища с очень специфичным шаблоном реализации Generic Repository. Обратите внимание, что общий репозиторий отличается от конкретного репозитория. Также нет требования о том, чтобы репозиторий применял методы, о которых вы упоминаете.

Да, вы можете отделить команду и запрос как две отдельные проблемы, но нет требования, чтобы вы делали это, чтобы сделать каждую отдельную ответственность. Command Query Seperation - это хороший принцип, но не тот, который покрыт SOLID, и, конечно же, нет консенсуса относительно того, отделяет ли эта проблема от ответственности за разные обязанности. Они больше похожи на разные аспекты одной и той же ответственности. Вы можете принять это на смеховом уровне, если хотите, и утверждать, что обновление - это другая ответственность за удаление или что запрос по идентификатору - это другая ответственность от запроса по типу или тому подобное. В какой-то момент вам нужно нарисовать линии и вещи в коробке, и для большинства людей "чтение и запись объекта" - это отдельная ответственность.

Принцип разложения схем хранилища репозитория

Во-первых, вы путаете Принципа Записки Liskov с Принципом Разделения Интерфейса. LSP - это то, что нарушено вашим примером.

Как я уже говорил ранее, нет требования, чтобы Repository реализовал какой-либо определенный набор методов, отличных от "подобный сборке интерфейс". На самом деле было бы вполне приемлемо реализовать его так:

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

Теперь он может опционально реализовать любой из других членов, не нарушая LSP, хотя это довольно глупая реализация, и я, конечно, не буду реализовывать это просто, чтобы не нарушать LSP.

Факт, вероятно, нет веской причины, по которой вам нужен репозиторий без удаления. Единственная возможная причина, по которой я могу думать, - это репозиторий "Только для чтения", который я бы назвал отдельным интерфейсом для использования интерфейса коллекции только для чтения.

Ответ 3

Я использую шаблон репозитория самостоятельно, и я использовал шаблон, чтобы убедиться, что все необходимые интерфейсы реализованы. Для этого я создал отдельные интерфейсы для всех действий (IEntityCreator, IEntityReader, IEntityUpdater, IEntityRemover) и сделал repostory наследовать все эти интерфейсы. Таким образом, я могу реализовать все методы в конкретном классе и по-прежнему использовать все интерфейсы отдельно. Я не вижу причин утверждать, что шаблон хранилища нарушает принципы SOLID. Вам просто нужно правильно определить "ответственность" репозитория: ответственность за репозиторий заключается в том, чтобы облегчить доступ к данным типа Т. Это все, что можно сказать. Как указано выше, у меня также есть интерфейс репозитория только для чтения с именем 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
{

}

Ответ 4

Я думаю, что это сломает провайдера. Просто так.

Возможно, это такая устоявшаяся модель, что людям трудно смириться с этим.

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

Принцип сегрегации интерфейса (ISP) гласит, что ни один клиент не должен зависеть от методов, которые он не использует. [1] Интернет-провайдер разбивает очень большие интерфейсы на более мелкие и более конкретные, чтобы клиенты могли знать только о методах, которые им интересны.

Я реализую ресурс API, чтобы получить заказ. С типичным репозиторием я вынужден зависеть от гигантского репозитория, который может обновлять и удалять вещи.

Я бы предпочел просто зависеть от насмешек или подделок, типа, который просто получает приказы.

Ответ 5

Я знаю, что это старый пост, просто хотел предложить мои 2 цента. Если вы хотите следовать более четко, вам нужно разделить интерфейс на отдельные версии. Один для чтения, один для редактирования, удаления и т.д. Для разделения интерфейса. Но в идеале я бы не использовал шаблон хранилища, и я бы предпочел создать хранилище для каждой сущности или цели с собственным интерфейсом.