Kiedy korzeń zagregowany powinien zawierać inny AR (a kiedy nie powinien)

15

Zacznę od przeprosin za długość posta, ale naprawdę chciałem przekazać z góry tyle szczegółów, aby nie tracić czasu na komentarze.

Projektuję aplikację zgodnie z podejściem DDD i zastanawiam się, jakie wskazówki mogę zastosować, aby ustalić, czy Root Aggregate powinien zawierać inny AR, czy też powinny być pozostawione jako osobne, „wolnostojące” AR.

Weźmy na przykład prostą aplikację Zegar czasu, która pozwala Pracownikom wejść lub wyjść na cały dzień. Interfejs użytkownika pozwala im wprowadzić identyfikator pracownika i kod PIN, który jest następnie sprawdzany i pobierany jest bieżący stan pracownika. Jeśli pracownik jest obecnie zalogowany, interfejs użytkownika wyświetla przycisk „Wyrejestruj”; i odwrotnie, jeśli nie są one taktowane, przycisk wyświetla „Clock In”. Działanie podejmowane przez przycisk odpowiada również stanowi pracownika.

Aplikacja jest klientem WWW, który wywołuje serwer zaplecza udostępniony przez interfejs usługi RESTful. Mój pierwszy krok w tworzeniu intuicyjnych, czytelnych adresów URL zaowocował następującymi dwoma punktami końcowymi:

http://myhost/employees/{id}/clockin
http://myhost/employees/{id}/clockout

UWAGA: Są one używane po sprawdzeniu poprawności ID pracownika i PIN-u, a „token” reprezentujący „użytkownika” jest przekazywany w nagłówku. Wynika to z faktu, że istnieje „tryb menedżera”, który pozwala menedżerowi lub przełożonemu zarejestrować lub wylogować innego pracownika. Ale ze względu na tę dyskusję staram się to uprościć.

Na serwerze mam usługę ApplicationService, która zapewnia interfejs API. Mój początkowy pomysł na metodę ClockIn jest mniej więcej taki:

public void ClockIn(String id)
{
    var employee = EmployeeRepository.FindById(id);

    if (employee == null) throw SomeException();

    employee.ClockIn();

    EmployeeRepository.Save();
}

Wygląda to dość prosto, dopóki nie uświadomimy sobie, że informacje o karcie czasu pracownika są faktycznie przechowywane jako lista transakcji. Oznacza to, że za każdym razem, gdy dzwonię do ClockIn lub ClockOut, nie zmieniam bezpośrednio stanu pracownika, ale dodajemy nowy wpis do arkusza czasu pracownika. Bieżący stan pracownika (zarejestrowany w zegarze lub nie) pochodzi z najnowszego wpisu w arkuszu czasu.

Tak więc, jeśli pójdę z kodem pokazanym powyżej, moje repozytorium musi rozpoznać, że trwałe właściwości pracownika nie uległy zmianie, ale nowy wpis został dodany do arkusza czasu pracownika i wykonał wstawienie do magazynu danych.

Z drugiej strony (i oto ostateczne pytanie dotyczące postu) TimeSheet wygląda na to, że jest to Root Aggregate, a także ma tożsamość (identyfikator pracownika i okres) i mógłbym równie łatwo zaimplementować tę samą logikę co TimeSheet.ClockIn (numer identyfikacyjny pracownika).

Dyskutuję o zaletach obu podejść i, jak stwierdzono w pierwszym akapicie, zastanawiam się, jakie kryteria powinienem ocenić, aby ustalić, które podejście jest bardziej odpowiednie dla problemu.

SonOfPirate
źródło
Zredagowałem / zaktualizowałem post, więc pytanie jest jaśniejsze i mam lepszy scenariusz (mam nadzieję).
SonOfPirate

Odpowiedzi:

4

Byłbym skłonny wdrożyć usługę śledzenia czasu:

public interface ITimeSheetTrackingService
{
   void TrackClockIn(Employee employee, Timesheet timesheet);

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

Nie sądzę, że obowiązkiem arkusza czasu jest wyrejestrowanie się lub wyrejestrowanie, ani nie jest to obowiązek pracownika.

CodeART
źródło
3

Zagregowane katalogi główne nie powinny się zawierać (chociaż mogą zawierać identyfikatory innych).

Po pierwsze, czy TimeSheet jest naprawdę zbiorczym katalogiem głównym? Fakt, że ma tożsamość, czyni go bytem, ​​niekoniecznie AR. Sprawdź jedną z zasad:

Root Entities have global identity.  Entities inside the boundary have local 
identity, unique only within the Aggregate.

Tożsamość arkusza czasu definiuje się jako identyfikator pracownika i okres czasu, co sugeruje, że arkusz czasu jest częścią pracownika, a okres czasu jest tożsamością lokalną. Ponieważ jest to aplikacja zegara czasu , czy głównym celem pracownika jest bycie kontenerem dla arkuszy czasu?

Zakładając, że musisz mieć AR, ClockIn i ClockOut bardziej przypominają operacje TimeSheet niż Pracowników. Metoda warstwy usługi może wyglądać mniej więcej tak:

public void ClockIn(String employeeId)
{
    var timeSheet = TimeSheetRepository.FindByEmployeeAndTime(employeeId, DateTime.Now);    
    timeSheet.ClockIn();    
    TimeSheetRepository.Save();
}

Jeśli naprawdę potrzebujesz śledzić stan wpisania się zarówno w Arkuszu Pracownika, jak i Arkuszu Czasu, spójrz na Wydarzenia Domeny (nie sądzę, że są one w książce Evansa, ale jest wiele artykułów online). Wygląda to mniej więcej tak: Employee.ClockIn () wywołuje zdarzenie EmployeeClockedIn, które program obsługi zdarzeń odbiera i z kolei wywołuje TimeSheet.LogEmployeeClockIn ().

Misko
źródło
Widzę twoje punkty. Istnieją jednak pewne reguły, które ograniczają to, czy i kiedy pracownik może zostać zarejestrowany. Reguły te są w większości zależne od aktualnego stanu pracownika, np. Czy są rozwiązani, już zarejestrowani, zaplanowani do pracy tego dnia, czy nawet aktualna zmiana itp. Czy arkusz czasu powinien posiadać tę wiedzę?
SonOfPirate
3

Projektuję aplikację zgodnie z podejściem DDD i zastanawiam się, jakie wskazówki mogę zastosować, aby ustalić, czy Root Aggregate powinien zawierać inny AR, czy też powinny być pozostawione jako osobne, „wolnostojące” AR.

Skumulowany katalog główny nigdy nie powinien zawierać innego zagregowanego katalogu głównego.

W swoim opisie masz encję pracownika , a także encję grafiku. Te dwie istoty są różne, ale mogą zawierać odniesienia do siebie (np. Jest to grafik Boba).

To tyle, co podstawowe modelowanie.

Zagregowane pytanie podstawowe jest nieco inne. Jeśli te dwa podmioty różnią się transakcyjnie od siebie, wówczas można je poprawnie modelować jako dwa odrębne agregaty. Przez odrębne transakcyjnie rozumiem, że nie ma niezmiennika biznesowego, który wymagałby jednoczesnej znajomości aktualnego stanu pracownika i bieżącego arkusza czasu.

Na podstawie tego, co opisujesz, Timesheet.clockIn i Timesheet.clockOut nigdy nie muszą sprawdzać żadnych danych w Pracowniku, aby ustalić, czy polecenie jest dozwolone. W związku z tym problemem są dwa różne AR.

Innym sposobem rozważenia granic AR byłoby pytanie, jakie zmiany mogą być wprowadzane jednocześnie. Czy kierownikowi wolno wyrejestrować pracownika, a jednocześnie HR bawi się jego profilem?

Z drugiej strony (i oto ostateczne pytanie dotyczące posta) TimeSheet wygląda na to, że jest to Root Aggregate, a także ma tożsamość (identyfikator pracownika i okres)

Tożsamość oznacza tylko, że jest bytem - niezmiennikiem biznesowym, który decyduje, czy należy ją traktować jako oddzielny zagregowany katalog główny.

Istnieją jednak pewne reguły, które ograniczają to, czy i kiedy pracownik może zostać zarejestrowany. Reguły te są w większości zależne od aktualnego stanu pracownika, np. Czy są rozwiązane, już zarejestrowane, zaplanowane do pracy tego dnia, czy nawet aktualna zmiana itp. Czy arkusz czasu powinien posiadać tę wiedzę?

Może - agregat powinien. Nie musi to koniecznie oznaczać, że grafik powinien.

Oznacza to, że jeśli zasady modyfikowania grafiku zależą od aktualnego stanu jednostki pracownika, wówczas pracownik i grafik zdecydowanie muszą należeć do tego samego agregatu, a agregat jako całość jest odpowiedzialny za zapewnienie, że reguły są śledził.

Agregat ma jedną jednostkę główną; zidentyfikowanie go jest częścią układanki. Jeśli pracownik ma więcej niż jeden grafik i oba są częścią tego samego agregatu, grafik zdecydowanie nie jest pierwiastkiem. Co oznacza, że ​​aplikacja nie może bezpośrednio modyfikować ani wysyłać poleceń do grafiku - należy je wysłać do obiektu głównego (przypuszczalnie pracownika), który może przekazać część odpowiedzialności.

Kolejnym sprawdzeniem byłoby rozważenie sposobu tworzenia grafików. Jeśli są tworzone niejawnie, gdy pracownik się zarejestruje, to kolejna wskazówka, że ​​są podrzędną jednostką w agregacie.

Nawiasem mówiąc, jest mało prawdopodobne, aby agregaty miały własne poczucie czasu. Zamiast tego należy im poświęcić czas

employee.clockIn(when);
VoiceOfUnreason
źródło