Соответствует ли шаблон репозитория принципам 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. Как вы думаете? Почему мы должны выбирать этот тип паттерна, который нарушает Принципал? Нужно ваше мнение.

4 ответа

Решение

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

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

Разрывы репозитория Принцип разделения интерфейса

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

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

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

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

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

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

Разрывы репозитория Принцип разделения интерфейса

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

Как я уже говорил ранее, не требуется, чтобы 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) и заставил хранилище наследовать все эти интерфейсы. Таким образом, я могу реализовать все методы в конкретном классе и по-прежнему использовать все интерфейсы отдельно. Я не вижу причин утверждать, что шаблон репозитория нарушает принципы 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
{

}

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

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

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

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

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

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

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

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

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

Последний CLI, который я реализовал, также имел только одну ответственность... обрабатывать связь с некоторым API... но на один уровень глубже... вы понимаете

Я предпочитаю определение Роберта К. Мартина, в котором говорится, что SRP означает: "Есть только одна причина для изменения". На мой взгляд, это более точное определение. Репозиторий может измениться при изменении записи / обновления (аудит), при изменении чтения (кэширование) или при изменении удаления (вызов перед фактическим удалением) и так далее.

После этого предложенные ответы с отдельными интерфейсами для каждой операции CRUD будут следовать за SRP, а также за ISP, потому что они в основном связаны друг с другом.

Другие вопросы по тегам