Ładowanie agregatów, granice agregatów, encje - kilka pytań

Ładowanie agregatów, granice agregatów, encje - kilka pytań
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
1

@Aventus:
Ja agregat przekazuję dalej do implementacji Repozytorium.

No bo masz sobie interfejs Repo z metodą

Kopiuj
Order save(Order order);

I musisz to gdzieś zaimplementować w warstwie infry. Czy to z jakimś ORM czy bezpośrednio robiąc SQL czy też może Nosql.

Więc warstwa infry wie o agregacie.

A co do serwisów aplikacyjnych. Ok, czyli u Ciebie to komendy + jakiś command handler. A jak on się ma do serwisów domenowych? Bo z tego co zrozumiałem to u Ciebie w jakimś module siedzi załadowana generyczna obsługa łapania komend, ładowania odpowiedniego agregatu, wołany metody Receive i zapisu.

A gdzie w tym serwisy domenowe, fabryki, polityki i jakas orkiestracja? Bo mówi się, że to w serwisach aplikacyjnych jest orkiestracja. U Ciebie to jest w serwisach domenowych jak mniemam ?

Mowisz ze serwisy aplikacyjne nie mogą się komunikować, ale jeśli np masz wymaganie, że przy dodawaniu lub updacie itemu w apce charytatywnej powiązana z nim kampania musi istnieć i być aktywna.

W requescie idą dane o itemie + supportedCampaignId. No i musisz odpytać moduł kampanii czy taka kampania o takim id istnieje i jest aktywna. I to niezależnie czy modul kampanii to inny mikroserwis czy tylko inny modul w tym samym mikroserwisie. I jak rozumiem to jest u Ciebie serwis domenowy?

edytowany 1x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@Bambo:

...
Więc warstwa infry wie o agregacie.

Oczywiście, koniec końców musisz to zapisać. Ale dla mnie to żaden powód żeby bawić się w jakieś nadmierne zabezpieczanie wnętrza agregatu. Przecież repo nie ma prawa wykonywać modyfikacji agregatu, tylko ma go po prostu zapisać. Znów- jeśli coś takiego ma miejsce (modyfikowanie agregatu) to jest to błąd który należy wyłapywać na etapie code review. Ja po prostu nie jestem zwolennikiem usilnego próbowania zapobiec każdej możliwej sytuacji z dokładnością atomową, skoro nie wystawiamy agregatów jako jakiegoś frameworka. Nie tworzymy tego do opakowania i wystawienia do użytku przez wiele innych aplikacji, użytkowników itp. Agregaty to sprawa wewnętrzna, więc takie bawienie się w zabezpieczanie jakbyśmy projektowali framework to strata czasu dla mnie.

A co do serwisów aplikacyjnych. Ok, czyli u Ciebie to komendy + jakiś command handler. A jak on się ma do serwisów domenowych? Bo z tego co zrozumiałem to u Ciebie w jakimś module siedzi załadowana generyczna obsługa łapania komend, ładowania odpowiedniego agregatu, wołany metody Receive i zapisu.

Zazwyczaj taki "serwis" domenowy to po prostu command handler, używany w sytuacji kiedy commenda nie może trafić bezpośrednio do agregatu (np. wspomniana wcześniej walidacja email). Innymi słowy zamiast mieć serwis z wieloma odpowiedzialnościami, przestrzrgam SRP mając proste mapowanie: command -> command handler. Wtedy taki handler zazwyczaj nie musi mieć dostępu do agregatu, a jeśli musi to pewnie wstrzyknął bym jakieś repo z którego handler by go załadował.

A gdzie w tym serwisy domenowe, fabryki, polityki i jakas orkiestracja? Bo mówi się, że to w serwisach aplikacyjnych jest orkiestracja. U Ciebie to jest w serwisach domenowych jak mniemam ?

Jako że agregaty idealnie powinny emitować eventy do opisywania zmian jakie zaszły, ja używam event-driven archiecture, czasem z event sourcingiem. Wtedy coś takiego jak orkiestracja procesu (o ile w ogóle jakaś jest) zależy od tego jak zaimplementowana jest taka architektura- albo mam orkiestrację do czego używam process managers/sagas albo mam choreografię, czyli poszczególnie elementy reagują na eventy z innych części systemu (agregatów) bez centralnej kontroli. Do tego zazwyczaj mam jakieś event handlery które mapują event na commandy trafiające do odpowiednio agregatu- taki handler jest trochę jak endpoint mapujący, z tą różnicą że w tym przypadku mapujemy z eventu na command, zamiast z DTO na command,

Mowisz ze serwisy aplikacyjne nie mogą się komunikować, ale jeśli np masz wymaganie, że przy dodawaniu lub updacie itemu w apce charytatywnej powiązana z nim kampania musi istnieć i być aktywna.

Tak jak wyżej, od tego mam event-driven architecture i asynchroniczne wykonywanie procesów. Ale to samo mógłbyś wykonać synchronicznie, obsługując łańcuch eventów w pamięci procesu.

Polecam ten artykuł Napisany przez Microsoft ale uniwersalnie pasuje do każdej technologii.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
N0
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:Gdańsk
  • Postów:647
0

@Aventus: czyli te komendy, które może obsłużyć samodzielnie agregat, trafiają bezpośrednio do niego, a pozostałe, wymagające wywoływania jakiś serwisów, trafiają do jakichś command handlerów (tak jak w MediatR)? Dużo masz takich komend, które idą do agregatu bezpośrednio, u siebie w projekcie?

Kopiuj
services.SetupAggregateRoutings(config => 
  config.OnCommand<AddOrderItem >()
    .SendToAggregate<Order>()
    .CorrelatedOn(c => c.OrderId)
edytowany 1x, ostatnio: nobody01
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
3

@nobody01: dokładnie tak, wtedy ten kod konfigurujacy trochę się różni bo zamiast SendToAggregate jest SendToHandler.

Teraz obecnie nie pracuje przy projekcie używającym DDD ale wtedy nie było tego dużo. Ciężko mi podać dokładne proporcje. Rzecz w tym że często da się uniknąć stosowania serwisów przy dobrze zamodelowanych agregatach. Oczywiście nie zawsze.

Poza tym pracowałem również przy dużym projekcie gdzie agregaty wykonywały również prace serwisów domenowych, a potrzebne zaleznosci były wstrzykiwane. Zasady DDD zostały złamane? Tak. Ale co z tego jeśli wszystko nadal zachowywało spójność i się sprawdzało?

Zresztą nawet Vernon w swojej książce wprost pisze że czasem zasady trzeba łamać. Trzeba tylko wiedzieć kiedy i co się z tym wiąże, zamiast na ślepo łamać wszystkie zasady kiedy tylko zderzamy się z jakimś problemem.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
N0
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:Gdańsk
  • Postów:647
1

@Aventus: W tym podejściu, gdzie agregat służy jako serwis domenowy, w agregacie Order mielibyśmy np. taką metodę?

Kopiuj
Receive(AddProduct command, IProductPriceValidator productPriceValidator) // cena nie może być mniejsza niż minimalna dopuszczalna

Podobnie wstrzykiwalibyśmy jakieś serwisy od autoryzacji?

Jak tak patrzę, to w wielu przypadkach command handlery z MediatR są 3-linijkowe praktycznie, np. tu: https://github.com/kgrzybek/modular-monolith-with-ddd/blob/master/src/Modules/Meetings/Application/Meetings/CancelMeeting/CancelMeetingCommandHandler.cs

edytowany 4x, ostatnio: nobody01
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
3

@nobody01: tak, chociaż w tym przypadku który ja opisywałem wstrzykiwaliśmy zależności przez konstruktor. Ponieważ dążyliśmy do tego aby tworzyć małe, spójne agregaty to nie było to problemem, ponieważ taki agregat przyjmował zazwyczaj jedna, a maksymalnie chyba trzy zależności jeśli dobrze pamiętam.

Podobnie wstrzykiwalibyśmy jakieś serwisy od autoryzacji?

Nie jestem pewny jaką autoryzację masz na myśli. Autoryzacja i uwierzytelnianie trzymam w warstwie web/aplikacji, chyba że masz na myśli jakąś "autoryzację" w formie walidacji zasady biznesowej, np. admin może zmieniać wszystkie ordery ale zwykły użytkownik może zmieniać tylko swoje ordery (a nie czyjeś). Wtedy tak, taką zależność również bym wstrzyknął.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus:

Oczywiście, koniec końców musisz to zapisać. Ale dla mnie to żaden powód żeby bawić się w jakieś nadmierne zabezpieczanie wnętrza agregatu. Przecież repo nie ma prawa wykonywać modyfikacji agregatu, tylko ma go po prostu zapisać. Znów- jeśli coś takiego ma miejsce (modyfikowanie agregatu) to jest to błąd który należy wyłapywać na etapie code review. Ja po prostu nie jestem zwolennikiem usilnego próbowania zapobiec każdej możliwej sytuacji z dokładnością atomową, skoro nie wystawiamy agregatów jako jakiegoś frameworka. Nie tworzymy tego do opakowania i wystawienia do użytku przez wiele innych aplikacji, użytkowników itp. Agregaty to sprawa wewnętrzna, więc takie bawienie się w zabezpieczanie jakbyśmy projektowali framework to strata czasu dla mnie.

To jest w sumie pewnego rodzaju szczegół implenentacyjny i jakoś super nie wpłynie na jakość projektu tak sądzę .. także nie ma co tu debatować.

Zazwyczaj taki "serwis" domenowy to po prostu command handler, używany w sytuacji kiedy commenda nie może trafić bezpośrednio do agregatu (np. wspomniana wcześniej walidacja email). Innymi słowy zamiast mieć serwis z wieloma odpowiedzialnościami, przestrzrgam SRP mając proste mapowanie: command -> command handler. Wtedy taki handler zazwyczaj nie musi mieć dostępu do agregatu, a jeśli musi to pewnie wstrzyknął bym jakieś repo z którego handler by go załadował.

Tutaj nie bardzo rozumiem. Czyli w takim przypadku Twój command handler jest bardziej rozbudowany bo musi mieć wstrzyknięty jakiś port i przez to zawiera jakąś dodatkową logikę biznesową. Nie rozumiem, jednak zdania, że handler nie musi mieć dostępu do agregatu. Czy to jest tak, że w takich command handlerach będących też serwisami domenowymi masz po prostu dodatkową warstwę logiki walidacyjnej (tu unikalnośc maila), a potem po prostu jak jest ALL OK to przekazujesz komendę do agregatu jak w standardowym przypadku?

EDIT: Ok, widziałem, że kolega @nobody01 rozwiał wątpliwość. Czyli w standardowym przypadku to nie trafia do command handlera nawet, ok.

Jako że agregaty idealnie powinny emitować eventy do opisywania zmian jakie zaszły, ja używam event-driven archiecture, czasem z event sourcingiem. Wtedy coś takiego jak orkiestracja procesu (o ile w ogóle jakaś jest) zależy od tego jak zaimplementowana jest taka architektura- albo mam orkiestrację do czego używam process managers/sagas albo mam choreografię, czyli poszczególnie elementy reagują na eventy z innych części systemu (agregatów) bez centralnej kontroli. Do tego zazwyczaj mam jakieś event handlery które mapują event na commandy trafiające do odpowiednio agregatu- taki handler jest trochę jak endpoint mapujący, z tą różnicą że w tym przypadku mapujemy z eventu na command, zamiast z DTO na command,

Ok, mając choreografię masz o wiele mniej sprzężeń, ale mi właśnie chodzi o taką orkiestrację jak:

  1. Dostaję polecenie zupdatowania produktu
  2. Sprawdzam czy nazwa produktu jest unikalna (to mogę zrobić w obrębie tego samego modułu, bo mam dostęp do repo)
  3. Sprawdzam czy powiązania kampania charytatywna istnieje i jest aktywna (tu muszę wywołać coś w stylu campaignService.doesCampaignExistAndIsActive(id)
  4. Wyciągam Product z repo
  5. Wołam product.update(form);
  6. Zapisuję

Napisałeś:

Tak jak wyżej, od tego mam event-driven architecture i asynchroniczne wykonywanie procesów. Ale to samo mógłbyś wykonać synchronicznie, obsługując łańcuch eventów w pamięci procesu.

Nie bardzo wiem czy dobrze Cię rozumiem, ale chodzi Tobie o to, że Twój moduł produktów w tym przypadku nasłuchiwałby na eventy pochodzące z modułu kampanii i trzymał w swojej bazie danych ich kopie (w sumie mogą być nawet okrojone dane, które potrzebujemy na rzecz modułu produktów)?

Chyba, że chodziło Ci o coś innego? No bo musimy sprawdzić te 2 niezmienniki (unikalność nazwy produktu oraz aktywność kampanii) przed wykonaniem updatu produktu.

edytowany 1x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0

@Bambo:

To jest w sumie pewnego rodzaju szczegół implenentacyjny i jakoś super nie wpłynie na jakość projektu tak sądzę .. także nie ma co tu debatować.

Racja, odpisywałem bo Ty odpisywales ;)

Nie bardzo wiem czy dobrze Cię rozumiem, ale chodzi Tobie o to, że Twój moduł produktów w tym przypadku nasłuchiwałby na eventy pochodzące z modułu kampanii i trzymał w swojej bazie danych ich kopie (w sumie mogą być nawet okrojone dane, które potrzebujemy na rzecz modułu produktów)?

Chyba, że chodziło Ci o coś innego? No bo musimy sprawdzić te 2 niezmienniki (unikalność nazwy produktu oraz aktywność kampanii) przed wykonaniem updatu produktu.

Tutaj są dwa możliwe rozwiązania. Albo to co napisałeś, czyli Twój BC przechowuje kopię aktywnych kampanii, a kopia ta jest budowana nasłuchując eventów z innego BC (np. marketing BC). Druga opcją jest wpięcie tego w jakiś proces, czyli jest update produktu, na tej podstawie emitowany jest event obsługiwany w BC marketingu, tam dochodzi do sprawdzenia czy dana kampania istnieje a jeśli nie to publikowany jest kolejny event wychwytywany tam gdzie masz agregat order, i na tej podstawie anulowana/cofana jest operacja. To się nazywa compensating events i często się używa takiego podejścia w złożonych procesach.

Czy takie podejście miało by sens w przypadku który opisałeś? To zależy.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus: ok, w jednym projekcie gdzie mam architekturę eventową mam takie rozwiązania. Trzymam sobie kopię obiektów, które potrzebuję.

A co jeśli nie mamy architektury eventowej a i tak musimy sprawdzać takie rzeczy, o których napisałem wyżej? Wtedy musimy się jak odpytywać innych modułów czy mikroserwisow. I jak rozumiem to ma się dziać w warstwie serwisów domenowych? Poprzez adaptery najlepiej?

Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@Bambo: Ja tak zrobiłem kiedy miałem modularny monolit. Wszystko odbywało się w jednym procesie (programie) a więc odpytywanie z innych BC było proste. Tak jak piszesz można użyć adaptery lub wysyłać w pamięci commandy udostępniane przez dany BC innym BCs.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus:

...lub wysyłać w pamięci commandy udostępniane przez dany BC innym BCs.

Czyli takie emitowanie eventów ale w bieda wersji robione synchronicznie gdzie jawnie musisz powiadomić odbiorców o tym?

Hmm, ale jak teraz sobie myślę, że nieważne czy mamy opcję, że nasłuchujemy eventy innego BC i aktualizujemy sobie kopię innych obiektów (tutaj Kampanii) czy też nie mamy w ogóle eventów to kod będzie wyglądał tak samo, bo tak czy siak musimy odpytać port IsCampaignActiveTester - po prostu jego implementacja będzie różna, bo w przypadku eventowej architektury odpytamy jakieś repozytorium czy bardziej rejestr lokalnych kopii (nie wiem czy jest taki klocek w DDD, który nazywa obiekty służące wyciąganiu kopii jakiś obcych obiektów z innych BC ? ;p), a w przypadku apki 100% synchronicznej odpytamy albo inny BC albo inny moduł w ramach tego samego BC.

Czyli generanie serwis domenowy wygląda tak:

Kopiuj
public class UpdateItemDomainService {
    
    private final IsCampaignActiveTesterPort isCampaignActiveTesterPort;
    private final ItemRepository itemRepository;
    
    public long updateItem(UpdateItemForm form) {
        if(itemRepository.existsByName(form.getName())) {
            // exception
        }
        
        if(isCampaignActiveTesterPort.isActive(form.getSupportingCampaignId())) {
            // exception
        }
        
        final Item item = itemRepository.find(form.getId);
        item.update(form);
        final Item saved = itemRepository.save(item);
        return saved.getId();
    }
}

I teraz implementacje portu przy architekturze eventowej:

Kopiuj
public class LocalCampaignRegistry implements IsCampaignActiveTesterPort {
    
    private final CampaignRepo campaignRepo; 
    
    @Override
    public boolean isActive(long campaignId) {
        //
    }
}

oraz implementacja w przypadku modularnego monolitu. Najłatwiej to osiągnąć po prostu w taki sposób, że serwis z innej poddomeny implementuje nam port.

Kopiuj
public class CampaignService implements IsCampaignActiveTesterPort {

}
edytowany 2x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0
Bambo napisał(a):

Czyli takie emitowanie eventów ale w bieda wersji robione synchronicznie gdzie jawnie musisz powiadomić odbiorców o tym?

Nie nazwał bym tego tak, ale mniej więcej o coś takiego chodzi (chociaż zwróć uwagę że pisałem o commandach a nie eventach). Po prostu wykorzystanie faktu że skoro mamy wszystko w jednym procesie, to możemy to synchronicznie odpalić. Co za tym idzie zyskujemy to że nie narażamy się na pewne wady jakie niosą ze sobą procesy asynchroniczne.

Hmm, ale jak teraz sobie myślę, że nieważne czy mamy opcję, że nasłuchujemy eventy innego BC i aktualizujemy sobie kopię innych obiektów (tutaj Kampanii) czy też nie mamy w ogóle eventów to kod będzie wyglądał tak samo, bo tak czy siak musimy odpytać port IsCampaignActiveTester - po prostu jego implementacja będzie różna, bo w przypadku eventowej architektury odpytamy jakieś repozytorium czy bardziej rejestr lokalnych kopii (nie wiem czy jest taki klocek w DDD, który nazywa obiekty służące wyciąganiu kopii jakiś obcych obiektów z innych BC ? ;p), a w przypadku apki 100% synchronicznej odpytamy albo inny BC albo inny moduł w ramach tego samego BC.

Tak. Z własnego doświadczenia takie serwisy zazwyczaj nazywamy tak że na końcu dodajemy Lookup. Np. IActiveCampaignsLookup.

Czyli generanie serwis domenowy wygląda tak:
...

Tak, chociaż ja nie jestem zwolennikiem takiego nazewnictwa (port) w warstwach domenowych.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
N0
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:Gdańsk
  • Postów:647
1

@Aventus: mam jeszcze pytanie dotyczące granic transakcyjności agregatów. Powiedzmy, że mamy takie wymagania:

Zamówienie wymaga specjalnej akceptacji, jeśli zawiera co najmniej jeden produkt z ceną niższą niż koszty produkcji. Po dodaniu produktu z ceną niż koszty produkcji użytkownik może wysłać prośbę o akceptację ceny. Osoba z odpowiednimi uprawnieniami może tę prośbę zaakceptować lub odrzucić. Po zaakceptowaniu wszystkich takich próśb możliwe jest zaakceptowanie całego zamówienia.

Załóżmy, że mamy agregat Order oraz interfejs IProductProductionCostProvider do pobierania kosztów wytworzenia produktu. Widzę 2 opcje:

  1. Agregat Order zawiera listę produktów, których ceny wymagają akceptacji, powiedzmy productsRequiringAcceptance. Metody AddProduct, ChangePrice, RemoveProduct mają wstrzyknięty IProductProductionCostProvider. Każda z powyższych metod może modyfikować listę productsRequiringAcceptance. Przy zmianie statusu zamówienia na Accepted zachodzi sprawdzenie, czy lista productsRequiringAcceptance jest pusta. Order wystawia metodę służącą do zaakceptowania ceny konkretnego produktu (usunięcia go z productsRequiringAcceptance).

  2. Mam agregat PriceAcceptanceRequest z właściwościami OrderId, ProductId i Price. Możliwe statusy: NotSent, Sent, Accepted, Rejected. Agregat Order emituje eventy (ProductAdded, ProductPriceChanged, ProductRemoved), na podstawie których tworzone są lub modyfikowane instancje agregatu PriceAcceptanceRequest. Za wysłanie, zaakceptowanie lub odrzucenie prośby o zaakceptowanie ceny odpowiada agregat PriceAcceptanceRequest, a nie Order jak w punkcie 1). Przy zmianie statusu zamówienia na Accepted serwis domenowy sprawdza, czy nie ma żadnych PriceAcceptanceRequest, które uniemożliwiałyby zaakceptowanie zamówienia.

Nazewnictwo może nie być zbyt trafne, ale mam nadzieję, że wiadomo mniej więcej, o co chodzi. :)

Moje pytanie: czy w powyższym przypadku obsługa akceptacji cen produktów powinna być odpowiedzialnością agregatu Order (opcja 1)? Vaughn Vernon pisze, że powinniśmy dążyć do projektowania małych agregatów (m.in. z powodu kwestii związanych z wydajnością i wielowątkowością). Ale jeśli aplikacja nie ma wielu użytkowników i wydajność nie jest problemem, to czy na pewno opcja 2) z mniejszymi agregatami jest lepsza?

edytowany 3x, ostatnio: nobody01
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@nobody01:
Ciekawy przykład. W wolnej chwili sobie to zamodeluje. Wstępnie zrobiłbym osobne agregaty od zamówienia "bezpiecznego" i takiego z produktami z niższa cena. Agregaty te mogłyby w siebie nawzajem przechodzić i też miałyby specyficzne metody, bo np SafeOrder nie miałby metody accept()

N0
W sumie jestem ciekaw jakby to wyglądało :)
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@nobody01: ja bym na pewno nie trzymał w agregacie listy produktów wymagających akceptacji. To nie leży w gestii zamówień, i powinno być bezpośrednią właściwością produktu jeśli jest taka potrzeba.

Zgodnie z tymi wymaganiami które podałeś, po prostu wstrzykiwał bym jakiś lookup do sprawdzania kosztu produkcji/czy dany produkt wymaga akceptacji ceny. Na tym w zasadzie można skończyć, i użyć sposobu #1 który podałeś. Można by to rozbić tak jak podajesz w punkcie #2, ale pamiętajmy o podstawowej zasadzie którą wspomniałem wcześniej- granica transakcyjności. W tym przypadku zmiana lub odrzucenie ceny produktu ma bezpośredni wpływ na zamówienie, więc moim zdaniem zamówienie jak i akceptacja ceny są elementem tej samej transakcji/bytu biznesowego.

Ja bym pokusił się o coś pośredniego między 1 i 2- jeden agregat Order, ale również request akceptacji zmieniający stan agregatu Order tak jak podałeś w punkcie 2. Czyli proces mógł by wyglądać tak (pomijam usuwanie produktów):

  • AddProduct -> ProductAddedToOrder // Na tym etapie tworzymy agregat po raz pierwszy
  • ChangeProductPrice -> ProductPriceChanged // Tutaj stanu agregatu może zostać zmodyfikowany jeśli wykryjemy że cena wymaga akceptacji . "Blokujemy" możliwość skończenia zamówienia
  • RequestProductPriceAcceptance -> ProductPriceAcceptanceRequested// Użytkownik wysyła prośbę o akceptację ceny
  • AcceptProductPrice lub RejectProductPrice// Cena jest akceptowana lub nie, i to odpowiednio modyfikuje stan agregatu- jeśli więcej cen nie wymaga akceptacji, to "odblokowujemy" agregat
  • PlaceOrder -> OrderPlaced

Vernon pisał o tym aby dążyć do małych agregatów, ale zwróć uwagę w jakim kontekście to robił- jeśli dobrze pamiętam to jako przykład podawał zarządzanie projektem, taskami itp. Wtedy taki moloch to faktycznie duży agregat. W tym przypadku nadal mamy do czynienia ze stosunkowo małym agregatem Order moim zdaniem, jednocześnie zachowując odpowiednią granicę transakcyjności. "Rozmiar" agregatu to przede wszystkim to jak wiele obiektów/procesów angażuje, a nie to ile w nim mamy metod/kodu.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 3x, ostatnio: Aventus
N0
Dziękuję za tak szczegółowe wyjaśnienie ;)
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus
Wspominałeś, że właśnie z jakimiś polisami miałeś tak, że nie było 1 agregatu polisa tylko różne typy tych polisy z konkretnymi metodami.

Najprostszy przykład do zamodelowanie to drzwi. Zamiast agregatu Door z flagą boolean mamy agregaty OpenedDoor oraz ClosedDoor. Ten pierwszy ma metodę close(), która zwraca ClosedDoor I podobnie z metodę open(). Mamy jasno zamodelowanie koncepcję i nie da się nic zepsuć.

O tym mowil S.Sobotka i pisal Jarek R na forum tu.

Co o tym sądzisz?

Wtedy chyba też to jest tak że pobieramy agregat z jednego repo i ten drugi, który otrzymamy w wyniku operacji zapisujemy do drugiego repo ?

Zastanawiam sie tez juz nad sama implementacja. Czy powinny mieć to samo ID czy osobne. Czy też muszą mieć jakiś timestamp żeby sprawdzac który jest najnowszy .. albo status?

Pamiętasz jak to było u Ciebie ?

edytowany 7x, ostatnio: Bambo
Charles_Ray
Ja bym powiedział, że trzeba to tak zaprojektować, żeby było używalne, czyli drajwował to use casami. Jestem prawie pewny, że w zależności od procesu biznesowego albo jeden, albo drugi stan ma sens. Jedno repo czy 2 repa chyba nieważne i tak trzeba wiedzieć jaki typ chce się pobrać, żeby nie musieć robić instanceofów. Wzorzec stanu jest niezależny od DDD btw
Bambo
Jak mam spersystowane OpenedDoor i ClosedDoor to muszę jakoś w querysie sprawdzić które są te najświeższe. Jeśli mam to samo ID to mogę sprawdzić po jakimś timestamp albo technicznym obiekcie typu version. Ewentualnie jakiś jawny status trzymać w db jest mieć 1 tabele. Są tu jakieś sprawdzone i stosowane implementację?
Charles_Ray
Oczywiście, albo pole ze statusem, albo osobne tabele. Nie ma jedynego słusznego rozwiązania, jak w całym IT. Takie dywagacje bez kontekstu nie maja najmniejszego sensu.
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0

@Bambo: zacznę od końca- agregat to agregat, i powinien mieć jakieś unikalne ID (root agregatu). Dodatkowo oba agregaty mogą mieć jakieś ID używane do korelacji, czyli np. ID polisy lub ID drzwi. Teoretycznie mógłbyś oczywiście użyć tego samego ID jeśli trzymałbyś agregaty w oddzielnych tabelach w SQL- ale co z innymi przypadkami, np. bazami NoSQL? Są bazy w których ID rekordu musi być unikalne w całej bazie, a nie tylko w konkretnej kolekcji/tabeli/kontenerze.

Jeśli zaś chodzi o same repozytoria to w tamtym konkretnym przypadku używaliśmy event sourcingu a więc nie mieliśmy repozytoriów jako takich. W sensie że nie mieliśmy repo dla każdego agregatu. Była jakaś fabryka która tworzyła instancję agregatu po załadowaniu jego wszystkich eventów, ale to wszystko było bardzo generyczne- przekazywałeś stream ID eventów, typ agregatu jaki oczekujesz i tyle.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus analizowałem to co napisałeś. I racja, że OpenedDoor oraz ClosedDoor powinny mieć osobne ID. Pytanie tylko czy przy wywolaniu metody open/close I tworzeniu nowego stanu drzwi za każdym razem powinniśmy tworzyć agregat z nowym ID czy jednak z juz istniejącym.

Czyli np utworzyliśmy OpenedDoor z ID = jakiś UUID:OPENED I wywołujemy metode close() która tworzy ClosedDoor z ID = UUID:CLOSED.

Dzięki takiemu złożonemu ID z uuida I stanu możemy w db trzymać po 1 krotce w obu tabelach i nadpisywać. Z drugiej strony możemy nie chcieć nadpisywać tylko za każdym razem tworzyć nowy unikalny agregat I wtedy ID może składać się z UUID + nr wersji, która może służyć do tego ze wiemy, która operacja jest najświeższa i ewentualnie zabezpieczyć się żeby ktoś nie chciał otworzyć drzwi który zostały zamknięte 5 kroków temu.

Zastanawiam się po prostu która część dywagacji na temat ID I ich tworzenia leży po stronie domeny a która po stronie juz implementacji samych repo.

EDIT i mówię tu o klasycznym rozwiązaniu, nie typowym Event Sourcingu bo z tym to wiadomo

edytowany 2x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0

@Bambo: tylko tutaj problemem jest trywialny przykład. Różnica w stanie między otwartymi i zamkniętymi drzwiami jest tak mała, że wydzielanie tego do osobnych agregatów nie ma sensu. No bo czego taki agregat miałby pilnować? Jeśli drzwi są otwarte, to nie mogą być ponownie otwarte dopóki nie zostaną zamknięte. Tylko to najlepiej aby leżało w gestii agregatu Door, który właśnie pilnowałby tych zasad i mógł płynnie przełączać między stanami (otwarte/zamknięte).

W prawdziwym systemie którego przykład podałem mieliśmy oddzielne agregaty dla konkretnych transakcji na polisie (utworzenie, dokonanie zmiany, odnowa, anulowanie) ponieważ każda taka transakcja sama w sobie to był dosyć złożony proces w związku z tym każdy agregat miał swoje oddzielne zasady oraz był częścią większego, rozproszonego procesu. To nie było tak że np. agregat MidTermAdjustment miał tylko jedną metodę. Tam był szereg kroków do wykonania- najpierw sam request dokonania zmiany i sprawdzenie czy zmiana faktycznie może zostać dokonana, jeśli tak to opublikowanie eventu który informował o tym że taka zmiana się rozpoczyna, na podstawie tego inne elementy systemu (agregaty w innych mikroserwisach) musiały wykonać swoją pracę, i dopiero po jakimś czasie (mowa o minutach, ponieważ w między czasie angażowany był użytkownik i jego manualna interwencja) agregat dostawał kolejne polecenie potwierdzenia transakcji.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus: zgadza się, to banalny przykład, ale mi już teraz chodzi o same technikalia.
Załóżmy, że mamy Order w kilku stanach i nie chcemy tego robić po klasycznemu czyli jakiś status i w środku drabinka ifow. Jak dochodzi kolejny status to ifologie się mnożą. Przykład z mojej roboty. 15 metod, 10 pół w klasie, prawie zerowa spojnosc.
Wolimy mieć wyspecjalizowane agregaty typu OrderDraft itd. Mamy silna kohezje dzieki temu.

No i zastanawiam się nad technikaliami typu zarządzanie ID. Czy dla klienta każdy z tych różnych Orderów powinien mieć to samo ID czy już inne? Bo w klasycznym przykładzie mamy jeden Order z jednym ID, a tu mamy jakby wiele agregatow w jednym ekosystemie.

I pytanie czy klient (UI) powinien dostawać o odsyłać ID to ogólnie czy szczególne dla każdego z agregatów.

Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@Bambo: obawiam się że moja odpowiedzieć to nie może być nic innego jak "to zależy" ;) w zasadzie to już wcześniej Ci na to odpowiadałem- jeśli masz jakieś zamówienie to siłą rzeczy musi być jakieś ID które to wszystko spina (OrderId). To czy poszczególne agregaty związane z tym zamówieniem będą miały dodatkowo unikalne ID zależy od natury systemu. W moim przypadku było jedno PolicyId które spinało razem wszystkie agregaty związane z konkretną polisą, ale poszczególne agregaty miały swoje unikalne ID jako że każda taka transakcja musiała być unikalnie identyfikowana.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus: a jak już masz osobne agregaty reprezentujące polisę w konkretnych stanach I każdy ma swoje unikalne ID to jeśli przechodzisz ze stanu A do B (z agregatu A tworzysz agregat B) I potem z jakiegoś powodu z agregatu B tworzysz A to ten "drugi" A ma to samo ID czy nowe?

I w jaki sposób wyliczasz i pokazujesz na UI który "stan" jest najnowszy? Masz jakieś timestsmpy lub wersje?

Bo też trzeba się jakoś zabezpieczyć żeby nie wywołać akcji na "starym" agregacie no nie ?

EDIT:
Zastanawiałem się też czy jeśli masz polisę podzieloną na osobne agregaty to czy potem usługi restowe też konkretnie projektujesz pod te agregaty?
Bo w standardowym podejściu np z jednym Orderem będziemy mieć usługi np:
/orders/{id}/accept
/orders/{id}/confirm

I potem pod spodem ładujemy order i mamy ifologię na statusach.

W podejściu z rozbiciem tego na np OrderDraft oraz AcceptedOrder
możemy mieć usługi

/orderdrafts/{id}/accept
/acceptedorders/{id}/confirm

Pytanie czy nie pokazujemy tu dla UI za dużo szczegółów implementacji tego.

edytowany 1x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@Bambo:

a jak już masz osobne agregaty reprezentujące polisę w konkretnych stanach I każdy ma swoje unikalne ID to jeśli przechodzisz ze stanu A do B (z agregatu A tworzysz agregat B) I potem z jakiegoś powodu z agregatu B tworzysz A to ten "drugi" A ma to samo ID czy nowe?

W przypadku który ja opisywałem zawsze było generowane nowe ID ponieważ to zawsze był nowy proces biznesowy (transakcja).

...
Pytanie czy nie pokazujemy tu dla UI za dużo szczegółów implementacji tego.

Nigdy nie pracowałem przy ani prywatnie nie budowałem aplikacji gdzie agregaty były bezpośrednio używane na potrzeby widoku. Zawsze miałem oddzielny model widoku (zastosowanie CQRS) który był regularnie budowany na podstawie zmian zachodzących w systemie. Czyli np. dla polisy następowały eventy:

  • NewPolicyCreated -> utworzenie modelu widoku polisy dla konkretnego PolicyId i zapisanie go do bazy
  • PolicyMidTermAdjusted -> update rekordu z nowymi informacjami
  • PolicyCancelled -> tak jak powyżej

Tutaj trochę więcej o takim podejściu:
What is the CQRS pattern?
Materialized View pattern


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus:
A w przypadku, gdy dla każdego typu polisy generowałeś jakieś ID to jak potem wyliczałeś, która z tych wszystkich różnych polis jest tą najnowszą? No bo wyobrażam sobie, że te różne polisy przechodziły w siebie, czyli zmieniały stan i w tym momencie jakoś trzeba sprawdzić, która jest ta ostatnia.

Podobnie w jaki sposób się zabezpieczałeś, żeby nie wykonano żądania z jakimś poprzednim ID?

Załóżmy, że zapisałeś jeden z rodzajów polisy z ID=1, potem user wykonał jakieś akcje i zapisałeś tą samą polisę z ID=2. Jak teraz zabezpieczyć się przed tym, żeby user nie wykonał akcji na polisie z ID=1?

Hmm nie chodzi mi o reprezenetacje widoku, bardziej chodzi o mi to, że jak mam sobie agregat Order to zazwyczaj mam RESTy ze ścieżką /orders.
A co jeśli rozbijam swój Order na mniejsze agregaty typu DraftOrder, AcceptedOrder itd? Również powinienem zmienić RESTy na /draftorder, /acceptedorder order itd? Np jak chce wysłać żądanie akceptacji draftu oferty.

EDIT: Nie mogłem spać po yerbie i rozkminiałem kilka h w nocy strategię na zamodelowanie tych wielu stanów orderu.
Załóżmy, że mamy tak jak mówiłem DraftOrder, AcceptedOrder, ConfirmedOrder oraz doszedł nowy typ: VipOrder, który nie musi być akceptowany, żeby był potwierdzony.

W klasycznym podejściu z 1 uber agregatem Order, mamy :

  • wywołanie REST /orders/{id}/confirm
  • i metodę confirm w Order, która ma
    w środku :
Kopiuj
if(status == VIP || status == ACCEPTED) {
   status = CONFIRM;
}

W podejściu z rozbiciem na mniejsze agregaty jak pisałem wyżej musimy jakoś drajwować te statusy.
Moim pierwszym pomysłem było zrobienie RESTów w stylu:

/acceptedorders/{id}/confirm ORAZ /viporders/{id}/confirm

Wtedy front znając obecny status zamówienia, który sobie pobrał z jakiegoś query service wie jaki ma wywołać endpoint i po stronie backendu uruchamiamy konkretny usecase:
ConfirmAcceptedOrder lub ConfirmVipOrder.

Nie podoba mi się to jednak z tego względu, że frontowi mówimy o wewnętrznej implementacji domeny i o tym jakie mamy byty.

Jeszcze inna strategia to zostanie przy klasycznym REST:
/orders/{id}/confirm

Ale wtedy po stronie backendu musimy jakoś drajwować to, czy użyć AcceptedOrderRepo czy też VipOrderRepo.
Wiadomo, że w warstwie aplikacji mogę pobrać z pierwszego repo i jeśli nie znajdzie to pobrać z drugiego repo i dopiero rzucić wyjątek lub zwrócić error jak nie znajdzie, ale to takie dziwne mi się wydaje.

Może po prostu VipOrder oraz AcceptedOrder powinny być jednym agregatem skoro mają to samo zachowanie confirm i wtedy nie ma problemu.

No właśnie .. tu szukam poprawnych strategii.
Wzywam też @Charles_Ray jeśli ma czas i chęci bo zawsze coś sensownego powie ;p

Tak w ogóle to zacząłem się nad tym zastanawiać https://bottega.com.pl/nabor-otwarty-ddd-online
Był ktoś?

edytowany 10x, ostatnio: Bambo
Charles_Ray
Brandolini mówił, ze lepiej zacząć od większej liczby agregatów i potem ewentualnie scalać.
Bambo
Też tak sądzę. A znasz strategie drive'owania potem poszczególnych agregatów?
Bambo
Ale jak się mają niezmienniki do mojego przykładu z potwierdzaniem zamówienia?
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
2

@Bambo: nie zrozumiałeś do końca tego co napisałem. Polisa to był jeden byt logiczny, w związku z czym konkretna polisa miała ID, ale nie miała swojego agregatu. Agregaty takie jak NewPolicy, MidTermAdjustment, PolicyCancellation to nie polisy tylko agregaty składające się na jedną logiczną polisę. Każdy taki agregat miał swój unikalny ID, ale ID polisy pozostawało niezmienne.

Co do zabezpieczania się- każdy agregat był krótkotrwały w tym sensie, że np. NewPolicy miał zamknięty stan po utworzeniu (zakupieniu) polisy. Skoro polisa została już utworzona, to nie mogła zostać utworzona drugi raz bo agregat z konkretnym ID polisy już powstał, i jego stan nie pozwalał na ponowne rozpoczęcie procesu kupna.

A co jeśli rozbijam swój Order na mniejsze agregaty typu DraftOrder, AcceptedOrder itd? Również powinienem zmienić RESTy na /draftorder, /acceptedorder order itd? Np jak chce wysłać żądanie akceptacji draftu oferty.

My stosowaliśmy task-based API, i to się najlepiej moim zdaniem sprawdza przy zastosowaniu DDD. Poza tym to nie ma aż takiego znaczenia- to co po stronie domeny ma być oddzielone od strony web API, a jeśli przez przypadek API będzie odzwierciedlać warstwę domeny i będzie to miało sens to też nie problem. Myślę że niepotrzebnie aż tak się rozdrabniasz nad takimi problemami, tutaj nie ma dobrej i złej odpowiedzi. Rozwiązań jest wiele a które są dobre okazuje się dopiero w praktyce ;)


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@Aventus:
To ja się niejasno wyraziłem. Rozumiem, że polisa to logiczny byt, a Ty masz po prostu oddzielne agregaty, które reprezentują polisę w różnych jej etapach życia. Chodzi o 1 polisę, dlatego też te agregaty są spięte jakimś PolicyID.

Ok, czyli rozumiem, że do UI wysyłałeś ID agregatu a nie ID polisy tak?

Co do zabezpieczania się- każdy agregat był krótkotrwały w tym sensie, że np. NewPolicy miał zamknięty stan po utworzeniu (zakupieniu) polisy. Skoro polisa została już utworzona, to nie mogła zostać utworzona drugi raz bo agregat z konkretnym ID polisy już powstał, i jego stan nie pozwalał na ponowne rozpoczęcie procesu kupna.

Tu nie bardzo rozumiem jak to technicznie wyglądało. NewPolicy była tworzona przez jakąś fabrykę/serwis czy tam normalnie poprzez konstruktor tak? W takim razie z zewnątrz nadawałeś ID temu agregatowi skoro piszesz, że nie mógł zostać utworzony po raz kolejny? No bo gdzieś musiałeś mieć sprawdzenie, że NewPolicy z danym ID istnieje.

My stosowaliśmy task-based API,
Masz gdzieś do poczytania o tym?

Jeszcze wróce do eventów, o których pisałeś wcześniej. Mówisz, że też jest coś takiego jak eventy aplikacyjne/integracyjne. Czy event OrderConfirmed to zdarzenie domenowe czy integracyjne?

Bo to jest klasyk, który pcham na jakąś kolejkę typu Kafka czy RabbitMQ i inne mikroserwisy nasłuchują na to i niektóre robią jakąś logikę z płatnościami, niektóre updatują swoje lokalne kopie, a niektóre updatują swój read model.

Kiedy w takim razie korzysta się ze zdarzeń domenowych?

Dodatkowo pisałeś, że event domenowy z kolei zawiera tylko to co się zmieniło, a event aplikacyjny/integracyjny zawiera dużo danych, np całą reprezentację agregatu.
Trochę zaczęło mi się to teraz mieszać.

edytowany 2x, ostatnio: Bambo
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@Bambo:

Ok, czyli rozumiem, że do UI wysyłałeś ID agregatu a nie ID polisy tak?

W sumie to jedno i drugie.

Tu nie bardzo rozumiem jak to technicznie wyglądało. NewPolicy była tworzona przez jakąś fabrykę/serwis czy tam normalnie poprzez konstruktor tak? W takim razie z zewnątrz nadawałeś ID temu agregatowi skoro piszesz, że nie mógł zostać utworzony po raz kolejny? No bo gdzieś musiałeś mieć sprawdzenie, że NewPolicy z danym ID istnieje.

Zgadza się, ID transakcji było nadawane z zewnątrz.

My stosowaliśmy task-based API,
Masz gdzieś do poczytania o tym?

Task-based API nazwaliśmy po task-based UI, i pod tym hasłem chyba najlepiej szukać. Chodzi o to że endpoint'y nazywaliśmy przede wszystkim po funkcji jaką obsługiwał, a nie przestrzegając ściśle REST. Przykład:

/policies/start-new-policy
/policies/cancel-policy

ID były wysyłane w paylodzie requesta.

Jeszcze wróce do eventów, o których pisałeś wcześniej. Mówisz, że też jest coś takiego jak eventy aplikacyjne/integracyjne. Czy event OrderConfirmed to zdarzenie domenowe czy integracyjne?

To zależy :) Wszystko zależy od tego co tam przesyłasz i przez co ten event jest obsługiwany. U nas eventy integracyjne to takie które są obsługiwane przez systemy "zewnętrzne", tzn. takie które nie są bezpośrednio zaangażowane w architekturę opartą o DDD. Przykład z polisami- nasz wewnętrzny system od znajdowania cen polis u ubezpieczycieli. Taki "silnik ratingowy".


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
N0
  • Rejestracja:około 7 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:Gdańsk
  • Postów:647
0

@Aventus: kolejne pytanie :)

Powiedzmy, że mamy w systemie proces tworzenia zamówienia z oferty. Mamy agregaty Offer oraz Order. W procesie musimy zrobić dwie rzeczy:

  1. Utworzyć zamówienie i przenieść do niego dane produktów i ich ceny z oferty.
  2. Zmienić status oferty.

Zakładamy, że oferty i zamówienia są w tym samym bounded countext.

Pod wpływem tego wątku i filmiku od CodeOpinion takie rozwiązanie mi przyszło do głowy:

Kopiuj
class OfferCompletion
{
  public Offer Offer { get; }
  public Order Order { get; private set; }
  private readonly ISomeService _someService;
  
  public OfferCompletion(Offer offer, ISomeService someService) { ... }

  public void Complete()
  {
    // ...
    Offer.Complete();
    Order = Order.CreateFromOffer(Offer);
  }
}

class OfferCompletionRepository
{
  private readonly DbContext _context;

  public void Add(OfferCompletion offerCompletion)
  {
     // aktualizacja oferty automatycznie przy SaveChanges
    _context.Orders.Add(offerCompletion.Order); // dodawanie zamówienia
  }
}

Granicę transakcyjności procesu stanowi agregat OfferCompletion. Co sądzisz o takim podejściu? :)

edytowany 6x, ostatnio: nobody01
Aventus
  • Rejestracja:prawie 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

@nobody01: wydaje się to OK. Jeśli chodzi o ten filmik i ogólnie koncepcję oddzielenia stanu od "zachowania" agregatu to stosowałem to przy użyciu event sourcingu, ale wydaje mi się że idea może zostać również użyta bez ES. Mianowicie stan agregatu staje się generycznym atrybutem agregatu bazowego, i zależnie od implementacji jakoś zostaje wstrzyknięty/utworzony w trakcie tworzenia instancji agregatu. Taki stan jest wtedy zwykłym data bag na którym operuje agregat. Gdzieś, kiedyś nawet natknąłem się na przykład tego w jakimś artykule, niestety nie mogę go znaleźć :/ A szkoda, może takie podejście ma swoją nazwę teraz.

Kopiuj
abstract class AggregateRoot<TState>
{
  protected TState State { get; }
}

class OfferCompletion : AggregateRoot<OfferCompletionState>
{
    public void Complete()
    {
      // ...
      State.Offer.Complete();
      State.Order = Order.CreateFromOffer(Offer);
    }
}

Wtedy masz nie tylko logiczne oddzielnie stanu od zachowania, ale i na poziomie implementacji kodu- stan jest oddzielnym bytem wystawianym jako właściwość przez agregat bazowy.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)