Jak poprawnie używać wzorca repozytorium?

86

Zastanawiam się, jak należy grupować repozytoria? Podobnie jak w przykładach, które widziałem w mvc asp.net, aw moich książkach zasadniczo używają jednego repozytorium na tabelę bazy danych. Ale wydaje się, że wiele repozytoriów prowadzi do tego, że później będziesz musiał wywoływać wiele repozytoriów w celu kpiny i innych rzeczy.

Więc myślę, że powinienem je pogrupować. Jednak nie jestem pewien, jak je pogrupować.

W tej chwili utworzyłem repozytorium rejestracji, aby obsłużyć wszystkie moje sprawy rejestracyjne. Jednak są jakieś 4 tabele, które muszę zaktualizować, a zanim miałem 3 repozytoria, aby to zrobić.

Na przykład jedna z tabel jest tabelą licencji. Kiedy się rejestrują, patrzę na ich klucz i sprawdzam, czy istnieje w bazie danych. Co się teraz stanie, jeśli będę musiał sprawdzić ten klucz licencyjny lub coś innego z tej tabeli w innym miejscu niż rejestracja?

Jedno miejsce może być zalogowane (sprawdź, czy klucz nie wygasł).

Więc co bym zrobił w tej sytuacji? Przepisz kod ponownie (przerwij DRY)? Spróbuj po prostu połączyć te 2 repozytoria razem i miej nadzieję, że żadna z metod nie będzie potrzebna w innym momencie (na przykład może mam metodę, która sprawdza, czy używana jest nazwa_użytkownika - może będę potrzebować tego gdzie indziej).

Również jeśli połączę je ze sobą, potrzebowałbym albo 2 warstw usług, które przechodzą do tego samego repozytorium, ponieważ myślę, że posiadanie całej logiki dla 2 różnych części witryny byłoby długie i musiałbym mieć nazwy takie jak ValidateLogin (), ValdiateRegistrationForm () , ValdiateLoginRetrievePassword () itd.

Albo zadzwoń do Repozytorium i po prostu masz dziwnie brzmiącą nazwę?

Po prostu wydaje się trudne stworzenie repozytorium, które ma wystarczająco ogólną nazwę, aby można było go używać w wielu miejscach aplikacji i nadal ma sens, a nie sądzę, aby wywoływanie innego repozytorium w repozytorium było dobrą praktyką?

chobo2
źródło
11
+1. Świetne pytanie.
griegs
Dzięki temu już od jakiegoś czasu mnie wkurza. Ponieważ uważam, że te, które widziałem w książce, są zbyt proste, nie pokazują ci, co robić w takich sytuacjach.
chobo2
Zasadniczo robię to samo co ty i tak, jeśli mam klasę linq2sql, która jest używana w więcej niż 1 repozytorium i muszę zmienić strukturę tabeli, zrywam DRY. Mniej niż ideał. Teraz planuję to trochę lepiej, więc nie muszę używać klasy linq2sql więcej niż raz, co wydaje mi się, że jest dobrym rozwiązaniem, ale przewiduję dzień, w którym będzie to dla mnie prawdziwy problem.
griegs
Zadałem podobne pytanie (ale nie identyczne) tutaj: stackoverflow.com/questions/910156/…
Grokys
Tak, ale trudno zaplanować to na każdą sytuację. Jak powiedziałem, mogę połączyć logowanie i rejestrację w uwierzytelnianie i mieć 2 oddzielne warstwy. To prawdopodobnie rozwiąże mój problem. Ale co się stanie, jeśli na przykład na stronie profilu mojej witryny z jakiegoś powodu chcę im pokazać ich klucz (może pozwolę htem to zmienić lub coś takiego). Co teraz mam zrobić zerwanie DRY i napisać to samo? Lub spróbuj stworzyć repozytorium, które w jakiś sposób będzie pasowało do wszystkich 3 z tych tabel z dobrą nazwą.
chobo2

Odpowiedzi:

41

Jedna rzecz, którą zrobiłem źle, bawiłem się wzorcem repozytorium - tak jak ty, myślałem, że ta tabela odnosi się do repozytorium 1: 1. Kiedy zastosujemy pewne reguły z Domain Driven Design - problem grupowania repozytoriów często znika.

Repozytorium powinno znajdować się w zbiorczym katalogu głównym, a nie w tabeli. Oznacza to - jeśli podmiot nie powinien żyć sam (tj. - jeśli masz element, Registrantktóry w szczególności uczestniczy Registration) - to tylko podmiot, nie potrzebuje repozytorium, powinien zostać zaktualizowany / utworzony / odzyskany przez repozytorium zagregowanego roota należy.

Oczywiście - w wielu przypadkach ta technika zmniejszania liczby repozytoriów (właściwie - jest to bardziej technika strukturyzowania modelu domeny) nie może być zastosowana, ponieważ każda jednostka ma być zbiorczym korzeniem (to w dużym stopniu zależy od Twojej domeny, Mogę podać tylko ślepe domysły). W twoim przykładzie - Licensewydaje się być zbiorczym korzeniem, ponieważ musisz mieć możliwość sprawdzenia ich bez kontekstu Registrationencji.

Ale to nie ogranicza nas do repozytoriów kaskadowych ( Registrationrepozytorium może odwoływać się do Licenserepozytorium w razie potrzeby). To nie ogranicza nas do odwoływania się do Licenserepozytorium (najlepiej - przez IoC) bezpośrednio z Registrationobiektu.

Po prostu staraj się nie popychać swojego projektu przez komplikacje wynikające z technologii lub niezrozumienie czegoś. Grupowanie repozytoriów ServiceXtylko dlatego, że nie chcesz tworzyć dwóch repozytoriów, nie jest dobrym pomysłem.

Znacznie lepiej byłoby nadać mu odpowiednią nazwę - RegistrationServicetj

Generalnie jednak należy unikać usług - często są one przyczyną, która prowadzi do anemicznego modelu domeny .

EDYCJA:
Zacznij używać IoC. Naprawdę łagodzi ból wstrzykiwania uzależnień.
Zamiast pisać:

var registrationService = new RegistrationService(new RegistrationRepository(),  
      new LicenseRepository(), new GodOnlyKnowsWhatElseThatServiceNeeds());

będziesz potrafił napisać:

var registrationService = IoC.Resolve<IRegistrationService>();

Ps Lepiej byłoby użyć tak zwanego wspólnego lokalizatora usług, ale to tylko przykład.

Arnis Lapsa
źródło
17
Hahaha ... radzę skorzystać z lokalizatora usług. Zawsze cieszy mnie widok, jak głupi byłem w przeszłości.
Arnis Lapsa
1
@Arnis Wygląda na to, że zmieniły się Twoje opinie - jestem zainteresowany, jak byś teraz inaczej odpowiedział na to pytanie?
ngm,
10
@ngm bardzo się zmieniło, odkąd to odpowiedziałem. Nadal zgadzam się, że zagregowany katalog główny powinien wyznaczać granice transakcji (być zapisywany jako całość), ale jestem znacznie mniej optymistyczny, jeśli chodzi o abstrakcyjną trwałość przy użyciu wzorca repozytorium. Ostatnio - po prostu używam ORM bezpośrednio, ponieważ takie rzeczy jak zarządzanie żądnym / leniwym ładowaniem są zbyt niewygodne. O wiele bardziej korzystne jest skupienie się na opracowywaniu bogatego modelu domeny zamiast skupiania się na abstrakcyjnej trwałości.
Arnis Lapsa
2
@Developer nie, niezupełnie. nadal powinni być wytrwałymi ignorantami. pobierasz zagregowany root z zewnątrz i wywołujesz na nim metodę, która wykonuje zadanie. mój model domeny ma zerowe odwołania, tylko standardowe .NET Framework. aby to osiągnąć, musisz mieć bogaty model domeny i narzędzia, które są wystarczająco inteligentne (NHibernate załatwia sprawę).
Arnis Lapsa
1
Przeciwnie. Pobieranie więcej niż jednego ggregatu często oznacza, że ​​możesz użyć PK lub indeksów. Znacznie szybsze niż wykorzystanie relacji, które generuje EF. Im mniejsze agregaty korzeni, tym łatwiej uzyskać na nich wydajność.
jgauffin
5

Jedną z rzeczy, które zacząłem robić, aby rozwiązać ten problem, jest faktyczne rozwijanie usług obejmujących repozytoria N. Miejmy nadzieję, że frameworki DI lub IoC mogą to ułatwić.

public class ServiceImpl {
    public ServiceImpl(IRepo1 repo1, IRepo2 repo2...) { }
}

Czy to ma sens? Rozumiem też, że mówienie o usługach w tym dworku może, ale nie musi, być zgodne z zasadami DDD, po prostu robię to, ponieważ wydaje się działać.

neouser99
źródło
1
Nie, nie ma to większego sensu. Nie używam obecnie frameworków DI ani IoC, ponieważ mam ich wystarczająco dużo.
chobo2
1
Jeśli możesz utworzyć instancję repozytoriów w / tylko new (), możesz wypróbować to ... public ServiceImpl (): this (new Repo1, new Repo2 ...) {} jako dodatkowy konstruktor w usłudze.
neouser99
Zastrzyk zależności? Robię to już, ale nadal nie jestem pewien, jaki jest twój kod i co rozwiązuje.
chobo2
W szczególności zamierzam połączyć repozytoria. Który kod nie ma sensu? Jeśli używasz DI, to kod w odpowiedzi będzie działał w tym sensie, że twoja struktura DI wstrzyknie te usługi IRepo, w kodzie komentarza jest to po prostu mała obejście do zrobienia DI (w zasadzie twój konstruktor bez parametrów 'wstrzykuje' te zależności w ServiceImpl).
neouser99
Używam już DI, więc mogę lepiej przetestować jednostkę. Mój problem polega na tym, że jeśli utworzysz warstwy usług i repozytoria z szczegółowymi nazwami. Wtedy, jeśli kiedykolwiek będziesz potrzebować ich używać w innym miejscu, będzie wyglądać dziwnie, wywołując jak warstwa RegistrationService, która wywołuje RegRepo w innej klasie, na przykład ProfileClass. Więc nie widzę z twojego przykładu tego, co w pełni robisz. Na przykład, jeśli zaczniesz mieć zbyt wiele repozytoriów w tej samej warstwie usług, będziesz mieć o wiele inną logikę biznesową i logikę walidacji. Ponieważ w warstwie usług zwykle umieszcza się logikę walidacji. Tak wielu potrzebuję więcej ...
chobo2
2

To, co robię, to abstrakcyjna klasa bazowa zdefiniowana w następujący sposób:

public abstract class ReadOnlyRepository<T,V>
{
     V Find(T lookupKey);
}

public abstract class InsertRepository<T>
{
     void Add(T entityToSave);
}

public abstract class UpdateRepository<T,V>
{
     V Update(T entityToUpdate);
}

public abstract class DeleteRepository<T>
{
     void Delete(T entityToDelete);
}

Następnie możesz wyprowadzić repozytorium z abstrakcyjnej klasy bazowej i rozszerzyć swoje pojedyncze repozytorium, o ile na przykład argumenty generyczne różnią się;

public class RegistrationRepository: ReadOnlyRepository<int, IRegistrationItem>,
                                     ReadOnlyRepository<string, IRegistrationItem> 

itp....

Potrzebuję oddzielnych repozytoriów, ponieważ mamy ograniczenia dotyczące niektórych naszych repozytoriów, co zapewnia nam maksymalną elastyczność. Mam nadzieję że to pomoże.

Michael Mann
źródło
Więc próbujesz stworzyć ogólne repozytorium, które poradzi sobie z tym wszystkim?
chobo2
Więc co tak naprawdę się dzieje, powiedzmy o metodzie Update. Tak jak masz taką aktualizację V i przechodzi w T entitytoUpdate, ale nie ma kodu faktycznie aktualizującego ją, czy istnieje?
chobo2
Tak ... W metodzie update będzie kod, ponieważ napiszesz klasę, która pochodzi z repozytorium ogólnego. Kod implementacji może być Linq2SQL lub ADO.NET lub jakikolwiek inny, który wybrałeś jako technologię implementacji dostępu do danych
Michael Mann
2

Mam to jako moją klasę repozytorium i tak, rozszerzam w repozytorium tabeli / obszaru, ale nadal czasami muszę zepsuć DRY.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MvcRepository
{
    public class Repository<T> : IRepository<T> where T : class
    {
        protected System.Data.Linq.DataContext _dataContextFactory;

        public IQueryable<T> All()
        {
            return GetTable.AsQueryable();
        }

        public IQueryable<T> FindAll(Func<T, bool> exp)
        {
            return GetTable.Where<T>(exp).AsQueryable();
        }

        public T Single(Func<T, bool> exp)
        {
            return GetTable.Single(exp);
        }

        public virtual void MarkForDeletion(T entity)
        {
            _dataContextFactory.GetTable<T>().DeleteOnSubmit(entity);
        }

        public virtual T CreateInstance()
        {
            T entity = Activator.CreateInstance<T>();
            GetTable.InsertOnSubmit(entity);
            return entity;
        }

        public void SaveAll()
        {
            _dataContextFactory.SubmitChanges();
        }

        public Repository(System.Data.Linq.DataContext dataContextFactory)
        {
            _dataContextFactory = dataContextFactory;
        }

        public System.Data.Linq.Table<T> GetTable
        {
            get { return _dataContextFactory.GetTable<T>(); }
        }

    }
}

EDYTOWAĆ

public class AdminRepository<T> : Repository<T> where T: class
{
    static AdminDataContext dc = new AdminDataContext(System.Configuration.ConfigurationManager.ConnectionStrings["MY_ConnectionString"].ConnectionString);

    public AdminRepository()
        : base( dc )
    {
    }

Mam też kontekst danych, który został utworzony przy użyciu klasy Linq2SQL.dbml.

Więc teraz mam standardowe repozytorium implementujące standardowe wywołania, takie jak All i Find, aw moim AdminRepository mam określone wywołania.

Nie odpowiada na pytanie DRY, chociaż nie sądzę.

griegs
źródło
Do czego służy „Repozytorium”? i lubisz CreateInstance? Nie jestem pewien, co robisz.
chobo2
To jest ogólne jak wszystko. Zasadniczo musisz mieć określone repozytorium dla swojego (obszaru). Sprawdź powyższą edycję dla mojego AdminRepository.
griegs
1

Oto przykład ogólnej implementacji repozytorium przy użyciu FluentNHibernate. Jest w stanie utrwalić każdą klasę, dla której napisałeś mapper. Jest nawet w stanie wygenerować bazę danych na podstawie klas mappera.

James Jones
źródło
1

Wzorzec repozytorium to zły wzorzec projektowy. Pracuję z wieloma starymi projektami .Net i ten wzorzec zwykle powoduje błędy „Transakcje rozproszone”, „Częściowe wycofanie zmian” i „Wyczerpana pula połączeń”, których można było uniknąć. Problem polega na tym, że wzorzec próbuje wewnętrznie obsługiwać połączenia i transakcje, ale te powinny być obsługiwane w warstwie kontrolera. Również EntityFramework już abstrakcjonuje wiele logiki. Sugerowałbym zamiast tego użycie wzorca usługi, aby ponownie użyć udostępnionego kodu.

ColacX
źródło
0

Proponuję przyjrzeć się Sharp Architecture . Sugerują użycie jednego repozytorium na jednostkę. Obecnie używam go w swoim projekcie i jestem bardzo zadowolony z efektów.

Chytry
źródło
Dałbym tutaj +1, ale wskazał, że pojemniki DI lub IoC nie są do końca opcją (pod warunkiem, że nie są to jedyne zalety Sharp Arch). Domyślam się, że istnieje sporo istniejącego kodu, nad którym pracuje.
neouser99
Co jest uważane za byt? Czy to cała baza danych? czy to jest tabela bazy danych? Kontenery DI lub IoC nie są obecnie dostępne, ponieważ po prostu nie chcę uczyć się tego na szczycie pozostałych 10 rzeczy, których uczę się w tym samym czasie. są czymś, czym zajrzę się przy następnej wersji mojej witryny lub w następnym projekcie. Chociaż nie jestem pewien, czy będzie to ten, szybki rzut oka na stronę i wygląda na to, że chcesz, abyś używał nhirbrate, a ja używam obecnie linq to sql.
chobo2