Niedopasowanie koncepcyjne między usługami aplikacji DDD a interfejsem API REST

20

Próbuję zaprojektować aplikację, która ma złożoną domenę biznesową i wymaga obsługi interfejsu API REST (nie tylko REST, ale zorientowana na zasoby). Mam problem z wynalezieniem modelu domeny w sposób zorientowany na zasoby.

W DDD klienci modelu domeny muszą przejść przez warstwę procedur „Application Services”, aby uzyskać dostęp do dowolnej funkcji biznesowej, wdrożonej przez Entities i Domain Services. Na przykład istnieje usługa aplikacji z dwiema metodami aktualizacji encji użytkownika:

userService.ChangeName(name);
userService.ChangeEmail(email);

Interfejs API tej usługi aplikacji udostępnia polecenia (czasowniki, procedury), a nie stan.

Ale jeśli musimy również dostarczyć interfejs API RESTful dla tej samej aplikacji, istnieje model zasobów użytkownika, który wygląda następująco:

{
name:"name",
email:"[email protected]"
}

Zorientowany na zasoby interfejs API udostępnia stan , a nie polecenia . Rodzi to następujące obawy:

  • każda operacja aktualizacji interfejsu API REST może być odwzorowana na jedno lub więcej wywołań procedur usługi aplikacji, w zależności od tego, jakie właściwości są aktualizowane w modelu zasobów

  • każda operacja aktualizacji wygląda jak atomowa dla klienta REST API, ale nie jest tak zaimplementowana. Każde wywołanie usługi aplikacji jest zaprojektowane jako osobna transakcja. Aktualizacja jednego pola w modelu zasobów może zmienić reguły sprawdzania poprawności dla innych pól. Musimy więc zweryfikować wszystkie pola modelu zasobów razem, aby upewnić się, że wszystkie potencjalne wywołania usługi aplikacji są prawidłowe, zanim zaczniemy je tworzyć. Sprawdzanie poprawności zestawu poleceń jednocześnie jest o wiele mniej trywialne niż wykonywanie pojedynczych poleceń. Jak to robimy na kliencie, który nawet nie wie, że istnieją poszczególne polecenia?

  • wywoływanie metod usługi aplikacji w innej kolejności może mieć inny efekt, a interfejs API REST sprawia, że ​​wygląda na to, że nie ma różnicy (w ramach jednego zasobu)

Mógłbym wymyślić więcej podobnych problemów, ale w zasadzie wszystkie one są spowodowane przez to samo. Po każdym wywołaniu usługi aplikacji zmienia się stan systemu. Zasady obowiązującej zmiany, zestaw działań, które jednostka może wykonać następną zmianę. Zorientowany na zasoby interfejs API stara się, aby wszystko wyglądało jak operacja atomowa. Ale złożoność przekroczenia tej luki musi gdzieś zniknąć i wydaje się ogromna.

Ponadto, jeśli interfejs użytkownika jest bardziej zorientowany na polecenia, co często ma miejsce, będziemy musieli odwzorować polecenia i zasoby po stronie klienta, a następnie ponownie po stronie interfejsu API.

Pytania:

  1. Czy cała ta złożoność powinna być obsługiwana przez (grubą) warstwę odwzorowania REST na AppService?
  2. A może brakuje mi czegoś w rozumieniu DDD / REST?
  3. Czy REST może po prostu być niepraktyczny do ujawnienia funkcjonalności modeli domen w pewnym (dość niskim) stopniu złożoności?
astreltsov
źródło
Pomyśl o kliencie REST jako o użytkowniku systemu. Absolutnie nie dbają o to, JAK system wykonuje czynności, które wykonuje. Nie można oczekiwać, że klient REST pozna wszystkie różne działania w domenie, niż użytkownik. Jak mówisz, ta logika musi iść gdzieś, ale musiałaby iść gdziekolwiek w dowolnym systemie, jeśli nie korzystasz z REST, po prostu przenosisz go do klienta. Nie robienie tego jest właśnie celem REST, klient powinien wiedzieć tylko, że chce zaktualizować stan i nie powinien mieć pojęcia, jak sobie z tym poradzić.
Cormac Mulhall
2
@astr Prosta odpowiedź jest taka, że ​​zasoby nie są twoim modelem, więc projekt kodu obsługi zasobów nie powinien wpływać na projekt twojego modelu. Zasoby są zewnętrznym aspektem systemu, w którym model jest wewnętrzny. Pomyśl o zasobach w taki sam sposób, jak myślisz o interfejsie użytkownika. Użytkownik może kliknąć jeden przycisk w interfejsie użytkownika i w modelu dzieje się sto różnych rzeczy. Podobne do zasobu. Klient aktualizuje zasób (pojedynczą instrukcję PUT) i w modelu może się zdarzyć milion różnych rzeczy. Anty-wzór polega na ścisłym powiązaniu modelu z zasobami.
Cormac Mulhall
1
To dobra rozmowa na temat traktowania działań w domenie jako skutków ubocznych zmian stanu REST, utrzymywania domeny i sieci osobno (szybkie przejście do 25 minut dla soczystych bitów) yow.eventer.com/events/1004/talks/1047
Cormac Mulhall
1
Nie jestem też pewny co do całego tego „użytkownika jako robota / maszyny stanów”. Myślę, że powinniśmy dążyć do tego, aby nasze interfejsy użytkownika były o wiele bardziej naturalne ...
guillaume31

Odpowiedzi:

10

Miałem ten sam problem i „go rozwiązałem”, modelując zasoby REST inaczej, np .:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Więc zasadniczo podzieliłem większy, złożony zasób na kilka mniejszych. Każdy z nich zawiera nieco spójną grupę atrybutów oryginalnego zasobu, która ma być przetwarzana razem.

Każda operacja na tych zasobach ma charakter atomowy, nawet jeśli może być zaimplementowana przy użyciu kilku metod serwisowych - przynajmniej w Spring / Java EE nie jest problemem utworzenie większej transakcji z kilku metod, które pierwotnie miały mieć swoją własną transakcję (przy użyciu transakcji WYMAGANEJ) propagacja). Często nadal musisz przeprowadzić dodatkową weryfikację tego specjalnego zasobu, ale nadal jest to całkiem możliwe do zarządzania, ponieważ atrybuty są (powinny) być spójne.

Jest to również dobre dla podejścia HATEOAS, ponieważ twoje bardziej szczegółowe zasoby przekazują więcej informacji o tym, co możesz z nimi zrobić (zamiast mieć tę logikę zarówno na kliencie, jak i serwerze, ponieważ nie można jej łatwo przedstawić w zasobach).

Oczywiście nie jest doskonały - jeśli interfejsy użytkownika nie są modelowane z myślą o tych zasobach (zwłaszcza interfejsy zorientowane na dane), mogą powodować pewne problemy - np. Interfejs użytkownika prezentuje dużą formę wszystkich atrybutów danych zasobów (i jego pod-zasobów) i pozwala edytuj je wszystkie i zapisz od razu - tworzy to iluzję atomowości, nawet jeśli klient musi wywołać kilka operacji na zasobach (które same są atomowe, ale cała sekwencja nie jest atomowa).

Ponadto ten podział zasobów czasami nie jest łatwy ani oczywisty. Robię to głównie na zasobach o złożonych zachowaniach / cyklach życia, aby zarządzać ich złożonością.

qbd
źródło
Tak też myślałem - utwórz bardziej szczegółowe reprezentacje zasobów, ponieważ są one wygodniejsze dla operacji zapisu. Jak radzisz sobie z zapytaniami o zasoby, gdy stają się one tak szczegółowe? Czy tworzyć również znormalizowane reprezentacje tylko do odczytu?
astreltsov
1
Nie, nie mam zdekormalizowanych reprezentacji tylko do odczytu. Używam jsonapi.org standardu i posiada mechanizm włączenia podobnych środków w odpowiedzi dla danego zasobu. Zasadniczo mówię: „daj mi Użytkownika o identyfikatorze 1, a także podaj jego adres e-mail i aktywację”. Pomaga to pozbyć się dodatkowych wywołań REST dla podrzędnych zasobów i nie wpływa na złożoność klienta radzącego sobie z tymi podrzędnymi zasobami, jeśli używana jest dobra biblioteka klienta JSON API.
qbd
Tak więc pojedyncze żądanie GET na serwerze przekłada się na jedno lub więcej rzeczywistych zapytań (w zależności od liczby uwzględnionych zasobów podrzędnych), które są następnie łączone w jeden obiekt zasobów?
astreltsov,
Co zrobić, jeśli potrzebny jest więcej niż jeden poziom zagnieżdżania?
astreltsov,
Tak, w relacyjnych DBS prawdopodobnie przełoży się to na wiele zapytań. Arbitralne zagnieżdżanie jest obsługiwane przez JSON API, jest opisane tutaj: jsonapi.org/format/#fetching-includes
qbd
0

Kluczową kwestią jest tutaj, w jaki sposób logika biznesowa jest wywoływana w sposób przejrzysty, gdy wykonywane jest wywołanie REST? Jest to problem, który nie jest bezpośrednio rozwiązany przez REST.

Rozwiązałem to, tworząc własną warstwę zarządzania danymi w oparciu o dostawcę trwałości, takiego jak JPA. Używając meta modelu z niestandardowymi adnotacjami, możemy wywoływać odpowiednią logikę biznesową, gdy zmienia się stan encji. Zapewnia to, że niezależnie od tego, jak zmienia się stan jednostki, wywoływana jest logika biznesowa. Utrzymuje architekturę SUCHĄ, a także logikę biznesową w jednym miejscu.

Korzystając z powyższego przykładu, możemy wywołać metodę logiki biznesowej o nazwie validateName, gdy pole nazwy zostanie zmienione za pomocą REST:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Mając do dyspozycji takie narzędzie, wszystko, co musisz zrobić, to odpowiednio opisać metody logiki biznesowej.

codedabbler
źródło
0

Mam problem z wynalezieniem modelu domeny w sposób zorientowany na zasoby.

Nie powinieneś ujawniać modelu domeny w sposób zorientowany na zasoby. Powinieneś udostępniać aplikację w sposób zorientowany na zasoby.

jeśli interfejs użytkownika jest bardziej zorientowany na polecenia, co często ma miejsce, wówczas będziemy musieli odwzorować polecenia i zasoby po stronie klienta, a następnie z powrotem po stronie interfejsu API.

Wcale nie - wysyłaj polecenia do zasobów aplikacji, które łączą się z modelem domeny.

każda operacja aktualizacji interfejsu API REST może być odwzorowana na jedno lub więcej wywołań procedur usługi aplikacji, w zależności od tego, jakie właściwości są aktualizowane w modelu zasobów

Tak, chociaż istnieje nieco inny sposób przeliterowania tego, który może uprościć sprawę; każda operacja aktualizacji w stosunku do interfejsu API REST odwzorowuje proces, który rozsyła polecenia do jednego lub większej liczby agregatów.

każda operacja aktualizacji wygląda jak atomowa dla klienta REST API, ale nie jest tak zaimplementowana. Każde wywołanie usługi aplikacji jest zaprojektowane jako osobna transakcja. Aktualizacja jednego pola w modelu zasobów może zmienić reguły sprawdzania poprawności dla innych pól. Musimy więc zweryfikować wszystkie pola modelu zasobów razem, aby upewnić się, że wszystkie potencjalne wywołania usługi aplikacji są prawidłowe, zanim zaczniemy je tworzyć. Sprawdzanie poprawności zestawu poleceń jednocześnie jest o wiele mniej trywialne niż wykonywanie pojedynczych poleceń. Jak to robimy na kliencie, który nawet nie wie, że istnieją poszczególne polecenia?

Gonisz tutaj niewłaściwy ogon.

Wyobraź sobie: całkowicie usuń REST ze zdjęcia. Wyobraź sobie, że piszesz interfejs pulpitu dla tej aplikacji. Wyobraźmy sobie, że masz naprawdę dobre wymagania projektowe i wdrażasz interfejs użytkownika oparty na zadaniach. Tak więc użytkownik otrzymuje minimalistyczny interfejs, który jest idealnie dostrojony do wykonywanego zadania; użytkownik określa niektóre dane wejściowe, a następnie „VERB!” przycisk.

Co się teraz stanie? Z punktu widzenia użytkownika jest to jedno zadanie atomowe do wykonania. Z perspektywy domainModel jest to szereg poleceń uruchamianych przez agregaty, przy czym każde polecenie jest uruchamiane w osobnej transakcji. Te są całkowicie niezgodne! Potrzebujemy czegoś pośrodku, aby wypełnić lukę!

Coś jest „aplikacją”.

Na szczęśliwej ścieżce aplikacja odbiera trochę DTO i analizuje ten obiekt, aby uzyskać zrozumiały komunikat, i wykorzystuje dane w komunikacie do tworzenia dobrze sformułowanych poleceń dla jednego lub większej liczby agregatów. Aplikacja upewni się, że każde polecenie, które wysyła do agregatów, jest dobrze uformowane (jest to działająca warstwa antykorupcyjna) i załaduje agregaty i zapisze agregaty, jeśli transakcja zakończy się pomyślnie. Agregat sam zdecyduje, czy polecenie jest prawidłowe, biorąc pod uwagę jego bieżący stan.

Możliwe wyniki - wszystkie komendy działają poprawnie - warstwa antykorupcyjna odrzuca komunikat - niektóre komendy działają poprawnie, ale potem jedna z agregacji narzeka i masz możliwość zaradzenia.

Teraz wyobraź sobie, że masz tę aplikację zbudowaną; jak współdziałasz z nim w sposób RESTful?

  1. Klient rozpoczyna od opisu hipermedialnego swojego obecnego stanu (tj. Interfejsu użytkownika opartego na zadaniu), w tym kontroli hipermedialnej.
  2. Klient wysyła reprezentację zadania (tj. DTO) do zasobu.
  3. Zasób analizuje przychodzące żądanie HTTP, pobiera reprezentację i przekazuje ją do aplikacji.
  4. Aplikacja uruchamia zadanie; z punktu widzenia zasobu jest to czarna skrzynka, która ma jeden z następujących wyników
    • aplikacja pomyślnie zaktualizowała wszystkie agregaty: zasób zgłasza klientowi sukces, kierując go do nowego stanu aplikacji
    • warstwa antykorupcyjna odrzuca komunikat: zasób zgłasza klientowi błąd 4xx (prawdopodobnie Błędne żądanie), prawdopodobnie przekazując opis napotkanego problemu.
    • aplikacja aktualizuje niektóre agregacje: zasób informuje klienta, że ​​polecenie zostało zaakceptowane, i kieruje klienta do zasobu, który zapewni reprezentację postępu polecenia.

Zaakceptowane jest zwykłe wylogowywanie, gdy aplikacja zamierza odroczyć przetwarzanie komunikatu do momentu odpowiedzi na klienta - zwykle używane przy akceptowaniu polecenia asynchronicznego. Ale działa również dobrze w tym przypadku, w którym operacja, która ma być atomowa, wymaga ograniczenia.

W tym idiomie zasób reprezentuje samo zadanie - zaczynasz nową instancję zadania, publikując odpowiednią reprezentację w zasobie zadania, a ten zasób łączy się z aplikacją i kieruje cię do następnego stanu aplikacji.

W , prawie za każdym razem, gdy koordynujesz wiele poleceń, chcesz myśleć w kategoriach procesu (inaczej proces biznesowy, inaczej saga).

W modelu do odczytu występuje podobne niedopasowanie pojęciowe. Ponownie rozważ interfejs oparty na zadaniach; jeśli zadanie wymaga modyfikacji wielu agregatów, wówczas interfejs użytkownika do przygotowania zadania prawdopodobnie zawiera dane z wielu agregatów. Jeśli twój plan zasobów to 1: 1 z agregatami, będzie to trudne do zorganizowania; zamiast tego należy udostępnić zasób, który zwraca reprezentację danych z kilku agregatów, wraz z formantem hipermedia, który odwzorowuje relację „uruchomienie zadania” na punkcie końcowym zadania, jak omówiono powyżej.

Zobacz także: REST w praktyce autorstwa Jima Webbera.

VoiceOfUnreason
źródło
Jeśli projektujemy interfejs API do interakcji z naszą domeną zgodnie z naszymi przypadkami użycia. Dlaczego nie zaprojektować rzeczy w taki sposób, aby Sagas nie były wcale wymagane? Może czegoś mi brakuje, ale czytając twoją odpowiedź, naprawdę wierzę, że REST nie pasuje do DDD i lepiej jest używać procedur zdalnych (RPC). DDD jest zorientowane na zachowanie, podczas gdy REST jest zorientowane na czasownik http. Dlaczego nie usunąć REST z obrazu i ujawnić zachowanie (polecenia) w interfejsie API? W końcu prawdopodobnie zostały zaprojektowane, aby spełnić scenariusze przypadków użycia, a prob są transakcyjne. Jaka jest zaleta REST, jeśli posiadamy interfejs użytkownika?
iberodev