Zmniejszanie repozytoriów do zagregowanych korzeni

83

Obecnie mam repozytorium dla prawie każdej tabeli w bazie danych i chciałbym dostosować się do DDD, redukując je tylko do zagregowanych korzeni.

Załóżmy, że mam następujące tabele Useri Phone. Każdy użytkownik może mieć jeden lub więcej telefonów. Bez pojęcia zbiorczego korzenia mógłbym zrobić coś takiego:

//assuming I have the userId in session for example and I want to update a phone number
List<Phone> phones = PhoneRepository.GetPhoneNumberByUserId(userId);
phones[0].Number = “911”;
PhoneRepository.Update(phones[0]);

Pojęcie korzeni kruszywa jest łatwiejsze do zrozumienia na papierze niż w praktyce. Nigdy nie będę mieć numerów telefonów, które nie należą do użytkownika, więc czy sensowne byłoby pozbycie się PhoneRepository i włączenie metod związanych z telefonem do UserRepository? Zakładając, że odpowiedź brzmi tak, mam zamiar przepisać poprzedni przykład kodu.

Czy mogę mieć metodę w UserRepository, która zwraca numery telefonów? Czy też powinien zawsze zwracać odniesienie do Użytkownika, a następnie przechodzić przez relację za pośrednictwem Użytkownika, aby uzyskać dostęp do numerów telefonów:

List<Phone> phones = UserRepository.GetPhoneNumbers(userId);
// Or
User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone

Niezależnie od tego, w jaki sposób kupię telefony, zakładając, że zmodyfikowałem jeden z nich, jak mam je zaktualizować? Moje ograniczone rozumienie jest takie, że obiekty znajdujące się pod korzeniem powinny być aktualizowane przez korzeń, co skierowałoby mnie do wyboru nr 1 poniżej. Chociaż będzie to działać doskonale z Entity Framework, wydaje się to wyjątkowo nieopisowe, ponieważ czytając kod, nie mam pojęcia, co właściwie aktualizuję, mimo że Entity Framework utrzymuje kartę zmienionych obiektów na wykresie.

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);

Wreszcie, zakładając Mam kilka tabel przeglądowych, które nie są bardzo przywiązane do niczego, takie jak CountryCodes, ColorsCodes, SomethingElseCodes. Mogę ich użyć do zapełnienia listy rozwijanej lub z jakiegokolwiek innego powodu. Czy są to samodzielne repozytoria? Czy można je połączyć w jakieś logiczne grupowanie / repozytorium, takie jak CodesRepository? Czy jest to sprzeczne z najlepszymi praktykami.

e36M3
źródło
2
Naprawdę bardzo dobre pytanie, że dużo ze sobą walczyłem. Wygląda na to, że jest to jeden z tych punktów kompromisowych, w których nie ma „właściwego” rozwiązania. Chociaż odpowiedzi dostępne w czasie, gdy to piszę, są dobre i obejmują większość problemów, nie wydaje mi się, aby
zawierały
Słyszę, że nie ma ograniczeń co do tego, jak blisko „właściwego” rozwiązania można się dostać. Myślę, że musimy zrobić wszystko, co w naszej mocy, dopóki nie nauczymy się lepszego sposobu :)
e36M3
+1 - Ja też z tym walczę. Wcześniej miałem oddzielną warstwę repozytorium i usługi dla każdej tabeli. Zacząłem łączyć je tam, gdzie miało to sens, ale skończyło się na warstwie repozytorium i usług z ponad 1 tys. Linii kodu. W moim najnowszym wycinku aplikacji utworzyłem kopię zapasową, aby umieścić tylko blisko powiązane koncepcje w tej samej warstwie repo / usług, nawet jeśli ten element jest zależny. np. - w przypadku bloga dodawałem komentarze do agregatu repozytorium postów, ale teraz wydzieliłem je na osobne repozytorium komentarzy / usługę.
jpshook

Odpowiedzi:

12

Możesz mieć dowolną metodę w swoim repozytorium :) W obu wymienionych przypadkach sensowne jest zwrócenie użytkownika z wypełnioną listą telefonów. Zwykle obiekt użytkownika nie byłby w pełni wypełniony wszystkimi podanymi informacjami (powiedzmy wszystkimi adresami, numerami telefonów) i możemy mieć różne metody wypełniania obiektu użytkownika różnymi rodzajami informacji. Nazywa się to leniwym ładowaniem.

User GetUserDetailsWithPhones()
{
    // Populate User along with Phones
}

W przypadku aktualizacji w tym przypadku aktualizowany jest użytkownik, a nie sam numer telefonu. Model pamięci masowej może przechowywać telefony w innej tabeli i w ten sposób możesz pomyśleć, że tylko telefony są aktualizowane, ale tak nie jest, jeśli myślisz z perspektywy DDD. Jeśli chodzi o czytelność, natomiast wiersz

UserRepository.Update(user)

sam nie przekazuje tego, co jest aktualizowane, powyższy kod wyjaśniłby, co jest aktualizowane. Najprawdopodobniej byłaby to część wywołania metody frontendu, która może oznaczać aktualizowane.

W przypadku tabel odnośników, a właściwie nawet w innych przypadkach, warto mieć GenericRepository i używać tego. Repozytorium niestandardowe może dziedziczyć z GenericRepository.

public class UserRepository : GenericRepository<User>
{
    IEnumerable<User> GetUserByCustomCriteria()
    {
    }

    User GetUserDetailsWithPhones()
    {
        // Populate User along with Phones
    }

    User GetUserDetailsWithAllSubInfo()
    {
        // Populate User along with all sub information e.g. phones, addresses etc.
    }
}

Wyszukaj Generic Repository Entity Framework i dobrze byłoby wiele ładnych implementacji. Użyj jednego z nich lub napisz własny.

amit_g
źródło
@amit_g, dzięki za informację. Korzystam już z repozytorium generycznego / podstawowego, z którego dziedziczą wszystkie inne. Mój pomysł na logiczne grupowanie tabel „przeglądowych” w jednym repozytorium polegał po prostu na zaoszczędzeniu czasu i zmniejszeniu liczby repozytoriów. Więc zamiast tworzyć ColorCodeRepository i AnotherCodeRepository, po prostu utworzyłbym CodesRepository.GetColorCodes () i CodesRepository.GetAnotherCodes (). Ale nie jestem pewien, czy logiczne grupowanie niepowiązanych jednostek w jednym repozytorium jest złą praktyką.
e36M3
Następnie potwierdzasz również, że zgodnie z regułami DDD metody w repozytorium odpowiadające elementowi głównemu powinny zwracać element główny, a nie jednostki bazowe na wykresie. Więc w moim przykładzie każda metoda w UserRepository może zwracać tylko typ użytkownika, niezależnie od tego, jak wygląda reszta wykresu (lub część wykresu, która naprawdę mnie interesuje, taka jak adresy lub telefony)?
e36M3
CodesRepository jest w porządku, ale trudno byłoby konsekwentnie utrzymać to, co do niego należy. To samo można osiągnąć po prostu przez GenericRepository <ColorCodes> GetAll (). Ponieważ GenericRepository miałby tylko bardzo ogólne metody (GetAll, GetByID itp.), Działałoby dobrze w przypadku tabel odnośników.
amit_g
1
@ e36M3, tak. Na przykład geekswithblogs.net/seanfao/archive/2009/12/03/136680.aspx
amit_g
2
Niestety ta odpowiedź jest błędna. Repozytorium powinno być traktowane jako zbiór obiektów w pamięci i należy unikać leniwego ładowania. Oto fajny artykuł o tym besnikgeek.blogspot.com/2010/07/…
Rafał Łużyński 10.10.14
9

Twój przykład w repozytorium Aggregate Root jest w porządku, tj. Każdy podmiot, który nie może istnieć bez zależności od innego, nie powinien mieć własnego repozytorium (w twoim przypadku Phone). Bez tego rozważania możesz szybko znaleźć się w eksplozji repozytoriów w mapowaniu 1-1 do tabel db.

Powinieneś przyjrzeć się używaniu wzorca jednostki pracy do zmian danych, a nie samych repozytoriów, ponieważ myślę, że powodują one pewne zamieszanie co do zamiaru, jeśli chodzi o utrwalanie zmian z powrotem do bazy danych. W rozwiązaniu EF jednostka pracy jest zasadniczo otoką interfejsu wokół kontekstu EF.

W odniesieniu do Twojego repozytorium danych wyszukiwania, po prostu tworzymy ReferenceDataRepository, które staje się odpowiedzialne za dane, które nie należą konkretnie do jednostki domeny (kraje, kolory itp.).

Darren Lewis
źródło
1
Dziękuję Ci. Nie jestem pewien, w jaki sposób Jednostka Pracy zastępuje repozytorium? Używam już UOW w tym sensie, że na końcu każdej transakcji biznesowej (koniec żądania HTTP) pojawi się pojedyncze wywołanie SaveChanges () do kontekstu Entity Framework. Jednak nadal przechodzę przez repozytoria (zawierające kontekst EF) w celu uzyskania dostępu do danych. Na przykład UserRepository.Delete (użytkownik) i UserRepository.Add (użytkownik).
e36M3
5

Jeśli telefon nie ma sensu bez użytkownika, jest to byt (jeśli zależy Ci na jego tożsamości) lub obiekt wartościowy i zawsze powinien być modyfikowany przez użytkownika i wspólnie pobierany / aktualizowany.

Pomyśl o źródłach zagregowanych jako o definicjach kontekstu - rysują one lokalne konteksty, ale same są w kontekście globalnym (Twoja aplikacja).

Jeśli stosujesz projekt oparty na domenie, repozytoria powinny być 1: 1 na zbiorcze korzenie.
Bez wymówek.

Założę się, że są to problemy, z którymi się borykasz:

  • trudności techniczne - niedopasowanie impedancji relacji z obiektem. Z łatwością zmagasz się z utrwalaniem całych wykresów obiektów, a struktura encji nie pomaga.
  • model domeny jest skoncentrowany na danych (w przeciwieństwie do zorientowanego na zachowanie). z tego powodu - tracisz wiedzę o hierarchii obiektów (wcześniej wspomniane konteksty) i magicznie wszystko staje się zbiorczym korzeniem.

Nie jestem pewien, jak rozwiązać pierwszy problem, ale zauważyłem, że naprawienie drugiego rozwiązuje wystarczająco dobrze. Aby zrozumieć, co mam na myśli mówiąc o zorientowaniu na zachowanie, wypróbuj ten artykuł .

Ps Redukcja repozytorium do zagregowanego katalogu głównego nie ma sensu.
Unikaj PPS "CodeRepositories". Prowadzi to do skoncentrowania się na danych -> kodzie proceduralnym.
Ppps Unikaj wzorca pracy. Korzenie agregatów powinny określać granice transakcji.

Arnis Lapsa
źródło
1
Ponieważ odsyłacz do artykułu nie jest już aktywny, użyj tego: web.archive.org/web/20141021055503/http://www.objectmentor.com/…
JwJosefy
3

To stare pytanie, ale warto zamieścić proste rozwiązanie.

  1. EF Context zapewnia już zarówno jednostkę pracy (śledzi zmiany), jak i repozytoria (odwołanie w pamięci do rzeczy z bazy danych). Dalsza abstrakcja nie jest obowiązkowa.
  2. Usuń DBSet z klasy kontekstu, ponieważ Phone nie jest zbiorczym katalogiem głównym.
  3. Zamiast tego użyj właściwości nawigacji „Telefony” na karcie Użytkownik.

static void updateNumber (int userId, string oldNumber, string newNumber)

static void updateNumber(int userId, string oldNumber, string newNumber)
    {
        using (MyContext uow = new MyContext()) // Unit of Work
        {
            DbSet<User> repo = uow.Users; // Repository
            User user = repo.Find(userId); 
            Phone oldPhone = user.Phones.Where(x => x.Number.Trim() == oldNumber).SingleOrDefault();
            oldPhone.Number = newNumber;
            uow.SaveChanges();
        }

    }
Kredowy
źródło
Abstrakcja nie jest obowiązkowa, ale jest zalecana. Entity Framework jest nadal tylko dostawcą i częścią infrastruktury. Nie chodzi nawet tylko o to, co miałoby się stać, gdyby zmienił się dostawca, ale w większych systemach może istnieć wiele typów dostawców, którzy utrzymują różne koncepcje domeny na różnych nośnikach trwałości. Jest to rodzaj abstrakcji, która jest niezwykle łatwa do wykonania na wczesnym etapie, ale jej refaktoryzacja jest bolesna przez wystarczająco długi czas i złożoność.
Joseph Ferris,
1
Uważam, że bardzo trudno jest zachować zalety ORM EF (np. Leniwe ładowanie, queryables), gdy próbuję abstrahować od interfejsu repozytorium.
Chalky
To z pewnością interesująca dyskusja. Ponieważ ładowanie z opóźnieniem jest bardzo specyficzne dla implementacji, uważam, że jego wartość jest ograniczona do infrastruktury (wejście i wyjście obiektu domeny z translacją ograniczoną warstwą). Wiele implementacji, które widziałem, napotykało problemy podczas próby generycznej abstrakcji. Zwykle wybieram jawną implementację, ponieważ metody ogólne mają bardzo małą wartość domeny. EF sprawia, że ​​zapytania zapytania są wysoce użyteczne, ale problem staje się rolą repozytorium - tj. Repozytoria używane przez kontrolery tracą korzyści z abstrakcji.
Joseph Ferris
0

Jeśli jednostka Phone ma sens tylko w połączeniu z zagregowanym użytkownikiem root, to uważam również, że ma sens, aby za operację dodania nowego rekordu Phone odpowiedzialny był obiekt domeny użytkownika za pomocą określonej metody (zachowanie DDD), a to mogłoby ma sens z kilku powodów, natychmiastowym powodem jest to, że powinniśmy sprawdzić, czy obiekt użytkownika istnieje, ponieważ jednostka Phone zależy od jego istnienia i być może utrzymywać na nim blokadę transakcji, wykonując więcej sprawdzeń poprawności, aby upewnić się, że żaden inny proces nie usunął wcześniej agregatu głównego zakończyliśmy walidację operacji. W innych przypadkach z innymi rodzajami agregatów głównych możesz chcieć zagregować lub obliczyć jakąś wartość i zachować ją we właściwościach kolumny agregatu głównego, aby później wydajniej przetwarzać je przez inne operacje.

Ponadto, jeśli chcesz użyć metod, które pobierają wszystkie telefony niezależnie od użytkowników, którzy są ich właścicielami, nadal możesz to zrobić za pośrednictwem repozytorium użytkowników, potrzebujesz tylko jednej metody zwracającej wszystkich użytkowników jako IQueryable, a następnie możesz zmapować je, aby uzyskać wszystkie telefony użytkowników i wykonać udoskonaloną zapytaj z tym. Więc w tym przypadku nie potrzebujesz nawet PhoneRepository. Poza tym wolałbym raczej użyć klasy z metodą rozszerzeń dla IQueryable, której mogę używać w dowolnym miejscu, nie tylko z klasy Repository, jeśli chciałbym abstrakcyjne zapytania za metodami.

Tylko jedno zastrzeżenie dotyczące możliwości usuwania jednostek Phone za pomocą tylko obiektu domeny, a nie repozytorium Phone, musisz się upewnić, że identyfikator użytkownika jest częścią klucza podstawowego Phone lub innymi słowy klucz podstawowy rekordu Phone jest kluczem złożonym składa się z UserId i innej właściwości (sugeruję tożsamość generowaną automatycznie) w encji Phone. Ma to sens intuicyjnie, ponieważ rekord Phone jest „własnością” rekordu użytkownika, a jego usunięcie z kolekcji nawigacji użytkownika byłoby równoznaczne z całkowitym usunięciem z bazy danych.

Kaveh Hadjari
źródło