Zaleta tworzenia repozytorium ogólnego w porównaniu z repozytorium specyficznym dla każdego obiektu?

132

Opracowujemy aplikację ASP.NET MVC, a teraz budujemy klasy repozytorium / usług. Zastanawiam się, czy są jakieś główne zalety tworzenia ogólnego interfejsu IRepository, który implementują wszystkie repozytoria, w porównaniu z każdym repozytorium mającym swój własny, unikalny interfejs i zestaw metod.

Na przykład: ogólny interfejs IRepository może wyglądać (zaczerpnięty z tej odpowiedzi ):

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();
}

Każde repozytorium implementowałoby ten interfejs, na przykład:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • itp.

Alternatywą, którą stosowaliśmy w poprzednich projektach, byłoby:

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);
}

W drugim przypadku wyrażenia (LINQ lub inne) byłyby w całości zawarte w implementacji repozytorium, każdy, kto implementuje usługę, musi tylko wiedzieć, którą funkcję repozytorium wywołać.

Wydaje mi się, że nie widzę korzyści z pisania całej składni wyrażeń w klasie usług i przekazywania do repozytorium. Czy nie oznaczałoby to, że łatwy do zepsucia kod LINQ jest w wielu przypadkach duplikowany?

Na przykład w naszym starym systemie fakturowania dzwonimy

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

z kilku różnych usług (klient, faktura, konto itp.). Wydaje się to o wiele czystsze niż pisanie tego w wielu miejscach:

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

Jedyną wadą, jaką widzę przy stosowaniu tego konkretnego podejścia, jest to, że możemy skończyć z wieloma permutacjami funkcji Get *, ale nadal wydaje się to lepsze niż wypychanie logiki wyrażeń do klas Service.

czego mi brakuje?

Sygnał dźwiękowy
źródło
Używanie repozytoriów ogólnych z pełnymi ORMami wygląda na bezużyteczne. Omówiłem to szczegółowo tutaj .
Amit Joshi,

Odpowiedzi:

170

Jest to problem tak stary jak sam wzorzec repozytorium. Niedawne wprowadzenie LINQ IQueryable, jednolitej reprezentacji zapytania, wywołało wiele dyskusji na ten temat.

Sam wolę określone repozytoria, po ciężkiej pracy nad zbudowaniem ogólnej struktury repozytorium. Bez względu na sprytny mechanizm, który wypróbowałem, zawsze kończyłem na tym samym problemie: repozytorium jest częścią modelowanej domeny, a ta domena nie jest generyczna. Nie każdą jednostkę można usunąć, nie każdą można dodać, nie każda jednostka ma repozytorium. Zapytania różnią się znacznie; API repozytorium staje się tak unikalne jak sama jednostka.

Wzorzec, którego często używam, to mieć specyficzne interfejsy repozytorium, ale klasę bazową dla implementacji. Na przykład używając LINQ to SQL, możesz zrobić:

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);
    }
}

Zastąp DataContextwybraną jednostką pracy. Przykładową implementacją może być:

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);
    }
}

Zauważ, że publiczny interfejs API repozytorium nie pozwala na usuwanie użytkowników. Odsłanianie IQueryableto także cała puszka robaków - opinii na ten temat jest tyle, ile pępków.

Bryan Watts
źródło
5
Jak więc używałbyś do tego IoC / DI? (Jestem nowicjuszem w IoC) Moje pytanie w odniesieniu do twojego wzorca w całości: stackoverflow.com/questions/4312388/ ...
dan
36
„repozytorium jest częścią modelowanej domeny, a ta domena nie jest ogólna. Nie każdą jednostkę można usunąć, nie każdą jednostkę można dodać, nie każda jednostka ma repozytorium” idealnie!
adamwtiko
Wiem, że to stara odpowiedź, ale jestem ciekawy, czy celowo pomijam metodę Update z klasy Repository. Mam problem ze znalezieniem na to prostego sposobu.
rtf
@Tanner: Aktualizacje są niejawne - podczas modyfikowania śledzonego obiektu, a następnie zatwierdzanie DataContext, LINQ to SQL wydaje odpowiednie polecenie.
Bryan Watts,
1
Oldie but a goodie. Ten post jest niesamowicie mądry i należy go przeczytać i ponownie przeczytać, ale wszyscy zamożni programiści. Dzięki @BryanWatts. Moja implementacja jest zazwyczaj elegancka, ale założenie jest takie samo. Podstawowe repozytorium z określonymi repozytoriami reprezentującymi domenę, która akceptuje funkcje.
pim
27

Właściwie to trochę się nie zgadzam z postem Bryana. Myślę, że ma rację, że ostatecznie wszystko jest bardzo wyjątkowe i tak dalej. Ale jednocześnie większość z tego pojawia się podczas projektowania i stwierdzam, że po utworzeniu ogólnego repozytorium i użyciu go podczas opracowywania modelu mogę bardzo szybko uzyskać aplikację, a następnie zmienić ją na większą szczegółowość, gdy znajdę trzeba to zrobić.

Tak więc w takich przypadkach często tworzyłem ogólne repozytorium IR, które ma pełny stos CRUD, co pozwala mi szybko zabrać się do zabawy z API i pozwolić ludziom grać z interfejsem użytkownika i równolegle przeprowadzać testy integracji i akceptacji użytkownika. Następnie, gdy stwierdzam, że potrzebuję określonych zapytań dotyczących repozytorium itp., Zaczynam zastępować tę zależność w / w konkretną, jeśli to konieczne, i przechodzę stamtąd. Jeden podstawowy impl. jest łatwy do utworzenia i użycia (i prawdopodobnie podpięcia do bazy danych w pamięci lub obiektów statycznych, obiektów pozorowanych itp.).

To powiedziawszy, to, co ostatnio zacząłem, to zrywanie tego zachowania. Tak więc, jeśli tworzysz interfejsy dla IDataFetcher, IDataUpdater, IDataInserter i IDataDeleter (na przykład), możesz mieszać i dopasowywać, aby zdefiniować swoje wymagania za pośrednictwem interfejsu, a następnie mieć implementacje, które zajmą się niektórymi lub wszystkimi z nich, a ja mogę nadal wstrzykuj implementację zrób wszystko do użycia podczas tworzenia aplikacji.

Paweł

Paweł
źródło
4
Dzięki za odpowiedź @Paul. Właściwie próbowałem też tego podejścia. Nie mogłem dowiedzieć się, jak ogólnie wyrażają bardzo pierwszą metodę próbowałam GetById(). Czy powinienem używać IRepository<T, TId>, GetById(object id)czy przyjmować założenia i używać GetById(int id)? Jak działałyby klucze złożone? Zastanawiałem się, czy ogólna selekcja według ID była wartą zachodu abstrakcją. Jeśli nie, to co jeszcze repozytoria generyczne byłyby zmuszone do wyrażania skandalicznie? Taka była argumentacja za abstrakcją implementacji , a nie interfejsu .
Bryan Watts,
10
ORM jest również odpowiedzialny za ogólny mechanizm zapytań. Twoje repozytoria powinny implementować określone zapytania dla jednostek projektu za pomocą ogólnego mechanizmu zapytań. Konsumenci twojego repozytorium nie powinni być zmuszani do pisania własnych zapytań, chyba że jest to część domeny, w której występuje problem, na przykład przy raportowaniu.
Bryan Watts,
2
@Bryan - Odnośnie Twojego GetById (). Używam FindById <T, TId> (TId id); W rezultacie powstaje coś w rodzaju repository.FindById <Invoice, int> (435);
Joshua Hayes
Szczerze mówiąc, zwykle nie umieszczam metod zapytań na poziomie pola w ogólnych interfejsach. Jak zauważyłeś, nie wszystkie modele powinny być odpytywane za pomocą jednego klucza, aw niektórych przypadkach Twoja aplikacja w ogóle nie pobierze czegoś na podstawie identyfikatora (np. Jeśli używasz kluczy podstawowych wygenerowanych przez bazę danych i pobierasz tylko przez klucz naturalny, na przykład nazwa logowania). Metody zapytań ewoluują w określonych interfejsach, które tworzę w ramach refaktoryzacji.
Paul
13

Wolę określone repozytoria, które wywodzą się z repozytorium ogólnego (lub listy repozytoriów generycznych, aby określić dokładne zachowanie) z możliwymi do zastąpienia podpisami metod.

Arnis Lapsa
źródło
Czy mógłbyś podać mały przykład?
Johann Gerell,
@Johann Gerell nie, ponieważ nie używam już repozytoriów.
Arnis Lapsa,
czego używasz teraz, gdy trzymasz się z dala od repozytoriów?
Chris
@Chris Skupiam się głównie na posiadaniu bogatego modelu domeny. ważna jest część wejściowa aplikacji. jeśli wszystkie zmiany stanu są dokładnie monitorowane, nie ma znaczenia, w jaki sposób odczytujesz dane, o ile są one wystarczająco wydajne. więc po prostu używam NHibernate ISession bezpośrednio. bez abstrakcji warstwy repozytorium o wiele łatwiej jest określić takie rzeczy, jak szybkie ładowanie, wiele zapytań itp., a jeśli naprawdę potrzebujesz, nie jest trudno również wyszydzić ISession.
Arnis Lapsa
3
@JesseWebb Naah ... Dzięki bogatej logice zapytań modelu domeny zostaje znacznie uproszczona. Np. Jeśli chcę wyszukać użytkowników, którzy coś kupili, po prostu wyszukuję użytkowników. Gdzie (u => u.HasPurchasedAnything) zamiast użytkowników. Dołącz (x => Zamówienia, coś, nie wiem linq). order => order.Status == 1) .Join (x => x.products). Where (x .... etc etc etc .... blah blah blah
Arnis Lapsa
5

Miej ogólne repozytorium, które jest opakowane przez określone repozytorium. W ten sposób możesz kontrolować interfejs publiczny, ale nadal korzystać z zalet ponownego wykorzystania kodu, który pochodzi z posiadania ogólnego repozytorium.

Piotr
źródło
3

public class UserRepository: Repository, IUserRepository

Nie powinieneś wstrzykiwać IUserRepository, aby uniknąć ujawnienia interfejsu. Jak powiedzieli ludzie, możesz nie potrzebować pełnego stosu CRUD itp.

Ste
źródło