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

Мы разрабатываем приложение 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.

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

5 ответов

Решение

Эта проблема такая же старая, как и сам шаблон Repository. Недавнее введение 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 это целая другая банка червей - столько же мнений, сколько пупков на эту тему.

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

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

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

Павел

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

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

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

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

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