DDD - reguła, że ​​encje nie mają bezpośredniego dostępu do repozytoriów

184

W Domain Driven Design, wydaje się, że wiele z umową , że podmioty nie powinny dostęp Repozytoria bezpośrednio.

Czy to pochodzi z książki Erica Evansa Domain Driven Design , czy pochodzi z innych źródeł?

Gdzie jest kilka dobrych wyjaśnień uzasadnienia?

edytuj: Aby wyjaśnić: nie mówię o klasycznej praktyce OO polegającej na oddzielaniu dostępu do danych na osobną warstwę od logiki biznesowej - mówię o konkretnym rozwiązaniu, zgodnie z którym w DDD jednostki nie powinny rozmawiać z danymi warstwa dostępu w ogóle (tzn. nie powinny one zawierać odniesień do obiektów repozytorium)

aktualizacja: Dałem nagrodę BacceSR, ponieważ jego odpowiedź wydawała się najbliższa, ale wciąż jestem całkiem nieźle w tej sprawie. Jeśli jest to tak ważna zasada, to na pewno powinny gdzieś być jakieś dobre artykuły online.

aktualizacja: marzec 2013 r., głosowanie w sprawie pytania sugeruje, że jest to bardzo interesujące i mimo wielu odpowiedzi, nadal uważam, że jest miejsce na więcej, jeśli ludzie mają pomysły na ten temat.

podobny do kodu
źródło
Spójrz na moje pytanie stackoverflow.com/q/8269784/235715 , pokazuje sytuację, w której trudno jest uchwycić logikę, bez Entity dostępu do repozytorium. Chociaż myślę, że podmioty nie powinny mieć dostępu do repozytoriów, i istnieje rozwiązanie mojej sytuacji, w której kod można przepisać bez odwołania do repozytorium, ale obecnie nie mogę o tym myśleć.
Alex Burtsev
Nie wiem skąd się wziął. Moje myśli: Myślę, że to nieporozumienie pochodzi od ludzi, którzy nie rozumieją, o co chodzi w DDD. Podejście to nie służy do wdrażania oprogramowania, ale do jego projektowania (projektowanie domeny.). W tamtych czasach mieliśmy architektów i wdrażających, ale teraz są tylko programiści. DDD jest przeznaczony dla architektów. A gdy architekt projektuje oprogramowanie, potrzebuje jakiegoś narzędzia lub wzorca do reprezentowania pamięci lub bazy danych dla deweloperów, którzy wdrożą przygotowany projekt. Ale sam projekt (z perspektywy biznesowej) nie ma lub nie potrzebuje repozytorium.
berhalak

Odpowiedzi:

47

Jest tu trochę zamieszania. Repozytoria mają dostęp do zagregowanych katalogów głównych. Zagregowane korzenie to byty. Powodem tego jest rozdzielenie obaw i dobre nakładanie warstw. Nie ma to sensu w przypadku małych projektów, ale jeśli pracujesz w dużym zespole, chcesz powiedzieć: „Uzyskujesz dostęp do produktu za pośrednictwem repozytorium produktów. Produkt jest zagregowanym katalogiem głównym dla zbioru podmiotów, w tym obiektu ProductCatalog. Jeśli chcesz zaktualizować ProductCatalog, musisz przejść przez ProductRepository. ”

W ten sposób masz bardzo, bardzo wyraźny podział na logikę biznesową i miejsca aktualizacji. Nie masz dzieciaka, który jest sam i pisze cały ten program, który robi te wszystkie skomplikowane rzeczy w katalogu produktów, a jeśli chodzi o zintegrowanie go z projektem wyższego szczebla, siedzisz tam, patrząc na to i zdając sobie z tego sprawę wszystko musi zostać porzucone. Oznacza to również, że kiedy ludzie dołączają do zespołu, dodają nowe funkcje, wiedzą, gdzie iść i jak zorganizować program.

Ale poczekaj! Repozytorium odnosi się również do warstwy trwałości, tak jak we wzorcu repozytorium. W lepszym świecie Repozytorium Erica Evansa i wzorzec repozytorium miałyby osobne nazwy, ponieważ często się nakładają. Aby uzyskać wzorzec repozytorium, masz kontrast z innymi sposobami uzyskiwania dostępu do danych, za pomocą magistrali usług lub systemu modeli zdarzeń. Zwykle po przejściu na ten poziom definicja Repozytorium Erica Evansa jest na marginesie i zaczynasz mówić o ograniczonym kontekście. Każdy ograniczony kontekst jest zasadniczo własną aplikacją. Być może masz wyrafinowany system zatwierdzania umożliwiający umieszczanie rzeczy w katalogu produktów. W oryginalnym projekcie produkt był centralnym elementem, ale w tym ograniczonym kontekście katalog produktów jest. Nadal możesz uzyskiwać dostęp do informacji o produkcie i aktualizować produkt za pośrednictwem magistrali usług,

Powrót do pierwotnego pytania. Jeśli uzyskujesz dostęp do repozytorium z poziomu encji, oznacza to, że encja nie jest tak naprawdę jednostką biznesową, ale prawdopodobnie czymś, co powinno istnieć w warstwie usług. Wynika to z faktu, że podmioty są obiektami biznesowymi i powinny zajmować się jak najbardziej zbliżonym do DSL (językiem specyficznym dla domeny). W tej warstwie znajdują się tylko informacje biznesowe. Jeśli rozwiążesz problem z wydajnością, będziesz musiał szukać gdzie indziej, ponieważ powinny tu znajdować się tylko informacje biznesowe. Jeśli nagle pojawią się problemy z aplikacją, bardzo utrudniasz rozszerzenie i utrzymanie aplikacji, która tak naprawdę jest sercem DDD: tworzenie oprogramowania, które można utrzymać.

Odpowiedź na komentarz 1 : Racja, dobre pytanie. Dlatego nie cała weryfikacja zachodzi w warstwie domeny. Sharp ma atrybut „DomainSignature”, który robi to, co chcesz. Jest świadomy trwałości, ale bycie atrybutem utrzymuje warstwę domeny w czystości. Zapewnia to, że nie masz zduplikowanego bytu, w twoim przykładzie o tej samej nazwie.

Porozmawiajmy jednak o bardziej skomplikowanych zasadach sprawdzania poprawności. Powiedzmy, że jesteś Amazon.com. Czy kiedykolwiek zamówiłeś coś z wygasłą kartą kredytową? Mam, gdzie nie zaktualizowałem karty i coś kupiłem. Przyjmuje zamówienie, a interfejs informuje mnie, że wszystko jest brzoskwiniowe. Około 15 minut później otrzymam wiadomość e-mail z informacją, że wystąpił problem z moim zamówieniem, moja karta kredytowa jest nieważna. To, co się tutaj dzieje, polega na tym, że w warstwie domeny istnieje pewna walidacja wyrażeń regularnych. Czy to prawidłowy numer karty kredytowej? Jeśli tak, zachowaj kolejność. Istnieje jednak dodatkowa weryfikacja w warstwie zadań aplikacji, w której sprawdzana jest usługa zewnętrzna, aby sprawdzić, czy można dokonać płatności za pomocą karty kredytowej. Jeśli nie, nie wysyłaj niczego, zawieś zamówienie i poczekaj na klienta.

Nie bój się tworzyć obiektów sprawdzania poprawności w warstwie usług, które mogą uzyskiwać dostęp do repozytoriów. Po prostu trzymaj go poza warstwą domeny.

rogowacenie
źródło
15
Dzięki. Ale powinienem dążyć do jak największej logiki biznesowej dla podmiotów (i powiązanych z nimi fabryk, specyfikacji itd.), Prawda? Ale jeśli żadnemu z nich nie wolno pobierać danych przez repozytoria, to jak mam napisać jakąkolwiek (dość skomplikowaną) logikę biznesową? Na przykład: Użytkownik pokoju rozmów nie może zmienić swojej nazwy na nazwę, która była już używana przez kogoś innego. Chciałbym, aby reguła ta była wbudowana w encję ChatUser, ale nie jest to łatwe, jeśli nie można stamtąd trafić do repozytorium. Więc co powinienem zrobić?
codeulike
Moja odpowiedź była większa niż pozwala na to pole komentarza, patrz edycja.
kertosis
6
Twoja istota powinna wiedzieć, jak się chronić przed krzywdą. Obejmuje to upewnienie się, że nie może dojść do nieprawidłowego stanu. To, co opisujesz u użytkownika pokoju rozmów, to logika biznesowa DODATKOWA do logiki, którą jednostka musi utrzymywać w mocy. Logika biznesowa, taka jak to, czego naprawdę pragniesz, to usługa Chatroom, a nie jednostka ChatUser.
Alec
9
Dzięki Alec. To jasny sposób na wyrażenie tego. Wydaje mi się jednak, że złota zasada Evansa „cała logika biznesowa powinna iść w warstwę domeny” jest sprzeczna z zasadą „podmioty nie powinny uzyskiwać dostępu do repozytoriów”. Mogę z tym żyć, jeśli rozumiem, dlaczego tak jest, ale nie mogę znaleźć dobrego wyjaśnienia w Internecie, dlaczego podmioty nie powinny uzyskiwać dostępu do repozytoriów. Evans nie wspomina o tym wprost. Skąd to się wzieło? Jeśli możesz napisać odpowiedź wskazującą na dobrą literaturę, być może uda ci się zdobyć 50-procentową nagrodę:)
codeulike
4
„jego nie ma sensu na małych” To jest duży błąd, który popełniają zespoły… to mały projekt, jako taki mogę to zrobić i to… przestać tak myśleć. Wiele małych projektów, z którymi współpracujemy, stają się duże ze względu na wymagania biznesowe. Jeśli zrobisz coś usychającego małym lub dużym, zrób to dobrze.
MeTitus,
35

Na początku przekonałem się, aby umożliwić niektórym z moich podmiotów dostęp do repozytoriów (tj. Leniwe ładowanie bez ORM). Później doszedłem do wniosku, że nie powinienem i że mogę znaleźć alternatywne sposoby:

  1. Powinniśmy znać nasze intencje w żądaniu i czego chcemy od domeny, dlatego możemy wykonywać wywołania repozytorium przed zbudowaniem lub wywołaniem zachowania agregującego. Pomaga to również uniknąć problemu niespójnego stanu w pamięci i konieczności leniwego ładowania (zobacz ten artykuł ). Zapach polega na tym, że nie można już tworzyć instancji w pamięci jednostki bez obawy o dostęp do danych.
  2. CQS (Command Query Separation) może pomóc zmniejszyć potrzebę wywoływania repozytorium dla rzeczy w naszych jednostkach.
  3. Możemy użyć specyfikacji do enkapsulacji i komunikowania potrzeb logiki domeny i przekazania jej do repozytorium (usługa może dla nas zaaranżować te rzeczy). Specyfikacja może pochodzić od podmiotu odpowiedzialnego za utrzymanie tego niezmiennika. Repozytorium zinterpretuje części specyfikacji we własnej implementacji zapytania i zastosuje reguły ze specyfikacji do wyników zapytania. Ma to na celu utrzymanie logiki domeny w warstwie domeny. Służy również lepiej wszechobecnemu językowi i komunikacji. Wyobraź sobie, że mówisz „specyfikacja zamówienia przeterminowanego” w porównaniu z powiedzeniem „filtruj kolejność z tbl_order gdzie umiejscowienie_at jest mniej niż 30 minut przed sysdate” (zobacz tę odpowiedź ).
  4. Utrudnia to rozumowanie na temat zachowania podmiotów, ponieważ naruszona jest Zasada Jednej Odpowiedzialności. Jeśli musisz rozwiązać problemy z pamięcią / trwałością, wiesz, gdzie iść i gdzie nie iść.
  5. Pozwala to uniknąć niebezpieczeństwa udzielenia podmiotowi dwukierunkowego dostępu do stanu globalnego (za pośrednictwem repozytorium i usług domenowych). Nie chcesz także przekraczać granicy transakcji.

Vernon Vaughn w czerwonej książce Implementing Domain-Driven Design odnosi się do tego problemu w dwóch znanych mi miejscach (uwaga: ta książka jest w pełni poparta przez Evansa, jak można przeczytać we wstępie). W rozdziale 7 dotyczącym usług używa usługi domenowej i specyfikacji, aby obejść potrzebę użycia agregatu do korzystania z repozytorium i innego agregatu do ustalenia, czy użytkownik jest uwierzytelniony. Cytowany jest jako:

Zasadniczo powinniśmy starać się unikać korzystania z repozytoriów (12) z wnętrza agregatów, jeśli to w ogóle możliwe.

Vernon, Vaughn (06.02.2013). Wdrażanie projektowania opartego na domenie (lokalizacja Kindle 6089). Edukacja Pearson. Wersja Kindle.

A w rozdziale 10 na temat agregatów, w części zatytułowanej „Nawigacja modelu” , mówi (zaraz po tym, jak zaleca użycie globalnych unikalnych identyfikatorów do odwoływania się do innych zagregowanych pierwiastków):

Odwołanie przez tożsamość nie całkowicie uniemożliwia nawigację w modelu. Niektórzy używają repozytorium (12) z agregatu do wyszukiwania. Ta technika nazywa się Disconnected Domain Model i jest w rzeczywistości formą leniwego ładowania. Zalecane jest jednak inne podejście: użyj repozytorium lub usługi domenowej (7), aby wyszukać zależne obiekty przed wywołaniem zachowania agregacji. Usługa aplikacji klienckiej może to kontrolować, a następnie wysłać do agregacji:

On pokazuje przykład tego w kodzie:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

Następnie wspomina o jeszcze innym rozwiązaniu, w jaki sposób usługa domeny może być używana w metodzie komend agregujących wraz z podwójną wysyłką . (Nie mogę zalecić, na ile korzystne jest czytanie jego książki. Po zmęczeniu się niekończącym się szperaniem w Internecie, przejrzyj zasłużone pieniądze i przeczytaj książkę).

Następnie przeprowadziłem dyskusję z zawsze wdzięcznym Marco Pivetta @Ocramius, który pokazał mi trochę kodu po wyciągnięciu specyfikacji z domeny i użyciu tego:

1) Nie jest to zalecane:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) W przypadku usługi domenowej jest to dobre:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}
prograhammer
źródło
1
Pytanie: Zawsze uczymy się, aby nie tworzyć obiektu w niepoprawnym lub niespójnym stanie. Gdy wczytasz użytkowników z repozytorium, a następnie zadzwonisz, getFriends()zanim zrobisz cokolwiek innego, będzie pusty lub załadowany leniwie. Jeśli pusty, to ten obiekt leży i jest w nieprawidłowym stanie. Masz jakieś przemyślenia na ten temat?
Jimbo,
Repozytorium wywołuje domenę, aby utworzyć nową instancję. Nie dostajesz wystąpienia użytkownika bez przejścia przez domenę. Problem, który rozwiązuje ta odpowiedź, jest odwrotny. Gdzie domena odwołuje się do repozytorium i należy tego unikać.
prograhammer
28

To bardzo dobre pytanie. Nie mogę się doczekać dyskusji na ten temat. Ale myślę, że wspomniano o tym w kilku książkach o DDD, Jimmy Nilssons i Eric Evans. Myślę, że jest to również widoczne w przykładach użycia wzorca reposistorii.

ALE pozwala dyskutować. Myślę, że bardzo słuszną myślą jest to, dlaczego istota powinna wiedzieć, jak zachować inną istotę? Ważne w przypadku DDD jest to, że każda jednostka ma obowiązek zarządzania własną „sferą wiedzy” i nie powinna wiedzieć nic o tym, jak czytać lub pisać inne jednostki. Pewnie, że prawdopodobnie możesz po prostu dodać interfejs repozytorium do Jednostki A w celu odczytania Jednostek B. Ale istnieje ryzyko, że ujawnisz wiedzę na temat tego, jak zachować B. Czy jednostka A również wykona walidację na B przed utrwaleniem B w db?

Jak widać, jednostka A może bardziej zaangażować się w cykl życia jednostki B, co może zwiększyć złożoność modelu.

Wydaje mi się (bez żadnego przykładu), że testy jednostkowe będą bardziej złożone.

Ale jestem pewien, że zawsze będą scenariusze, w których masz ochotę korzystać z repozytoriów za pośrednictwem podmiotów. Musisz spojrzeć na każdy scenariusz, aby dokonać właściwej oceny. Plusy i minusy. Ale moim zdaniem rozwiązanie repozytorium-jednostka zaczyna się od wielu wad. To musi być bardzo szczególny scenariusz z Plusami, który równoważy Wady ....

Magnus Backeus
źródło
1
Słuszna uwaga. Model domeny starej szkoły prawdopodobnie miałby Podmiot B odpowiedzialny za weryfikację siebie, zanim się upuści, tak myślę. Czy na pewno Evans wspomina o bytach nie korzystających z repozytoriów? Jestem w połowie książki i jeszcze o niej nie wspominałem ...
codeulike
Cóż, czytałem książkę kilka lat temu (no 3 ...) i moja pamięć mnie zawodzi. Nie pamiętam, czy dokładnie to sformułował, ALE jednak sądzę, że zilustrował to przykładami. Interpretację jego przykładu Cargo (z jego książki) można również znaleźć na stronie dddsamplenet.codeplex.com . Pobierz projekt kodu (spójrz na projekt Vanilla - to przykład z książki). Przekonasz się, że repozytoria są używane tylko w warstwie aplikacji do uzyskiwania dostępu do jednostek domeny.
Magnus Backeus,
1
Pobierając przykład DDD SmartCA z książki p2p.wrox.com/ ... zobaczysz inne podejście (chociaż jest to klient Windows RIA), w którym repozytoria są używane w usługach (tutaj nic dziwnego), ale usługi są używane w entites. Tego bym nie zrobił, ALE jestem facetem od aplikacji webowych. Biorąc pod uwagę scenariusz aplikacji SmartCA, w której musisz być w stanie pracować w trybie offline, być może projekt ddd będzie wyglądał inaczej.
Magnus Backeus,
Przykład SmartCA brzmi interesująco, w którym jest rozdziale? (pliki do pobrania są uporządkowane według rozdziałów)
codeulike
1
@codeulike Obecnie projektuję i wdrażam framework wykorzystujący koncepcje ddd. Czasami sprawdzanie poprawności wymaga dostępu do bazy danych i zapytania do niej (na przykład: sprawdzanie unikatowego indeksu wielu kolumn). W związku z tym i faktem, że zapytania powinny być zapisywane w warstwie repozytorium Okazuje się, że jednostki domeny muszą mieć odniesienia do ich interfejsy repozytorium w warstwie modelu domeny, aby całkowicie zweryfikować walidację w warstwie modelu domeny. Czy zatem podmioty domeny mogą mieć dostęp do repozytoriów?
Karamafrooz
13

Po co wydzielać dostęp do danych?

Wydaje mi się, że na podstawie dwóch pierwszych stron rozdziału Model Driven Design znajduje się uzasadnienie, dlaczego chcesz wyodrębnić szczegóły techniczne implementacji z modelu domeny.

  • Chcesz zachować ścisłe połączenie między modelem domeny a kodem
  • Oddzielenie problemów technicznych pomaga udowodnić, że model jest praktyczny do wdrożenia
  • Chcesz, aby wszechobecny język przeniknął do projektu systemu

Wszystko to wydaje się mieć na celu uniknięcie osobnego „modelu analizy”, który zostaje oddzielony od faktycznego wdrożenia systemu.

Z tego, co rozumiem z tej książki, wynika, że ​​ten „model analizy” może ostatecznie zostać zaprojektowany bez uwzględnienia implementacji oprogramowania. Gdy programiści próbują wdrożyć model rozumiany przez stronę biznesową, tworzą własne abstrakcje z konieczności, powodując ściankę w komunikacji i zrozumieniu.

Z drugiej strony programiści, którzy wprowadzają zbyt wiele problemów technicznych do modelu domeny, mogą również powodować ten podział.

Można więc wziąć pod uwagę, że praktykowanie oddzielenia problemów, takich jak wytrwałość, może pomóc zabezpieczyć się przed zaprojektowaniem rozbieżnych modeli analizy. Jeśli wprowadzenie do modelu czegoś takiego jak wytrwałość wydaje się konieczne, oznacza to czerwoną flagę. Być może model nie jest praktyczny do wdrożenia.

Cytowanie:

„Pojedynczy model zmniejsza ryzyko błędu, ponieważ projekt jest teraz bezpośrednim skutkiem starannie przemyślanego modelu. Projekt, a nawet sam kod, ma komunikatywność modelu”.

Sposób, w jaki to interpretuję, jeśli skończyłeś z większą liczbą linii kodu zajmujących się takimi kwestiami jak dostęp do bazy danych, tracisz komunikatywność.

Jeśli potrzeba dostępu do bazy danych dotyczy między innymi sprawdzania unikatowości, spójrz na:

Udi Dahan: największe błędy popełniane przez zespoły przy stosowaniu DDD

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

w sekcji „Wszystkie reguły nie są sobie równe”

i

Wykorzystanie wzorca modelu domeny

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

w części „Scenariusze nieużywania modelu domeny”, która dotyczy tego samego tematu.

Jak oddzielić dostęp do danych

Ładowanie danych przez interfejs

„Warstwa dostępu do danych” została wydzielona przez interfejs, który wywołujesz w celu pobrania wymaganych danych:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

Plusy: Interfejs oddziela kod hydrauliczny „dostęp do danych”, pozwalając na pisanie testów. Dostęp do danych może być obsługiwany indywidualnie dla każdego przypadku, co zapewnia lepszą wydajność niż ogólna strategia.

Wady: kod wywołujący musi zakładać, co zostało załadowane, a co nie.

Powiedzmy, że GetOrderLines zwraca obiekty OrderLine z zerową właściwością ProductInfo ze względu na wydajność. Deweloper musi mieć dogłębną znajomość kodu kryjącego się za interfejsem.

Wypróbowałem tę metodę na prawdziwych systemach. Ostatecznie zmieniasz zakres tego, co jest ładowane przez cały czas, próbując naprawić problemy z wydajnością. W końcu zaglądasz za interfejs, aby zobaczyć kod dostępu do danych i zobaczyć, co jest ładowane, a co nie.

Teraz rozdzielenie problemów powinno pozwolić deweloperowi skupić się na jednym aspekcie kodu naraz, o ile to możliwe. Technika interfejsu usuwa W JAKI sposób ładowane są te dane, ale NIE W JAKI SPOSÓB ładowane są DUŻE dane, KIEDY są ładowane i GDZIE są ładowane.

Wniosek: dość niska separacja!

Powolne ładowanie

Dane są ładowane na żądanie. Wywołania w celu załadowania danych są ukryte w samym grafie obiektowym, gdzie dostęp do właściwości może spowodować wykonanie zapytania SQL przed zwróceniem wyniku.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

Plusy: „KIEDY, GDZIE I JAK” dostęp do danych jest ukryty przed deweloperem skupiającym się na logice domeny. W agregacie nie ma kodu zajmującego się ładowaniem danych. Ilość załadowanych danych może być dokładną ilością wymaganą przez kod.

Wady: gdy pojawia się problem z wydajnością, trudno jest go naprawić, gdy masz ogólne rozwiązanie „jeden rozmiar dla wszystkich”. Leniwe ładowanie może ogólnie pogorszyć wydajność, a wdrożenie leniwego ładowania może być trudne.

Interfejs ról / chętne pobieranie

Każdy przypadek użycia jest jawnie wyrażony za pomocą interfejsu roli zaimplementowanego przez klasę zagregowaną, umożliwiając obsługę strategii ładowania danych dla każdego przypadku użycia.

Strategia pobierania może wyglądać następująco:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

Wówczas Twój agregat może wyglądać następująco:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategy służy do budowania agregatu, a następnie agregat wykonuje swoją pracę.

Zalety: Pozwala na niestandardowy kod dla każdego przypadku użycia, pozwalając na optymalną wydajność. Jest zgodny z zasadą segregacji interfejsu . Brak wymagań dotyczących kodu złożonego. Testy jednostkowe agregatów nie muszą naśladować strategii ładowania. W większości przypadków można zastosować ogólną strategię ładowania (np. Strategię „wczytaj wszystko”), a w razie potrzeby można wdrożyć specjalne strategie ładowania.

Minusy: programista wciąż musi modyfikować / weryfikować strategię pobierania po zmianie kodu domeny.

Dzięki strategii pobierania możesz wciąż zmieniać niestandardowy kod pobierania w celu zmiany reguł biznesowych. Nie jest to idealna separacja problemów, ale będzie łatwiejsza w utrzymaniu i jest lepsza niż pierwsza opcja. Strategia pobierania zawiera dane HOW, WHEN i WHERE wczytywane. Ma lepszą separację problemów, bez utraty elastyczności, tak jak jeden rozmiar pasuje do wszystkich leniwych metod ładowania.

ttg
źródło
Dzięki, sprawdzę linki. Ale czy w swojej odpowiedzi mylisz „rozdzielenie obaw” z „całkowitym brakiem dostępu”? Z pewnością większość ludzi zgodziłaby się, że warstwa trwałości powinna być oddzielona od warstwy, w której znajdują się Istoty. Ale to różni się od powiedzenia „byty nie powinny być w stanie nawet zobaczyć warstwy trwałości, nawet poprzez bardzo ogólną implementację berło'.
codeulike
Ładowanie danych przez interfejs, czy nie, nadal martwisz się ładowaniem danych podczas wdrażania reguł biznesowych. Zgadzam się, że wiele osób wciąż nazywa ten rozdział obaw, być może lepszym terminem byłoby zastosowanie zasady pojedynczej odpowiedzialności.
ttg
1
Nie jestem pewien, jak przeanalizować swój ostatni komentarz, ale myślę, że sugerujesz, aby nie przetwarzać danych podczas przetwarzania reguł biznesowych? Widzę, że dzięki temu zasady byłyby „czystsze”. Ale wiele rodzajów reguł biznesowych będzie musiało odwoływać się do innych danych - czy sugerujesz, że należy je wcześniej załadować osobnym obiektem?
codeulike
@codeulike: Zaktualizowałem swoją odpowiedź. Nadal możesz ładować dane podczas reguł biznesowych, jeśli uważasz, że jest to absolutnie konieczne, ale nie wymaga to dodawania wierszy kodu dostępu do danych do modelu domeny (np. Leniwe ładowanie). W modelach domen, które zaprojektowałem, dane są zazwyczaj ładowane z wyprzedzeniem, tak jak powiedziałeś. Przekonałem się, że prowadzenie reguł biznesowych zwykle nie wymaga nadmiernej ilości danych.
ttg
12

Co za wspaniałe pytanie. Jestem na tej samej ścieżce odkrywania i większość odpowiedzi w Internecie wydaje się przynosić tyle problemów, ile przynosi rozwiązania.

Tak więc (na ryzyko napisania czegoś, z czym nie zgadzam się za rok) oto moje dotychczasowe odkrycia.

Przede wszystkim podoba nam się bogaty model domeny , który zapewnia nam wysoką wykrywalność (tego, co możemy zrobić z agregacją) i czytelność (ekspresyjne wywołania metod).

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

Chcemy to osiągnąć bez wprowadzania jakichkolwiek usług do konstruktora jednostki, ponieważ:

  • Wprowadzenie nowego zachowania (korzystającego z nowej usługi) może doprowadzić do zmiany konstruktora, co oznacza, że zmiana wpływa na każdą linię, która tworzy instancję encji !
  • Te usługi nie są częścią modelu , ale konstruktor wtryskiem sugerowałoby, że byli.
  • Często usługa (nawet jej interfejs) jest szczegółem implementacyjnym, a nie częścią domeny. Model domeny miałby zewnętrzną zależność .
  • To może być mylące dlaczego istota nie może istnieć bez tych zależności. (Mówisz, że jest to usługa not kredytowych? Nawet nic nie zrobię z notami kredytowymi ...)
  • Utrudniałoby to tworzenie instancji, a tym samym trudne do przetestowania .
  • Problem rozprzestrzenia się łatwo, ponieważ inne byty zawierające tę miałoby takie same zależności - które mogą na nich wyglądać bardzo nienaturalnie .

Jak więc możemy to zrobić? Do tej pory doszedłem do wniosku, że zależności od metod i podwójna wysyłka zapewniają godne rozwiązanie.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()wymaga teraz usługi odpowiedzialnej za tworzenie not kredytowych. Wykorzystuje podwójną wysyłkę , w pełni odciążając pracę do odpowiedzialnej usługi, przy jednoczesnym zachowaniu wykrywalności od Invoicejednostki.

SetStatus()teraz ma prostą zależność od programu rejestrującego, który oczywiście wykona część pracy .

W tym drugim przypadku, aby ułatwić kod klienta, możemy zamiast tego zalogować się za pomocą IInvoiceService. W końcu rejestrowanie faktur wydaje się dość nieodłącznym elementem faktury. Taki pojedynczy IInvoiceServicepomaga uniknąć potrzeby wszelkiego rodzaju mini-usług dla różnych operacji. Minusem jest to, że niejasne jest, co dokładnie zrobi ta usługa . Może nawet zacząć wyglądać na podwójną wysyłkę, podczas gdy większość pracy jest nadal wykonywana SetStatus()sama w sobie.

Wciąż moglibyśmy nazwać parametr „logger”, mając nadzieję na ujawnienie naszych zamiarów. Wydaje się jednak trochę słaby.

Zamiast tego wolałbym poprosić o IInvoiceLogger(jak to już zrobiliśmy w przykładzie kodu) i IInvoiceServicewdrożyć ten interfejs. Kod klienta może po prostu użyć jego pojedynczego IInvoiceServicekodu dla wszystkich Invoicemetod, które wymagają jakiejkolwiek tak szczególnej, „faktury usługowej” związanej z fakturami, podczas gdy podpisy metod wciąż wyraźnie wyjaśniają, o co proszą.

Zauważam, że nie zwracałem się do repozytoriów w odpowiedni sposób. Cóż, program rejestrujący jest repozytorium lub korzysta z niego, ale pozwól, że podam również bardziej wyraźny przykład. Możemy zastosować to samo podejście, jeśli repozytorium jest potrzebne tylko w jednej lub dwóch metodach.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

W rzeczywistości stanowi to alternatywę dla zawsze kłopotliwych leniwych ładunków .

Aktualizacja: zostawiłem poniższy tekst do celów historycznych, ale sugeruję 100% unikanie leniwych ładunków.

Dla prawdziwych, opartych na własności leniwych obciążeń, ja nie wykorzystują obecnie konstruktora wtrysku, ale w sposób utrwalania-ignorantami.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

Z jednej strony repozytorium ładujące dane Invoicez bazy danych może mieć swobodny dostęp do funkcji, która załaduje odpowiednie noty kredytowe i wstrzyknie tę funkcję do Invoice.

Z drugiej strony, kod, który tworzy rzeczywistą nową, Invoice po prostu przekaże funkcję, która zwraca pustą listę:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Zwyczaj ILazy<out T>mógłby nas uwolnić od brzydkiej obsady IEnumerable, ale skomplikowałoby to dyskusję).

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

Z przyjemnością usłyszę twoje opinie, preferencje i ulepszenia!

Timo
źródło
3

Wydaje mi się, że jest to ogólnie dobra praktyka związana z OOD, a nie specyficzna dla DDD.

Powody, o których mogę myśleć to:

  • Rozdzielenie obaw (jednostki powinny być oddzielone od sposobu, w jaki są utrwalane. Może istnieć wiele strategii, w których ten sam byt byłby utrwalony w zależności od scenariusza użytkowania)
  • Logicznie rzecz biorąc, jednostki mogą być widoczne na poziomie poniżej poziomu, na którym działają repozytoria. Komponenty niższego poziomu nie powinny mieć wiedzy na temat komponentów wyższego poziomu. Dlatego wpisy nie powinny mieć wiedzy o repozytoriach.
użytkownik1502505
źródło
2

po prostu Vernon Vaughn daje rozwiązanie:

Użyj repozytorium lub usługi domenowej, aby wyszukać zależne obiekty przed wywołaniem zachowania agregującego. Usługa aplikacji klienckiej może to kontrolować.

Alireza Rahmani Khalili
źródło
Ale nie od bytu.
ssmith
Źródło: Vernon Vaughn IDDD: klasa publiczna Kalendarz rozszerza EventSourcedRootEntity {... public CalendarEntry harmonogramCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz,
sprawdź swój artykuł @Teimuraz
Alireza Rahmani Khalili
1

Nauczyłem się kodować programowanie obiektowe, zanim pojawi się całe to oddzielne brzęczenie warstwy, a moje pierwsze obiekty / klasy DID odwzorowują bezpośrednio do bazy danych.

W końcu dodałem warstwę pośrednią, ponieważ musiałem przeprowadzić migrację na inny serwer bazy danych. Kilka razy widziałem / słyszałem o tym samym scenariuszu.

Myślę, że oddzielenie dostępu do danych (aka „Repozytorium”) od logiki biznesowej jest jedną z tych rzeczy, które zostały kilkakrotnie wymyślone na nowo, pomyślała książka Domain Driven Design, sprawiają, że jest to dużo „szumu”.

Obecnie używam 3 warstw (GUI, logika, dostęp do danych), podobnie jak wielu programistów, ponieważ jest to dobra technika.

Rozdzielenie danych na Repositorywarstwę (zwaną też Data Accesswarstwą) może być postrzegane jako dobra technika programowania, a nie tylko reguła.

Podobnie jak wiele metodologii, możesz zacząć, NIE zaimplementowany, i ostatecznie zaktualizować swój program, gdy tylko je zrozumiesz.

Cytat: Iliada nie została całkowicie wynaleziona przez Homera, Carmina Burana nie została całkowicie wymyślona przez Carla Orffa, aw obu przypadkach osoba, która zatrudniała innych, razem, otrzymała uznanie ;-)

umlcat
źródło
1
Dzięki, ale nie pytam o oddzielenie dostępu do danych od logiki biznesowej - to bardzo jasne, że istnieje bardzo szeroka zgoda. Pytam, dlaczego w architekturach DDD, takich jak S # arp, Entity nie mogą nawet „rozmawiać” z warstwą dostępu do danych. To ciekawa aranżacja, o której nie mogłem znaleźć wiele dyskusji.
codeulike
0

Czy to pochodzi z książki Erica Evansa Domain Driven Design, czy pochodzi z innych źródeł?

To stare rzeczy. Książka Erica sprawiła, że ​​trochę się rozgorzała.

Gdzie jest kilka dobrych wyjaśnień uzasadnienia?

Powód jest prosty - ludzki umysł słabnie, gdy staje w obliczu niejasno powiązanych wielu kontekstów. Prowadzą do dwuznaczności (Ameryka w Ameryce Południowej / Północnej oznacza Amerykę Południową / Północną), niejednoznaczność prowadzi do ciągłego mapowania informacji za każdym razem, gdy umysł „dotyka” tego, co podsumowuje się jako zła produktywność i błędy.

Logika biznesowa powinna być odzwierciedlona tak wyraźnie, jak to możliwe. Klucze obce, normalizacja, mapowanie relacyjne obiektów pochodzą z zupełnie innej dziedziny - są to kwestie techniczne, związane z komputerem.

Analogicznie: jeśli uczysz się pisma odręcznego, nie powinieneś być obciążony zrozumieniem, gdzie powstał pióro, dlaczego atrament trzyma się na papierze, kiedy wynaleziono papier i jakie są inne słynne chińskie wynalazki.

edytuj: Aby wyjaśnić: nie mówię o klasycznej praktyce OO polegającej na oddzielaniu dostępu do danych na osobną warstwę od logiki biznesowej - mówię o konkretnym rozwiązaniu, zgodnie z którym w DDD jednostki nie powinny rozmawiać z danymi warstwa dostępu w ogóle (tzn. nie powinny one zawierać odniesień do obiektów repozytorium)

Powód jest nadal taki sam, o którym wspomniałem powyżej. Tutaj jest tylko krok dalej. Dlaczego byty powinny częściowo ignorować upór, skoro mogą być (przynajmniej blisko) całkowicie? Nasz model ma mniej problemów niezwiązanych z dziedziną - więcej oddechu nasz umysł otrzymuje, gdy musi go ponownie zinterpretować.

Arnis Lapsa
źródło
Dobrze. Jak więc całkowicie ignorancka Istota wdraża Logikę Biznesową, jeśli nawet nie wolno jej rozmawiać z warstwą trwałości? Co robi, gdy musi patrzeć na wartości w dowolnych innych podmiotach?
codeulike
Jeśli twoja jednostka musi spojrzeć na wartości w dowolnych innych jednostkach, prawdopodobnie masz pewne problemy projektowe. Być może zastanów się nad podziałem klas, aby były bardziej spójne.
cdaq
0

Cytując Carolina Lilientahl, „Wzory powinny zapobiegać cyklom” https://www.youtube.com/watch?v=eJjadzMRQAk , gdzie odnosi się do cyklicznych zależności między klasami. W przypadku repozytoriów wewnątrz agregatów istnieje pokusa, aby tworzyć cykliczne zależności z powodu braku nawigacji po obiektach jako jedynego powodu. Wzorzec wspomniany powyżej przez prograhammer, który był zalecany przez Vernona Vaughna, w którym do innych agregatów odwołują się identyfikatory zamiast instancji root (czy istnieje nazwa dla tego wzorca?) Sugeruje alternatywę, która mogłaby prowadzić do innych rozwiązań.

Przykład cyklicznej zależności między klasami (spowiedź):

(Time0): Dwie klasy, Sample i Well, odnoszą się do siebie (zależność cykliczna). Studnia odnosi się do próbki, a próbka odwołuje się do studni, dla wygody (czasami zapętla próbki, a czasami zapętla wszystkie dołki w płytce). Nie wyobrażałem sobie przypadków, w których Próbka nie odwoływałaby się do studni, w której jest umieszczona.

(Czas1): Rok później wdrożono wiele przypadków użycia .... i są teraz przypadki, w których Próbka nie powinna odwoływać się do studni, w której jest umieszczona. W ramach jednego kroku znajdują się płytki tymczasowe. Tutaj studnia odnosi się do próbki, która z kolei odnosi się do studni na innej płytce. Z tego powodu czasami zachowuje się dziwne zachowanie, gdy ktoś próbuje wdrożyć nowe funkcje. Penetracja wymaga czasu.

Pomógł mi również wspomniany wyżej artykuł o negatywnych aspektach leniwego ładowania.

Edvard Englund
źródło
-1

W idealnym świecie DDD proponuje, aby jednostki nie miały odniesienia do warstw danych. ale nie żyjemy w idealnym świecie. Domeny mogą wymagać odwoływania się do innych obiektów domeny dla logiki biznesowej, z którymi mogą nie być zależne. Logiczne jest, aby jednostki odwoływały się do warstwy repozytorium wyłącznie w celu odczytu, aby pobrać wartości.

vsingh
źródło
Nie, wprowadza to niepotrzebne sprzężenie z bytami, narusza SRP i separację obaw i utrudnia deserializację bytu z trwałości (ponieważ proces deserializacji musi teraz również wstrzykiwać usługi / repozytoria, które bytuje byt).
ssmith