Преимущество создания общего репозитория по сравнению с конкретным репозиторием для каждого объекта?

132

Мы разрабатываем приложение ASP.NET MVC и сейчас создаем классы репозитория / службы. Мне интересно, есть ли какие-либо серьезные преимущества в создании общего интерфейса IRepository, который реализуют все репозитории, по сравнению с каждым репозиторием, имеющим свой собственный уникальный интерфейс и набор методов.

Например: общий интерфейс IRepository может выглядеть (взято из этого ответа ):

public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

Каждый репозиторий будет реализовывать этот интерфейс, например:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • и т.п.

Альтернатива, которой мы следовали в предыдущих проектах, будет:

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

Во втором случае выражения (LINQ или иначе) будут полностью содержаться в реализации репозитория, и тот, кто реализует службу, просто должен знать, какую функцию репозитория вызывать.

Думаю, я не вижу преимущества написания всего синтаксиса выражений в классе обслуживания и передачи его в репозиторий. Разве это не означало бы, что простой код LINQ во многих случаях дублируется?

Например, в нашей старой системе выставления счетов мы вызываем

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

из нескольких различных сервисов (клиент, счет, счет и т. д.). Это кажется намного проще, чем писать следующее в нескольких местах:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

Единственный недостаток, который я вижу в использовании конкретного подхода, заключается в том, что мы можем получить множество перестановок функций Get *, но это все же кажется предпочтительным, чем проталкивание логики выражения в классы Service.

Чего мне не хватает?

Beep Beep
источник
Использование общих репозиториев с полными ORM выглядит бесполезным. Я подробно об этом говорил здесь .
Амит Джоши

Ответы:

169

Это проблема такая же старая, как и сам шаблон репозитория. Недавнее введение LINQ IQueryable, единообразного представления запроса, вызвало много дискуссий по этой самой теме.

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

Я часто использую шаблон, чтобы иметь определенные интерфейсы репозитория, но базовый класс для реализаций. Например, используя LINQ to SQL, вы можете:

public abstract class Repository<TEntity>
{
    private DataContext _dataContext;

    protected Repository(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    protected IQueryable<TEntity> Query
    {
        get { return _dataContext.GetTable<TEntity>(); }
    }

    protected void InsertOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
    }

    protected void DeleteOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
    }
}

Замените DataContextединицей работы по своему выбору. Пример реализации может быть:

public interface IUserRepository
{
    User GetById(int id);

    IQueryable<User> GetLockedOutUsers();

    void Insert(User user);
}

public class UserRepository : Repository<User>, IUserRepository
{
    public UserRepository(DataContext dataContext) : base(dataContext)
    {}

    public User GetById(int id)
    {
        return Query.Where(user => user.Id == id).SingleOrDefault();
    }

    public IQueryable<User> GetLockedOutUsers()
    {
        return Query.Where(user => user.IsLockedOut);
    }

    public void Insert(User user)
    {
        InsertOnCommit(user);
    }
}

Обратите внимание, что общедоступный API репозитория не позволяет удалять пользователей. Кроме того, разоблачение IQueryable- это совершенно другая баня червей - мнений на эту тему столько же, сколько и пупков.

Брайан Уоттс
источник
9
Серьезно, отличный ответ. Спасибо!
Beep beep
5
Итак, как бы вы использовали с этим IoC / DI? (Я новичок в IoC) Мой вопрос относительно вашего шаблона в полном объеме: stackoverflow.com/questions/4312388/…
дан
36
«репозиторий является частью моделируемого домена, и этот домен не является общим. Не каждый объект может быть удален, не каждый объект может быть добавлен, не каждый объект имеет репозиторий» идеально!
adamwtiko
Я знаю, что это старый ответ, но мне любопытно, намеренно ли исключен метод Update из класса Repository. У меня проблемы с поиском чистого способа сделать это.
RTF
1
Олди, но положительный герой. Этот пост невероятно мудр, его следует читать и перечитывать, но всем состоятельным разработчикам. Спасибо @BryanWatts. Моя реализация обычно основана на dapper, но предпосылка та же. Базовый репозиторий с определенными репозиториями для представления домена, который выбирает функции.
pimbrouwers
27

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

Поэтому в подобных случаях я часто создавал общий IRepository с полным стеком CRUD, что позволяет мне быстро играть с API и позволять людям играть с пользовательским интерфейсом и параллельно проводить интеграционное и приемлемое тестирование пользователей. Затем, когда я обнаружил, что мне нужны конкретные запросы в репо и т. Д., Я начинаю заменять эту зависимость на конкретную, если это необходимо, и переходить оттуда. Один базовый имп. легко создать и использовать (и, возможно, подключиться к базе данных в памяти или статическим объектам, имитируемым объектам или чему-то еще).

Тем не менее, то, что я начал делать в последнее время, - это ломать поведение. Итак, если вы создаете интерфейсы для IDataFetcher, IDataUpdater, IDataInserter и IDataDeleter (например), вы можете смешивать и согласовывать, чтобы определять свои требования через интерфейс, а затем иметь реализации, которые позаботятся о некоторых или всех из них, и я могу по-прежнему внедрять универсальную реализацию, чтобы использовать ее, пока я создаю приложение.

Павел

Павел
источник
4
Спасибо за ответ @Paul. Я тоже пробовал этот подход. Я не мог понять, как обобщенно выразить самый первый метод , который я пытался, GetById(). Должен ли я использовать IRepository<T, TId>, GetById(object id)или делать предположения и использовать GetById(int id)? Как будут работать составные ключи? Я задавался вопросом, является ли общий выбор по идентификатору стоящей абстракцией. Если нет, то что еще в общих репозиториях пришлось бы выразить неудобно? Это была аргументация абстрагирования реализации , а не интерфейса .
Брайан Уоттс,
10
Кроме того, за общий механизм запросов отвечает ORM. Ваши репозитории должны реализовывать конкретные запросы для сущностей вашего проекта с использованием универсального механизма запросов. Потребителей вашего репозитория не следует заставлять писать свои собственные запросы, если это не является частью вашей проблемной области, например, с отчетами.
Брайан Уоттс,
2
@Bryan - По поводу вашего GetById (). Я использую FindById <T, TId> (TId id); В результате получается что-то вроде repository.FindById <Invoice, int> (435);
Джошуа Хейс
Честно говоря, я обычно не использую методы запросов на уровне поля в общих интерфейсах. Как вы отметили, не все модели следует запрашивать с помощью одного ключа, и в некоторых случаях ваше приложение вообще не будет извлекать что-либо по идентификатору (например, если вы используете первичные ключи, сгенерированные DB, и вы получаете только естественный ключ, например логин). Методы запросов развиваются на основе конкретных интерфейсов, которые я создаю в рамках рефакторинга.
Пол
13

Я предпочитаю определенные репозитории, которые происходят из общего репозитория (или списка общих репозиториев, чтобы указать точное поведение) с переопределяемыми сигнатурами методов.

Арнис Лапса
источник
Не могли бы вы привести небольшой пример отрывка?
Johann Gerell
@Johann Gerell нет, потому что я больше не использую репозитории.
Арнис Лапса,
что вы используете сейчас, когда держитесь подальше от репозиториев?
Крис
@Chris, я сильно сосредоточен на богатой модели предметной области. входная часть приложения - вот что важно. Если все изменения состояния тщательно отслеживаются, не имеет большого значения, как вы читаете данные, если это достаточно эффективно. поэтому я просто использую NHibernate ISession напрямую. без абстракции уровня репозитория гораздо проще указать такие вещи, как активная загрузка, множественные запросы и т. д., и если вам действительно нужно, несложно также имитировать ISession.
Арнис Лапса
3
@JesseWebb Naah ... С помощью богатой модели предметной области логика запросов значительно упрощается. Например, если я хочу найти пользователей, которые что-то купили, я просто ищу users.Where (u => u.HasPurchasedAnything) вместо users.Join (x => Orders, something something, я не знаю linq) .Where ( order => order.Status == 1) .Join (x => x.products) .Where (x .... и т.д. и т.д .... бла-бла-бла
Арнис Лапса
5

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

Питер
источник
2

открытый класс UserRepository: Repository, IUserRepository

Разве вы не должны вводить IUserRepository, чтобы избежать раскрытия интерфейса. Как уже говорили люди, вам может не понадобиться полный стек CRUD и т. Д.

Ste
источник
2
Это больше похоже на комментарий, чем на ответ.
Амит Джоши