Соответствует ли шаблон репозитория принципам 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, потому что они в основном связаны друг с другом.