Zaletą tworzenia ogólnego repozytorium a konkretnego repozytorium dla każdego obiektu?

Opracowujemy ASP.NET aplikacji MVC, a obecnie budują klasy repozytorium/usługi. Zastanawiam się, czy istnieją jakieś główne zalety tworzenia ogólnego interfejsu IRepository, który implementują wszystkie repozytoria, a każde repozytorium ma swój własny unikalny interfejs i zestaw metod.

Na przykład: ogólny interfejs IRepository może wyglądać tak (zaczerpnięty z ta odpowiedź):

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 zaimplementowałoby ten interfejs, dla przykład:

  • CustomerRepository:Irepository
  • ProductRepository:Irepository
  • itd.

Alternatywą, którą zastosowaliśmy we wcześniejszych 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 całkowicie zawarte w implementacji repozytorium, ktokolwiek implementuje usługę, musi tylko wiedzieć, którą funkcję repozytorium wywołać.

Chyba nie widzę zalet pisania wszystkich składnia wyrażenia w klasie service i przekazanie do repozytorium. Czy to nie oznacza, że w wielu przypadkach kod LINQ jest duplikowany?

Na przykład w naszym starym systemie fakturowania nazywamy

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

Z kilku różnych usług (Klient, Faktura, konto itp.). To wydaje się dużo czystsze niż pisanie następujących w wielu miejscach:

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

Jedyną wadą, jaką widzę przy użyciu konkretnego podejścia, jest to, że możemy skończyć z wieloma permutacjami Get * functions, ale nadal wydaje się to korzystne niż przeniesienie logiki wyrażeń do klas usług.

Co mi umyka?
Author: Community, 2009-08-05

5 answers

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 preferuję konkretne repozytoria, po ciężkiej pracy nad zbudowaniem generycznego frameworka repozytoriów. Bez względu na to, jaki sprytny mechanizm próbowałem, zawsze kończyłem na tym samym problemie: repozytorium jest częścią modelowanej domeny, a ta domena nie jest ogólna. Nie każdy byt można usunąć, nie każdy encja może być dodany, nie każdy encja ma repozytorium. Zapytania różnią się bardzo; API repozytorium staje się tak samo unikalne jak sam podmiot.

Wzorcem, którego często używam jest posiadanie specyficznych interfejsów repozytorium, ale klasy bazowej dla implementacji. Na przykład, używając LINQ do 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);
    }
}

Zwróć uwagę, że publiczne API repozytorium nie Zezwalaj na usuwanie użytkowników. Poza tym odsłanianie IQueryable to zupełnie inna puszka robaków - na ten temat jest tyle opinii co pępek.

 158
Author: Bryan Watts,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-23 15:00:51

Właściwie nie zgadzam się nieco z postem Bryana. Myślę, że ma rację, że ostatecznie wszystko jest bardzo unikalne i tak dalej. Ale w tym samym czasie, większość z tego wychodzi podczas projektowania, i odkrywam, że uzyskanie ogólnego repozytorium i korzystanie z niego podczas opracowywania mojego modelu, mogę uzyskać aplikację bardzo szybko, a następnie refaktor do większej specyficzności, ponieważ znajduję potrzebę tego.

Tak więc, w takich przypadkach, często tworzyłem generyczny IRepository, który ma pełny stos CRUD i który pozwala mi szybko przejść do grania z API i pozwalając ludziom grać w / UI i robić równolegle testy integracji i akceptacji użytkowników. Następnie, jak okaże się, że potrzebuję konkretnych zapytań na repo, itp, zaczynam zastępować tę zależność w / konkretny jeden w razie potrzeby i będzie stamtąd. Jeden z podstawowych impl. jest łatwy w tworzeniu i użyciu(i ewentualnie hook do bazy danych w pamięci lub obiektów statycznych lub wyśmiewanych obiektów lub czegokolwiek).

To, co ostatnio zacząłem robić, to zrywać z zachowanie. Tak więc, jeśli robisz 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 zajmują się niektórymi lub wszystkimi z nich, a ja nadal mogę wstrzykiwać does-it-all implementację do użycia podczas budowania aplikacji.

Paweł

 26
Author: Paul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-20 13:56:07

Preferuję konkretne repozytoria, które pochodzą z repozytorium generycznego (lub listy repozytoriów generycznych, aby określić dokładne zachowanie) z nadpisywalnymi podpisami metod.

 11
Author: Arnis Lapsa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-05 08:06:10

Mają ogólne repozytorium, które jest zawinięte przez konkretne repozytorium. W ten sposób możesz kontrolować publiczny interfejs, ale nadal masz tę zaletę, że ponowne użycie kodu pochodzi z ogólnego repozytorium.

 5
Author: Peter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-14 11:45:25

Public class UserRepository: Repository, Iuserrepository

Nie należy wstrzykiwać leku IUserRepository, aby uniknąć odsłonięcia interfejsu. Jak ludzie powiedzieli, możesz nie potrzebować pełnego stosu CRUD itp.

 1
Author: Ste,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-15 09:52:45