Перевага створення загального сховища порівняно з конкретним сховищем для кожного об’єкта?


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 *, але це все ще здається кращим, ніж висунення логіки виразів до класів служби.

Що я пропускаю?


Використання загальних сховищ з повними ORM виглядає марним. Я детально обговорював це тут .
Аміт Джоші

Відповіді:


169

Це питання, старий, як і сам шаблон сховища. Нещодавнє введення LINQ IQueryable, рівномірного подання запиту, викликало багато дискусій з цієї самої теми.

Сам я віддаю перевагу конкретним сховищам після того, як дуже наполегливо працював над створенням загальної бази репозиторіїв. Незалежно від того, який розумний механізм я намагався, я завжди стикався з однією проблемою: сховище - це частина домену, який моделюється, і цей домен не є загальним. Не кожну сутність можна видалити, не кожну сутність можна додати, не кожна сутність має сховище. Запити різко змінюються; API сховища стає таким же унікальним, як і сама сутність.

Шаблон, який я часто використовую, - це мати конкретні інтерфейси сховища, але базовий клас для реалізацій. Наприклад, використовуючи LINQ для 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
Серйозно, чудова відповідь. Дякую!
Звуковий сигнал

5
То як би ви використовували IoC / DI для цього? (Я новачок у IoC) Моє питання стосується вашого шаблону в повному обсязі: stackoverflow.com/questions/4312388 / ...
дан

36
"репозиторій - це частина домену, який моделюється, і цей домен не є загальним. Не кожну сутність можна видалити, не кожну сутність можна додати, не кожна сутність має сховище" ідеально!
adamwtiko

Я знаю, що це стара відповідь, але мені цікаво, якщо навмисно виключати метод оновлення з класу репозиторію. У мене виникають проблеми з чистим способом зробити це.
rtf

1
Старий, але добрий. Ця публікація неймовірно мудра, і її слід читати та перечитувати, але всім хорошим розробникам. Дякую @BryanWatts Моя реалізація, як правило, заснована на Dapper, але припущення - те саме. Базовий сховище зі специфічними сховищами для представлення домену, який використовується для функцій.
pimbrouwers

27

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

Так, у подібних випадках я часто створював загальний IRepository, який має повний стек CRUD, і це дозволяє мені швидко грати з API та дозволяти людям грати без інтерфейсу та паралельно робити тестування інтеграції та прийняття користувачів. Потім, коли мені здається, що мені потрібні конкретні запити на репо, тощо, я починаю замінювати цю залежність w / конкретну, якщо потрібно, і йду звідти. Один основний імпл. легко створювати та використовувати (і, можливо, підключити до db пам'яті або статичних об'єктів, або знущаються з них або будь-якого іншого).

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

paul


4
Дякуємо за відповідь @Paul. Я фактично спробував і цей підхід. Я не міг зрозуміти, як узагальнено висловити найперший метод , який я намагався, GetById(). Чи варто використовувати IRepository<T, TId>, GetById(object id)чи робити припущення та використовувати GetById(int id)? Як би працювали складові ключі? Мені було цікаво, чи є загальний вибір за ідентифікацією гідною абстракцією. Якщо ні, то що ще б загальні сховища були б змушені виражати аккредно? Це було судженням абстрагування реалізації , а не інтерфейсу .
Брайан Воттс

10
Також за загальний механізм запитів покладено обов'язок ORM. Ваші сховища повинні реалізовувати конкретні запити для суб'єктів вашого проекту, використовуючи загальний механізм запитів. Споживачів вашого сховища не слід змушувати писати власні запити, якщо це не є частиною вашого проблемного домену, наприклад, з доповіддю.
Брайан Воттс

2
@Bryan - щодо вашого GetById (). Я використовую FindById <T, TId> (ідентифікатор TId); Таким чином, виходить щось схоже на сховище.FindById <Invoice, int> (435);
Джошуа Хейс

Я зазвичай не ставлю методи запиту на рівні поля на загальних інтерфейсах, відверто кажучи. Як ви зазначали, не всі моделі повинні запитуватися одним ключем, а в деяких випадках ваша програма взагалі ніколи не отримуватиме щось за допомогою ідентифікатора (наприклад, якщо ви використовуєте первинні ключі, згенеровані DB, і ви отримуєте лише природний ключ, наприклад ім’я для входу). Методи запитів розвиваються на конкретних інтерфейсах, які я будую в рамках рефакторингу.
Пол

13

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


Чи можете ви надати невеликий приклад фрагменту?
Йоганн Герелл

@Johann Gerell ні, тому що я більше не використовую сховища.
Арніс Лапса

чим ви користуєтесь зараз, коли тримаєтесь подалі від сховищ?
Кріс

@Chris я зосереджуюсь на тому, щоб мати багату модель домену. Вхідна частина програми - це те, що важливо. якщо всі зміни стану ретельно відстежуються, не важливо, наскільки ви читаєте дані, доки вони досить ефективні. тому я просто використовую NHibernate ISession безпосередньо. без абстрагування шару сховища набагато простіше вказати такі речі, як прагнення до завантаження, багато запитів і т. д., і якщо вам це справді потрібно, не важко також знущатися з ISession.
Арніс Лапса

3
@JesseWebb Naah ... З багатою доменною моделлю домена логіка запитів значно спрощується. Наприклад, якщо я хочу шукати користувачів, які щось придбали, я просто шукаю користувачів. Куди (u => u.HasPurchasedAnything) замість користувачів.Join (x => Замовлення, щось щось, я не знаю linq). order => order.Status == 1) .Join (x => x.products). Куди (x .... і т. д. і т. д. .... бла-бла-бла
Арніс Лапса

5

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


2

UserRepository публічного класу: сховище, IUserRepository

Чи не слід вводити IUserRepository, щоб не піддавати інтерфейс. Як кажуть люди, можливо, вам не знадобиться повний стек CRUD тощо.


2
Це більше схоже на коментар, ніж на відповідь.
Аміт Джоші
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.