Wann sollte ein Aggregate Root ein anderes AR enthalten (und wann sollte es nicht)

15

Lassen Sie mich zunächst für die Länge des Beitrags entschuldigen, aber ich wollte unbedingt möglichst viele Details im Vorfeld vermitteln, damit ich mir nicht die Zeit nehme, in Kommentaren hin und her zu gehen.

Ich entwerfe eine Anwendung nach einem DDD-Ansatz und frage mich, welche Anleitung ich befolgen kann, um zu bestimmen, ob eine aggregierte Root eine andere AR enthalten soll oder ob sie als separate, "freistehende" ARs belassen werden sollen.

Nehmen Sie den Fall einer einfachen Stechuhr-Anwendung, mit der Mitarbeiter sich für den Tag ein- oder ausstempeln können. Über die Benutzeroberfläche können sie ihre Mitarbeiter-ID und PIN eingeben, die dann validiert und der aktuelle Status des Mitarbeiters abgerufen werden. Wenn der Mitarbeiter gerade eingetaktet ist, wird auf der Benutzeroberfläche die Schaltfläche "Ausstempeln" angezeigt. und umgekehrt, wenn sie nicht eingetaktet sind, lautet die Schaltfläche "Eingetaktet". Die durch die Schaltfläche ausgeführte Aktion entspricht auch dem Status des Mitarbeiters.

Die Anwendung ist ein Webclient, der einen Back-End-Server aufruft, der über eine RESTful-Serviceschnittstelle verfügbar gemacht wird. Mein erster Versuch, intuitive, lesbare URLs zu erstellen, führte zu den folgenden zwei Endpunkten:

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

HINWEIS: Diese werden verwendet, nachdem die Mitarbeiter-ID und die PIN validiert wurden und ein "Token", das den "Benutzer" darstellt, in einem Header übergeben wurde. Dies liegt daran, dass es einen "Manager-Modus" gibt, mit dem ein Manager oder Vorgesetzter einen anderen Mitarbeiter ein- oder ausstempeln kann. Im Interesse dieser Diskussion versuche ich, es einfach zu halten.

Auf dem Server habe ich einen ApplicationService, der die API bereitstellt. Meine anfängliche Idee für die ClockIn-Methode ist wie folgt:

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

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

    employee.ClockIn();

    EmployeeRepository.Save();
}

Dies sieht ziemlich einfach aus, bis wir feststellen, dass die Zeitkarteninformationen des Mitarbeiters tatsächlich als Liste von Transaktionen gespeichert sind. Das heißt, jedes Mal, wenn ich ClockIn oder ClockOut aufrufe, ändere ich nicht direkt den Status des Mitarbeiters, sondern füge stattdessen einen neuen Eintrag in das Arbeitszeitblatt des Mitarbeiters ein. Der aktuelle Status des Mitarbeiters (eingetaktet oder nicht eingetaktet) wird aus dem letzten Eintrag im Arbeitszeitblatt abgeleitet.

Wenn ich also den oben gezeigten Code befolge, muss mein Repository erkennen, dass sich die dauerhaften Eigenschaften des Mitarbeiters nicht geändert haben, sondern dass ein neuer Eintrag zum Arbeitszeitblatt des Mitarbeiters hinzugefügt wurde, und eine Einfügung in den Datenspeicher vornehmen.

Auf der anderen Seite (und hier ist die ultimative Frage des Beitrags) scheint TimeSheet ein aggregierter Stamm zu sein, und es hat eine Identität (die Mitarbeiter-ID und die Periode), und ich könnte genauso einfach dieselbe Logik wie TimeSheet.ClockIn implementieren (Mitarbeiter-ID).

Ich diskutiere die Vorzüge der beiden Ansätze und frage mich, wie eingangs erwähnt, welche Kriterien ich bewerten sollte, um festzustellen, welcher Ansatz für das Problem besser geeignet ist.

SonOfPirate
quelle
Ich habe den Beitrag bearbeitet / aktualisiert, damit die Frage klarer wird und (hoffentlich) ein besseres Szenario verwendet.
SonOfPirate

Antworten:

4

Ich würde gerne einen Service zur Zeiterfassung implementieren:

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

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

Ich glaube nicht, dass es in der Verantwortung des Arbeitszeitblattes liegt, ein- oder auszusteigen, und auch nicht in der Verantwortung des Mitarbeiters.

CodeART
quelle
3

Aggregierte Wurzeln sollten sich nicht gegenseitig enthalten (obwohl sie möglicherweise IDs für andere enthalten).

Ist TimeSheet wirklich eine aggregierte Wurzel? Die Tatsache, dass es eine Identität hat, macht es zu einer Entität, nicht notwendigerweise zu einem AR. Schauen Sie sich eine der Regeln an:

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

Sie definieren die Identität des Arbeitszeitblatts als Mitarbeiter-ID und Zeitraum, was darauf hindeutet, dass das Arbeitszeitblatt Teil des Mitarbeiters ist und der Zeitraum die lokale Identität darstellt. Ist es der Hauptzweck von Employee, ein Container für TimeSheets zu sein, da dies eine Time Clock- Anwendung ist?

Vorausgesetzt, ARs, ClockIn und ClockOut ähneln eher TimeSheet-Operationen als Employee-Operationen. Ihre Service-Layer-Methode könnte etwa so aussehen:

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

Wenn Sie wirklich den Status "Eingestempelt" sowohl in Employee als auch in TimeSheet nachverfolgen müssen, werfen Sie einen Blick auf Domain-Ereignisse (ich glaube nicht, dass sie in Evans 'Buch enthalten sind, aber es sind zahlreiche Artikel online). Es würde ungefähr so ​​aussehen: Employee.ClockIn () löst das EmployeeClockedIn-Ereignis aus, das ein Ereignishandler aufnimmt und wiederum TimeSheet.LogEmployeeClockIn () aufruft.

Misko
quelle
Ich sehe deine Punkte. Es gibt jedoch bestimmte Regeln, die einschränken, ob und wann ein Mitarbeiter eingetaktet werden kann. Diese Regeln hängen hauptsächlich vom aktuellen Status des Mitarbeiters ab, z Aktuelle Schicht usw. Sollte das Arbeitszeitblatt über dieses Wissen verfügen?
SonOfPirate
3

Ich entwerfe eine Anwendung nach einem DDD-Ansatz und frage mich, welche Anleitung ich befolgen kann, um zu bestimmen, ob eine aggregierte Root eine andere AR enthalten soll oder ob sie als separate, "freistehende" ARs belassen werden sollen.

Eine aggregierte Wurzel sollte niemals eine andere aggregierte Wurzel enthalten.

In Ihrer Beschreibung haben Sie eine Entität Employee und ebenfalls eine Entität Timesheet. Diese beiden Entitäten sind unterschiedlich, können jedoch Verweise aufeinander enthalten (Beispiel: Dies ist Bobs Arbeitszeittabelle).

Das ist grundlegende Modellierung.

Die Gesamtwurzelfrage ist etwas anders. Wenn diese beiden Entitäten transaktionell voneinander verschieden sind, können sie korrekt als zwei verschiedene Aggregate modelliert werden. Mit Transaktionsunterscheidung meine ich, dass es keine Geschäftsinvariante gibt, für die es erforderlich ist, den aktuellen Status des Mitarbeiters und den aktuellen Status von TimeSheet gleichzeitig zu kennen.

Basierend auf Ihrer Beschreibung müssen Timesheet.clockIn und Timesheet.clockOut niemals Daten in Employee überprüfen, um festzustellen, ob der Befehl zulässig ist. Für so ein Großteil des Problems scheinen zwei verschiedene AR vernünftig zu sein.

Eine andere Möglichkeit, die AR-Grenzen zu berücksichtigen, wäre die Frage, welche Arten von Bearbeitungen gleichzeitig zulässig sind. Darf der Vorgesetzte den Mitarbeiter ausschalten, während die Personalabteilung gleichzeitig mit dem Profil des Mitarbeiters arbeitet?

Auf der anderen Seite (und hier ist die ultimative Frage des Beitrags) scheint TimeSheet eine aggregierte Wurzel zu sein und auch eine Identität zu haben (die Mitarbeiter-ID und die Periode).

Identität bedeutet nur, dass es sich um eine Entität handelt - es ist die Geschäftsinvariante, die entscheidet, ob sie als separate Gesamtwurzel betrachtet werden soll.

Es gibt jedoch bestimmte Regeln, die einschränken, ob und wann ein Mitarbeiter eingetaktet werden kann. Diese Regeln hängen hauptsächlich vom aktuellen Status des Mitarbeiters ab, z Aktuelle Schicht usw. Sollte das Arbeitszeitblatt über dieses Wissen verfügen?

Vielleicht - das Aggregat sollte. Das bedeutet nicht unbedingt, dass die Arbeitszeittabelle sollte.

Das heißt, wenn die Regeln zum Ändern einer Arbeitszeittabelle vom aktuellen Status der Entität Employee abhängen, müssen Employee und Timesheet auf jeden Fall Teil desselben Aggregats sein, und das Aggregat als Ganzes ist dafür verantwortlich, dass die Regeln eingehalten werden gefolgt.

Ein Aggregat hat eine Stammentität. es zu identifizieren ist Teil des Puzzles. Wenn ein Mitarbeiter mehr als eine Arbeitszeittabelle hat und beide Teil desselben Aggregats sind, ist die Arbeitszeittabelle definitiv nicht der Stamm. Das bedeutet, dass die Anwendung Befehle nicht direkt ändern oder an die Arbeitszeittabelle senden kann. Sie müssen an das Stammobjekt (vermutlich den Mitarbeiter) gesendet werden, das einen Teil der Verantwortung delegieren kann.

Eine weitere Prüfung wäre, zu prüfen, wie Arbeitszeittabellen erstellt werden. Wenn sie implizit erstellt werden, wenn sich ein Mitarbeiter einschaltet, ist dies ein weiterer Hinweis darauf, dass sie im Aggregat eine untergeordnete Entität sind.

Abgesehen davon ist es unwahrscheinlich, dass Ihre Aggregate über ein eigenes Zeitgefühl verfügen. Stattdessen sollte ihnen Zeit eingeräumt werden

employee.clockIn(when);
VoiceOfUnreason
quelle