Usługi iniekcji DDD do wywołań metod podmiotów


11

Krótki format pytania

Czy w ramach najlepszych praktyk DDD i OOP jest wstrzykiwanie usług do wywołań metod encji?

Przykład długiego formatu

Załóżmy, że mamy w DDD klasyczny przypadek Line-LineItems, w którym mamy Encję Domenową o nazwie Zamówienie, która działa również jako Korzeń Agregacji, a Encja składa się nie tylko z jej Obiektów Wartości, ale także kolekcji Elementu Zamówienia. Podmioty

Załóżmy, że chcemy płynnej składni w naszej aplikacji, abyśmy mogli zrobić coś takiego (zwracając uwagę na składnię w wierszu 2, w którym wywołujemy getLineItemsmetodę):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Nie chcemy wstrzykiwać żadnego rodzaju LineItemRepository do OrderEntity, ponieważ jest to naruszenie kilku zasad, o których mogę myśleć. Ale płynność składni jest czymś, czego naprawdę chcemy, ponieważ jest łatwa do odczytania i utrzymania, a także do testowania.

Rozważ następujący kod, odnotowując metodę getLineItemsw OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Czy jest to przyjęty sposób implementacji płynnej składni w Entities bez naruszenia podstawowych zasad DDD i OOP? Wydaje mi się, że jest w porządku, ponieważ ujawniamy tylko warstwę usługi, a nie warstwę infrastruktury (która jest zagnieżdżona w usłudze)

Odpowiedzi:


9

Przekazanie usługi domeny w połączeniu z podmiotem jest całkowicie w porządku . Powiedzmy, że musimy obliczyć sumę faktury za pomocą skomplikowanego algorytmu, który może zależeć, powiedzmy, od rodzaju klienta. Oto, jak może to wyglądać:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Innym podejściem jest jednak wyodrębnienie logiki biznesowej, która znajduje się w usłudze domeny za pośrednictwem zdarzeń domeny . Należy pamiętać, że takie podejście implikuje tylko różne usługi aplikacji, ale ten sam zakres transakcji bazy danych.

Trzecie podejście jest tym, za którym opowiadam się: jeśli korzystam z usługi domenowej, prawdopodobnie oznacza to, że przegapiłem jakąś koncepcję domeny, ponieważ modeluję swoje koncepcje przede wszystkim za pomocą rzeczowników , a nie czasowników. Idealnie więc nie potrzebuję usługi domenowej, a duża część mojej logiki biznesowej znajduje się w dekoratorach .


6

Jestem zszokowany czytając niektóre odpowiedzi tutaj.

Całkowicie poprawne jest przekazywanie usług domenowych do metod encji w DDD w celu delegowania niektórych obliczeń biznesowych. Na przykład wyobraź sobie, że Twój zagregowany katalog główny (jednostka) musi uzyskać dostęp do zewnętrznego zasobu za pośrednictwem protokołu http, aby wykonać logikę biznesową i wywołać zdarzenie. Jeśli nie wstrzykniesz usługi metodą biznesową jednostki, jak inaczej byś to zrobił? Czy utworzyłbyś instancję klienta http wewnątrz swojej jednostki? To brzmi jak okropny pomysł.

Niepoprawne jest wstrzykiwanie usług w agregaty za pośrednictwem jego konstruktora. Ale dzięki metodzie biznesowej jest to w porządku i całkowicie normalne.


1
Dlaczego podana sprawa nie byłaby odpowiedzialna za usługę domenową?
e_i_pi

1
jest to usługa domenowa, ale została wprowadzona do metody biznesowej. Warstwa aplikacji jest tylko orkiestratorem,
diegosasw

Nie mam doświadczenia w DDD, ale czy usługa domeny nie powinna być wywoływana z usługi aplikacji, a po sprawdzeniu poprawności usługi domeny nadal wywoływać metody encji za pośrednictwem tej usługi aplikacji? W moim projekcie mam do czynienia z tym samym problemem, ponieważ usługa domenowa uruchamia wywołanie bazy danych przez repozytorium ... Nie wiem, czy to jest w porządku.
Muflix,

Usługa domeny powinna koordynować, jeśli wywołasz ją później z aplikacji, oznacza to, że w jakiś sposób przetworzysz odpowiedź, a następnie coś z nią zrobisz. Może to brzmi jak logika biznesowa. Jeśli tak, należy do warstwy Domeny, a aplikacja po prostu rozwiązuje zależność i wstrzykuje ją do agregatu. Usługa domeny mogła wstrzyknąć repozytorium, którego baza danych uderzeń implementacji powinna należeć do warstwy infrastruktury (tylko implementacja, a nie interfejs / umowa). Jeśli opisuje Twój wszechobecny język, należy do domeny.
diegosasw

5

Czy w ramach najlepszych praktyk DDD i OOP jest wstrzykiwanie usług do wywołań metod encji?

Nie, nie powinieneś wstrzykiwać niczego w warstwę domeny (obejmuje to byty, obiekty wartości, fabryki i usługi domenowe). Ta warstwa powinna być niezależna od dowolnego frameworka, bibliotek stron trzecich lub technologii i nie powinna wykonywać żadnych wywołań IO.

$order->getLineItems($orderService)

Jest to niewłaściwe, ponieważ agregat nie powinien potrzebować niczego innego oprócz siebie, aby zwrócić zamówione elementy. Cały Kruszywo powinno być już załadowany przed jego wywołania metody. Jeśli uważasz, że to powinno być leniwie załadowane, są dwie możliwości:

  1. Twoje granice agregatów są nieprawidłowe, są zbyt duże.

  2. W tym przypadku używasz agregatu tylko do czytania. Najlepszym rozwiązaniem jest rozdzielenie modelu zapisu od modelu odczytu (tj. Użycie CQRS ). W tej bardziej przejrzystej architekturze nie można przesyłać zapytań do agregatu, ale model odczytu.


Jeśli potrzebuję wezwania do sprawdzenia poprawności bazy danych, muszę wywołać je w usłudze aplikacji i przekazać wynik do usługi domeny lub bezpośrednio do zagregowanego katalogu głównego, a następnie wstrzyknąć repozytorium do usługi domeny?
Muflix,

1
@Muflix tak, zgadza się
Constantin Galbenu,

3

Kluczowa idea w taktycznych wzorcach DDD: aplikacja uzyskuje dostęp do wszystkich danych w aplikacji, działając na zagregowanym katalogu głównym. Oznacza to, że jedynymi jednostkami dostępnymi poza modelem domeny są zagregowane korzenie.

Główny agregat zamówienia nigdy nie dałby odwołania do swojej kolekcji elementu liniowego, który pozwoliłby na modyfikację kolekcji, ani nie dałby kolekcji odniesień do dowolnego elementu zamówienia, który pozwoliłby na jego modyfikację. Jeśli chcesz zmienić agregację zamówień, obowiązuje zasada hollywood: „Powiedz, nie pytaj”.

Zwracanie wartości z agregatu jest w porządku, ponieważ wartości są z natury niezmienne; nie możesz zmienić moich danych, zmieniając ich kopię.

Użycie usługi domenowej jako argumentu, aby pomóc agregacji w zapewnieniu poprawnych wartości, jest całkowicie rozsądnym posunięciem.

Zwykle nie używasz usługi domenowej, aby zapewnić dostęp do danych znajdujących się w agregacie, ponieważ agregat powinien już mieć do niego dostęp.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Pisownia jest więc dziwna, jeśli próbujemy uzyskać dostęp do kolekcji wartości zamówienia tego elementu zamówienia. Bardziej naturalna pisownia byłaby

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Oczywiście zakłada to, że elementy zamówienia zostały już załadowane.

Zwykle wzorzec jest taki, że obciążenie agregatu będzie obejmować cały stan wymagany dla konkretnego przypadku użycia. Innymi słowy, możesz mieć kilka różnych sposobów ładowania tego samego agregatu; twoje metody repozytorium są odpowiednie do celu .

Podejście to nie jest czymś, co można znaleźć w oryginalnym Evansie, w którym założył, że z agregatem byłby powiązany jeden model danych. Bardziej naturalnie wypada z CQRS.


Dzięki za to. Przeczytałem już około połowy „czerwonej książki” i po raz pierwszy spróbowałem właściwie zastosować zasadę Hollywood w warstwie infrastruktury. Ponownie czytając wszystkie te odpowiedzi, wszystkie mają dobre punkty, ale myślę, że twoje mają bardzo ważne punkty dotyczące zakresu lineItems()i wstępnego ładowania po pierwszym pobraniu Korzenia Agregatu.
e_i_pi,

3

Ogólnie rzecz biorąc, obiekty wartości należące do agregacji same w sobie nie mają repozytorium. Zagospodarowanie ich jest obowiązkiem użytkownika root. W twoim przypadku obowiązkiem Twojego OrderRepository jest wypełnienie zarówno obiektów encji Order, jak i obiektów OrderLine.

Jeśli chodzi o implementację infrastruktury OrderRepository, w przypadku ORM, jest to relacja jeden do wielu, i możesz wybrać chętnie lub leniwie ładować OrderLine.

Nie jestem pewien, co dokładnie oznaczają twoje usługi. Jest to dość zbliżone do „usługi aplikacji”. W takim przypadku generalnie nie jest dobrym pomysłem wstrzykiwanie usług do Aggregate root / Entity / Value Object. Usługa aplikacji powinna być klientem zagregowanego katalogu głównego / obiektu / wartości i usługi domeny. Inną rzeczą w twoich usługach jest to, że ujawnianie obiektów wartości w usłudze aplikacji również nie jest dobrym pomysłem. Powinny być dostępne przez zagregowany root.


2

Odpowiedź brzmi: zdecydowanie NIE, unikaj przekazywania usług metodami encji.

Rozwiązanie jest proste: pozwól repozytorium zamówień zwrócić zamówienie ze wszystkimi jego elementami LineItems. W twoim przypadku agregacja to Order + LineItems, więc jeśli repozytorium nie zwróci pełnej agregacji, to nie wykonuje swojego zadania.

Szerszą zasadą jest: oddziel bity funkcjonalne (np. Logika domeny) od bitów niefunkcjonalnych (np. Trwałość).

Jeszcze jedna rzecz: jeśli możesz, staraj się unikać tego:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Zrób to zamiast tego

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

W projektowaniu obiektowym staramy się unikać połowów danych obiektowych. Wolimy prosić obiekt o zrobienie tego, co chcemy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.