Jak traktować sprawdzanie poprawności odniesień między agregacjami?

11

Mam trochę trudności z odniesieniami między agregacjami. Załóżmy, że agregat Carma odniesienie do agregatu Driver. Odniesienie to będzie modelowane poprzez Car.driverId.

Teraz moim problemem jest to, jak daleko mam się posunąć, aby sprawdzić poprawność tworzenia Caragregatu CarFactory. Czy powinienem ufać, że przekazane DriverIdodnosi się do istniejącego, Driver czy też powinienem sprawdzić ten niezmiennik?

Do sprawdzenia widzę dwie możliwości:

  • Mógłbym zmienić podpis fabryki samochodów, aby zaakceptować kompletną jednostkę kierowcy. Następnie fabryka po prostu wybierze identyfikator od tego bytu i zbuduje samochód. Tutaj niezmiennik jest sprawdzany domyślnie.
  • Mógłbym mieć odniesienie DriverRepositorydo CarFactoryi wyraźnie zadzwonić driverRepository.exists(driverId).

Ale teraz zastanawiam się, czy to nie tyle niezmiennego sprawdzania? Mogłem sobie wyobrazić, że te agregaty mogą żyć w osobnym ograniczonym kontekście, a teraz zanieczyściłbym BC samochodu zależnością od DriverRepository lub encji Driver BC kierowcy.

Ponadto, gdybym porozmawiał z ekspertami w dziedzinie, nigdy nie zakwestionowaliby ważności takich referencji. Wyczuwam, że zanieczyszczam mój model domeny niepowiązanymi obawami. Ale w pewnym momencie dane wejściowe użytkownika powinny zostać zatwierdzone.

Markus Malkusch
źródło

Odpowiedzi:

6

Mógłbym zmienić podpis fabryki samochodów, aby zaakceptować kompletną jednostkę kierowcy. Następnie fabryka po prostu wybierze identyfikator od tego bytu i zbuduje samochód. Tutaj niezmiennik jest sprawdzany domyślnie.

To podejście jest atrakcyjne, ponieważ otrzymujesz czek za darmo i jest dobrze dostosowane do wszechobecnego języka. A Carnie jest napędzany przez driverId, ale przez Driver.

Takie podejście jest w rzeczywistości stosowane przez Vaughna Vernona w kontekście ograniczonym do próbki tożsamości i dostępu, w którym przekazuje Useragregat do Groupagregatu, ale Groupjedyny zachowuje typ wartości GroupMember. Jak widać, pozwala mu to również sprawdzić, czy użytkownik jest włączony (jesteśmy świadomi, że czek może być nieaktualny).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

Jednak przekazując Driverinstancję, otwierasz się również na przypadkową modyfikację Driverwnętrza Car. Przekazywanie referencji wartości ułatwia rozumowanie zmian z punktu widzenia programisty, ale jednocześnie w DDD chodzi o język wszechobecny, więc być może warto zaryzykować.

Jeśli rzeczywiście możesz wymyślić dobre nazwiska, aby zastosować zasadę segregacji interfejsów (ISP) , możesz polegać na interfejsie, który nie ma metod behawioralnych. Być może można również wymyślić koncepcję obiektu wartości, która reprezentuje niezmienne odniesienie do sterownika i może być utworzona tylko z istniejącego sterownika (np DriverDescriptor driver = driver.descriptor().).

Mogłem sobie wyobrazić, że te agregaty mogą żyć w osobnym ograniczonym kontekście, a teraz zanieczyściłbym BC samochodu zależnością od DriverRepository lub encji Driver BC kierowcy.

Nie, nie zrobiłbyś tego. Zawsze istnieje warstwa antykorupcyjna, aby upewnić się, że koncepcje jednego kontekstu nie przenikną do innego. Jest to o wiele łatwiejsze, jeśli masz BC dedykowany stowarzyszeniom kierowców samochodów, ponieważ możesz modelować istniejące koncepcje, takie jak Cari Driverspecjalnie dla tego kontekstu.

Dlatego możesz mieć DriverLookupServicezdefiniowane w BC odpowiedzialne za zarządzanie stowarzyszeniami kierowców samochodów. Ta usługa może wywołać usługę internetową ujawnioną przez kontekst zarządzania sterownikami, który zwraca Driverinstancje, które najprawdopodobniej będą obiektami wartościowymi w tym kontekście.

Pamiętaj, że usługi sieciowe niekoniecznie są najlepszą metodą integracji między BC. Można również polegać na przesyłaniu wiadomości, gdzie na przykład UserCreatedwiadomość z kontekstu zarządzania sterownikami byłaby konsumowana w zdalnym kontekście, który przechowywałby reprezentację sterownika we własnej bazie danych. DriverLookupServiceMoże następnie użyć tego DB i dane kierowcy będą uaktualniane z kolejnych wiadomości (np DriverLicenceRevoked).

Nie mogę ci powiedzieć, które podejście jest lepsze dla twojej domeny, ale mam nadzieję, że da ci to wystarczającą wiedzę, aby podjąć decyzję.

plalx
źródło
3

Sposób, w jaki zadajesz pytanie (i proponujesz dwie alternatywy), jest tak, jakby jedyną obawą było to, że kierowca nadal jest ważny w momencie tworzenia samochodu.

Należy jednak obawiać się, że kierowca powiązany z kierowcą nie zostanie usunięty przed usunięciem samochodu lub przekazaniem innego kierowcy (i być może również, że kierowca nie jest przypisany do innego samochodu (jeśli domena ogranicza kierowcę tylko być powiązany z jednym samochodem)).

Sugeruję, że zamiast sprawdzania poprawności przydzielasz (co obejmuje sprawdzanie poprawności obecności). Następnie nie zezwalasz na usuwanie, gdy są jeszcze przydzielone, chroniąc w ten sposób przed wyścigiem nieaktualnych danych podczas budowy, a także przed innym problemem długoterminowym. (Należy pamiętać, że alokacja zarówno potwierdza, jak i przyznaje alokacje, i działa atomowo.)

Przy okazji, zgadzam się z @PriceJones, że powiązanie między samochodem a kierowcą jest prawdopodobnie odpowiedzialnością niezależną od samochodu lub kierowcy. Tego rodzaju powiązanie z czasem będzie się komplikować, ponieważ brzmi jak problem z planowaniem (kierowcy, samochody, przedziały czasowe / okna, zamienniki itp.). Nawet jeśli bardziej przypomina problem z rejestracją, można chcieć historycznie rejestracje, a także aktualne rejestracje. Dlatego może równie dobrze zasługiwać na własne BC.

Możesz podać schemat alokacji (taki jak wartość logiczna lub liczba referencyjna) w BC przypisanych agregatów lub w oddzielnym BC, powiedzmy, odpowiedzialnym za utworzenie powiązania między samochodem a kierowcą. Jeśli zrobisz to pierwsze, możesz zezwolić na (prawidłowe) operacje usuwania wydane dla samochodu lub kierowcy BC; jeśli zrobisz to drugie, będziesz musiał zapobiec usunięciu z BC samochodu i kierowcy, a zamiast tego wysłać je za pomocą harmonogramu skojarzenia samochodu i kierowcy.

Możesz również podzielić niektóre obowiązki związane z alokacją na BC w następujący sposób. Zarówno samochód, jak i kierowca zapewniają schemat „alokacji”, który zatwierdza i ustawia przydzielone wartości logiczne dla tego BC; gdy ich wartość logiczna przydziału jest ustawiona, BC zapobiega usuwaniu odpowiednich encji. (A system jest skonfigurowany w taki sposób, że BC samochodu i kierowcy zezwala tylko na przydział i dezalokację z harmonogramu BC skojarzenia samochodu / kierowcy)

Planowanie samochodu i kierowcy BC następnie utrzymuje kalendarz kierowców związanych z samochodem przez pewien czas / czas, teraz i w przyszłości, i powiadamia inne BC o zwolnieniu tylko przy ostatnim użyciu zaplanowanego samochodu lub kierowcy.


Jako bardziej radykalne rozwiązanie, możesz potraktować samochody BC i kierowców jako fabryki dołączające tylko historyczne dane, pozostawiając prawo własności do harmonogramu stowarzyszenia samochodów / kierowców. Samochód BC może wygenerować nowy samochód, wraz ze wszystkimi szczegółami samochodu, wraz z jego VIN. Właścicielem samochodu zajmuje się planista stowarzyszenia samochodów / kierowców. Nawet jeśli powiązanie samochód / kierowca zostanie usunięte, a sam samochód zostanie zniszczony, zapisy samochodu nadal istnieją w BC samochodu z definicji, a my możemy użyć BC samochodu do wyszukiwania danych historycznych; podczas gdy powiązania / własności samochodów / kierowców (przeszłość, teraźniejszość i potencjalnie przyszłość) są obsługiwane przez inny BC.

Erik Eidt
źródło
2

Załóżmy, że zagregowany samochód ma odniesienie do zagregowanego sterownika. Odniesienie to zostanie modelowane przy użyciu Car.driverId.

Tak, to właściwy sposób na połączenie jednego agregatu z drugim.

gdybym porozmawiał z ekspertami w dziedzinie, nigdy nie zakwestionowaliby ważności takich referencji

Niezupełnie właściwe pytanie do ekspertów z Twojej domeny. Spróbuj „jaki jest koszt firmy, jeśli sterownik nie istnieje?”

Prawdopodobnie nie użyłbym DriverRepository do sprawdzenia driverId. Zamiast tego użyłbym do tego usługi domeny. Myślę, że lepiej jest wyrazić zamiar - pod przykrywkami usługa domenowa nadal sprawdza system zapisu.

Coś w stylu

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

W rzeczywistości pytasz domenę o driverId w kilku różnych punktach

  • Od klienta przed wysłaniem polecenia
  • W aplikacji przed przekazaniem polecenia do modelu
  • W modelu domeny podczas przetwarzania poleceń

Każda lub wszystkie z tych kontroli mogą zmniejszyć liczbę błędów wprowadzanych przez użytkownika. Ale wszystkie działają na podstawie przestarzałych danych; drugi agregat może ulec zmianie natychmiast po zadaniu pytania. Dlatego zawsze istnieje niebezpieczeństwo fałszywych negatywów / pozytywów.

  • W raporcie wyjątków uruchom po zakończeniu polecenia

Tutaj nadal pracujesz z nieaktualnymi danymi (agregaty mogą uruchamiać polecenia podczas uruchamiania raportu, możesz nie być w stanie zobaczyć najnowszych zapisów do wszystkich agregatów). Ale kontrole między agregatami nigdy nie będą idealne (Car.create (driver: 7) działający w tym samym czasie co Driver.delete (driver: 7)). To daje dodatkową warstwę ochrony przed ryzykiem.

VoiceOfUnreason
źródło
1
Driver.deletenie powinno istnieć. Tak naprawdę nigdy nie widziałem domeny, w której agregaty ulegają zniszczeniu. Trzymając AR wokół siebie, nigdy nie skończysz z sierotami.
plalx
1

Warto zapytać: Czy jesteś pewien, że samochody są zbudowane z kierowcami? Nigdy nie słyszałem o samochodzie złożonym z kierowcy w prawdziwym świecie. Powodem, dla którego to pytanie jest ważne, jest to, że może on skierować Cię w kierunku samodzielnego tworzenia samochodów i kierowców, a następnie stworzenia zewnętrznego mechanizmu, który przypisuje kierowcę do samochodu. Samochód może istnieć bez odniesienia do kierowcy i nadal być ważnym samochodem.

Jeśli samochód musi bezwzględnie mieć kierowcę w twoim kontekście, możesz rozważyć wzorzec konstruktora. Ten wzór będzie odpowiedzialny za zapewnienie, że samochody będą budowane z istniejącymi kierowcami. Fabryki będą obsługiwały niezależnie sprawdzone samochody i kierowców, ale konstruktor zapewni, że samochód będzie miał potrzebne referencje, zanim zacznie on obsługiwać samochód.

Price Jones
źródło
Myślałem też o relacji samochód / kierowca - ale wprowadzenie agregatu DriverAssignment po prostu przesuwa, które odniesienie należy sprawdzić.
VoiceOfUnreason
1

Ale teraz zastanawiam się, czy to nie tyle niezmiennego sprawdzania?

Chyba tak. Pobieranie danego DriverId z DB zwraca pusty zestaw, jeśli nie istnieje. Sprawdzanie wyniku zwracania powoduje, że nie jest konieczne pytanie, czy istnieje (a następnie pobieranie).

Dzięki temu projektowanie klas czyni je również niepotrzebnymi

  • Jeśli istnieje wymóg „zaparkowany samochód może, ale nie musi mieć kierowcy”
  • Jeśli obiekt Driver wymaga a DriverIdi jest ustawiony w konstruktorze.
  • Jeśli Carpotrzebujesz tylko DriverId, mieć Driver.Idgetter. Bez setera.

Repozytorium nie jest miejscem dla reguł biznesowych

  • A Carto obchodzi, jeśli ma on Driver(lub jego identyfikator przynajmniej). A Driverto obchodzi, jeśli to ma DriverId. Do Repositorydba o integralność danych i nie obchodzi o samochodach kierowców mniej.
  • Baza danych będzie miała reguły integralności danych. Klucze inne niż null, ograniczenia nie-null itp. Ale integralność danych dotyczy schematu danych / tabeli, a nie reguł biznesowych. W tym przypadku mamy silnie skorelowany, symbiotyczny związek, ale nie mieszajcie tych dwóch.
  • Fakt, że a DriverIdjest domeną biznesową, jest obsługiwany w odpowiednich klasach.

Oddzielenie naruszenia zasad

... dzieje się, gdy Repository.DriverIdExists()zadaje pytanie.

Zbuduj obiekt domeny. Jeśli nie, Driverto może obiekt DriverInfo(tylko DriverIda Name, powiedzmy). DriverIdJest sprawdzany na budowie. Musi istnieć, być odpowiednim typem i cokolwiek innego. Następnie jest kwestią projektu klasy klienta, jak radzić sobie z nieistniejącym sterownikiem / driverId.

Może Carbez kierowcy nie ma problemu, dopóki nie zadzwonisz Car.Drive(). W takim przypadku Carobiekt oczywiście zapewnia swój własny stan. Nie można prowadzić bez Driver- no, jeszcze nie całkiem.

Oddzielanie własności od jej klasy jest złe

Jasne, Car.DriverIdjeśli chcesz. Ale powinno to wyglądać mniej więcej tak:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Nie to:

public class Car {
    public int DriverId {get; protected set;}
}

Teraz Carmuszą poradzić sobie ze wszystkimi DriverIdkwestiami dotyczącymi ważności - naruszenie zasady jednej odpowiedzialności; i prawdopodobnie nadmiarowy kod.

radarbob
źródło