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?
źródło
Odpowiedzi:
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
DataContext
wybraną 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
IQueryable
to także cała puszka robaków - opinii na ten temat jest tyle, ile pępków.źródło
DataContext
, LINQ to SQL wydaje odpowiednie polecenie.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ł
źródło
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 .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.
źródło
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.
źródło
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.
źródło