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 getLineItems
metodę):
$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ę getLineItems
w 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)
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.
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:
Twoje granice agregatów są nieprawidłowe, są zbyt duże.
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.
źródło
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.
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
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.
źródło
lineItems()
i wstępnego ładowania po pierwszym pobraniu Korzenia Agregatu.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.
źródło
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:
Zrób to zamiast tego
W projektowaniu obiektowym staramy się unikać połowów danych obiektowych. Wolimy prosić obiekt o zrobienie tego, co chcemy.
źródło