Quando una radice aggregata dovrebbe contenere un altro AR (e quando non dovrebbe)


15

Vorrei iniziare scusandomi prima per la lunghezza del post, ma volevo davvero trasmettere tutti i dettagli in anticipo, quindi non mi prendo il tuo tempo andando avanti e indietro nei commenti.

Sto progettando un'applicazione seguendo un approccio DDD e mi chiedo quali linee guida posso seguire per determinare se una radice aggregata dovrebbe contenere un altro AR o se debbano essere lasciati come AR separati "indipendenti".

Prendiamo il caso di una semplice applicazione Time Clock che consente ai Dipendenti di autenticarsi o meno per il giorno. L'interfaccia utente consente loro di inserire l'ID e il PIN dell'impiegato, che viene quindi convalidato e recuperato lo stato corrente dell'impiegato. Se il dipendente è attualmente connesso, l'interfaccia utente visualizza un pulsante "Clock Out"; e, al contrario, se non sono sincronizzati, il pulsante indica "Clock In". L'azione intrapresa dal pulsante corrisponde anche allo stato dell'impiegato.

L'applicazione è un client Web che chiama un server back-end esposto tramite un'interfaccia di servizio RESTful. Il mio primo passaggio alla creazione di URL intuitivi e leggibili ha portato ai seguenti due endpoint:

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

NOTA: vengono utilizzati dopo la convalida dell'ID e del PIN dell'impiegato e il passaggio di un "token" che rappresenta "utente" in un'intestazione. Questo perché esiste una "modalità manager" che consente a un manager o supervisore di accedere o disconnettere un altro dipendente. Ma, per il bene di questa discussione, sto cercando di renderlo semplice.

Sul server, ho un ApplicationService che fornisce l'API. La mia idea iniziale per il metodo ClockIn è qualcosa del tipo:

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

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

    employee.ClockIn();

    EmployeeRepository.Save();
}

Questo sembra piuttosto semplice fino a quando non ci rendiamo conto che le informazioni sulla time card del Dipendente vengono effettivamente mantenute come un elenco di transazioni. Ciò significa che ogni volta che chiamo ClockIn o ClockOut, non cambio direttamente lo stato del Dipendente ma, invece, aggiungo una nuova voce nel TimeSheet del Dipendente. Lo stato corrente del Dipendente (con o senza clock) deriva dalla voce più recente nel TimeSheet.

Quindi, se vado con il codice mostrato sopra, il mio repository deve riconoscere che le proprietà permanenti del Dipendente non sono cambiate ma che è stata aggiunta una nuova voce nel TimeSheet del Dipendente ed eseguire un inserimento nell'archivio dati.

D'altra parte (ed ecco l'ultima domanda del post), TimeSheet sembra che sia una radice aggregata oltre che abbia identità (ID impiegato e periodo) e potrei implementare altrettanto facilmente la stessa logica di TimeSheet.ClockIn (ID Dipendente).

Mi trovo a discutere i meriti dei due approcci e, come affermato nel paragrafo iniziale, mi chiedo quali criteri dovrei valutare per determinare quale approccio è più adatto al problema.


Ho modificato / aggiornato il post in modo che la domanda sia più chiara e utilizzi uno scenario migliore (si spera).
SonOfPirate,

Risposte:


4

Sarei propenso a implementare un servizio per il monitoraggio del tempo:

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

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

Non penso che sia responsabilità del foglio presenze di eseguire il clock in o out, né è responsabilità dei dipendenti.


3

Le radici aggregate non devono contenere l'un l'altro (sebbene possano contenere ID per gli altri).

Innanzitutto, TimeSheet è davvero una radice aggregata? Il fatto che abbia un'identità la rende un'entità, non necessariamente un'AR. Dai un'occhiata a una delle regole:

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

L'identità del TimeSheet viene definita come ID dipendente e periodo di tempo, il che suggerisce che TimeSheet fa parte del Dipendente con periodo di tempo come identità locale. Poiché si tratta di un'applicazione Time Clock , lo scopo principale di Employee è un contenitore per TimeSheets?

Supponendo che tu debba AR, ClockIn e ClockOut sembrano più operazioni TimeSheet che dipendenti. Il metodo del livello di servizio potrebbe essere simile al seguente:

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

Se hai davvero bisogno di tracciare lo stato di clock in Employee e TimeSheet, dai un'occhiata a Domain Events (non credo che siano nel libro di Evans, ma ci sono numerosi articoli online). Sembrerebbe qualcosa di simile: Employee.ClockIn () genera l'evento EmployeeClockedIn, che un gestore di eventi rileva e, a sua volta, chiama TimeSheet.LogEmployeeClockIn ().


Vedo i tuoi punti. Tuttavia, ci sono alcune regole che limitano se e quando un Dipendente può essere registrato. Queste regole sono principalmente guidate dallo stato corrente del Dipendente, ad esempio se sono terminate, già registrate, programmate per funzionare quel giorno o anche il turno corrente, ecc. Il TimeSheet dovrebbe possedere questa conoscenza?
SonOfPirate,

3

Sto progettando un'applicazione seguendo un approccio DDD e mi chiedo quali linee guida posso seguire per determinare se una radice aggregata dovrebbe contenere un altro AR o se debbano essere lasciati come AR separati "indipendenti".

Una radice aggregata non dovrebbe mai contenere un'altra radice aggregata.

Nella descrizione, hai un'entità Dipendente e allo stesso modo un'entità Scheda attività. Queste due entità sono distinte, ma potrebbero includere riferimenti reciproci (es: questa è la scheda attività di Bob).

Questo è il modello di base.

La domanda di radice aggregata è leggermente diversa. Se queste due entità sono distinte tra loro in modo transazionale, possono essere correttamente modellate come due aggregati distinti. Per transazione distinta, intendo dire che non vi sono invarianti aziendali che richiedono la conoscenza simultanea dello stato attuale del Dipendente e dello stato attuale di TimeSheet.

Sulla base di ciò che descrivi, Timesheet.clockIn e Timesheet.clockOut non devono mai controllare i dati in Employee per determinare se il comando è consentito. Quindi, per gran parte del problema, due AR diversi sembrano ragionevoli.

Un altro modo di considerare i limiti di AR sarebbe quello di chiedere quali tipi di modifiche possono avvenire contemporaneamente. Il manager è autorizzato a disconnettere il dipendente mentre allo stesso tempo le risorse umane stanno armeggiando con il profilo del dipendente?

D'altra parte (ed ecco l'ultima domanda del post), TimeSheet sembra essere una radice aggregata e ha un'identità (l'ID e il periodo dell'impiegato)

L'identità significa solo che è un'entità: è l'invariante del business che determina se debba essere considerata una radice aggregata separata.

Tuttavia, ci sono alcune regole che limitano se e quando un Dipendente può essere registrato. Queste regole sono per lo più guidate dallo stato corrente del Dipendente, ad esempio se sono terminate, già registrate, programmate per funzionare quel giorno o anche il turno corrente, ecc. Il TimeSheet dovrebbe possedere questa conoscenza?

Forse - l'aggregato dovrebbe. Ciò non significa necessariamente che il Timesheet dovrebbe.

Vale a dire, se le regole per modificare una scheda attività dipendono dallo stato corrente dell'entità Dipendente, allora Dipendente e scheda attività devono sicuramente far parte dello stesso aggregato e l'aggregato nel suo insieme è responsabile di garantire che le regole siano seguita.

Un aggregato ha un'entità radice; identificarlo fa parte del puzzle. Se un dipendente ha più di una scheda attività e fanno entrambe parte dello stesso aggregato, la scheda attività non è sicuramente la radice. Ciò significa che l'applicazione non può modificare o inviare direttamente i comandi alla scheda attività: devono essere inviati all'oggetto radice (presumibilmente il Dipendente), che può delegare parte della responsabilità.

Un altro controllo sarebbe considerare come vengono create le schede attività. Se vengono creati in modo implicito quando un dipendente accede, questo è un altro suggerimento che sono un'entità subordinata nell'aggregato.

A parte questo, è improbabile che i tuoi aggregati abbiano il loro senso del tempo. Invece, il tempo dovrebbe essere passato a loro

employee.clockIn(when);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.