User Tools

Site Tools


technology:domainmodel:domainservice

Domain Service

Es wäre optimal, weil sehr simpel zum entwickeln, wenn die gesamte Business Logik unabhängig in den Aggregates implementiert würde. Man ruft einen Methode in einem Aggregate auf, alle Regeln werden geprüft, und das Aggregate nimmt dann die entsprechenden Änderungen vor.

Es gibt aber Szenarien in denen Business Regeln mehrere Aggregates betreffen. Sei es eine einfache Regel wie der Transfer von einem Bankkonto zu einem anderen - der Transfer darf nur statt finden wenn ein Konto be- und das andere entlastet wurde. Hierfür gibt es mehrere Möglichkeiten der Umsetzung. Entweder findet man ein neues Aggregate um die Regel sicher zu stellen, z.B. das KontoTransfer Aggregate. Dieses würde die Transaktionalität sicher stellen, indem es die Be- und Entlastung der beiden Konten in einer Änderung darstellen kann. Es müsste aber vorher die beiden Konten fragen, ob der Transfer erlaubt ist - ein Fall für Eventual Consistency, weil die Konten diese Fragen nur beantworten können mit Daten, die nicht zu 100% aktuell sein müssen1).

Oder man setzt einen Workflow als Saga um, der u.a. zusätzlich das Reservation Pattern verwenden kann, um einzelne Aktionen des Transfers rückgängig zu machen, falls der Transfer im Ganzen nicht rechtzeitig vollendet werden kann.

Wenn man alle mitwirkenden Aggregates im Zugriff hat, kann man den Transfer aber auch als einen Domain Service implementieren. Ein Domain Service ist ein erweiterter Transaktionsrahmen. Der Domain Service würde beide Konten laden und eines be- und das andere entlasten. Die Infastrukturschicht stellt dabei sicher, dass das alles innerhalb von einer Transaktion geschieht. Zudem ist ein Kontotransfer nur über den Domain Service möglich, d.h. die entsprechenden Methoden in den Konto Aggregates sind nicht öffentlich zugängig.

Beispiel Zeitdatensatz

Ein Zeitdatensatz hat u.a. einen Zeitraum, für den er gilt. Eine Regel besagt, dass sich keine zwei Zeitdatensätze überschneiden dürfen. Die meisten würden das lösen, indem sie die sofortige Einhaltung der Regel weg diskutieren würden2). Sie würden sagen, dass man das Read-Model nach einer Überschneidung fragen sollte, und wenn es keine gibt, den Zeitdatensatz neu terminieren. Da das nicht 100% sicher ist3), müsste man für eventuelle Überschneidungen kompensieren - und das würde sowieso nicht so oft vorkommen.

Nehmen wir aber mal an, die Regel, dass es keine Zeitüberschneidung geben darf, ist nicht weg zu diskutieren. Was tun? Nun, ein Zeitdatensatz als Aggregate hat nicht alle notwendigen Informationen um Zeitüberschneidungen mit anderen Zeitdatensätzen zu überprüfen und zu verhindern. Es fehlt also ein Konzept. Das Konzept ist der Terminkalender des Mitarbeiters. Einen Zeitdatensatz zu erfassen passiert somit in zwei Schritten. Erstens wird der Datensatz selbst erfasst, mit der Projektzuordnung, dem Status, der Notiz, usw. Dann wir der Zeitdatensatz im Terminkalender des Mitarbeiters eingetragen. Erst wenn beide Schritte erfolgreich durchgeführt wurden, wurde ein vollständiger und terminierter Zeitdatensatz erfasst.

In einer verteilten Umgebung kann man das entweder lösen, indem man die beiden Schritte 4) von einer SAGA durchführen lässt. Oder aber man führt sie einfach hinter einander aus - denn wenn der Zweite fehlschlägt, gibt es nichts im Terminkalender des Mitarbeiters, und man kann das Ergebnis des ersten Schrittes einfach ignorieren5).

Es gibt durchaus gute Gründe, verteilte Lösungen vorzubereiten. Wenn man eine Regel für alle Terminkalender aller Mitarbeiter hätte, wäre es denkbar, dass das Aggregate zu groß wird. Bei vielen6) Mitarbeitern könnte es sein, dass die Mitarbeiter und somit auch die Terminkalender der Mitarbeiter auf unterschiedliche Rechner aufgeteilt7) werden. Ein Mitarbeiterkalender wird aber nicht so groß werden, dass er partitioniert werden muss. Wenn ein Mitarbeiter 50 Jahre lange pro Tag 10 Zeitdatensätze erfasst, und das an 220 Tagen im Jahr, hat er 110.000 Zeitdatensätze erfasst. Im Terminkalender sind das 16 Byte8) für die ID des Mitarbeiters, 16 Byte9) für den referenzierten Zeitdatensatz, der im Kalender terminiert ist, 8 Byte10) für den Terminbeginn und 8 Byte für das Ende. Das sind also 48 Byte pro Datensatz, macht insgesamt 5.280.000 Byte = 5,0345 MB. 5 MB können locker im Speicher gehalten werden11), bzw. kann ein Snapshot des Terminkalenders mit einer Größe von 5MB in Sekundenbruchteil Schnelle geladen werden. Ich sehe also einen Terminkalender als geeignetes Aggregate12).

Wenn ich also einen neuen Zeitdatensatz anlegen möchte, würde ich einen Befehl abschicken, der in etwa wie folgt aussieht:

[DataContract]
public class AddServiceAction: Command
{
  [DataMember]
  public Guid Id { get; set; }
  [DataMember]
  public Guid EmployeeCalendarId { get; set; }
  [DataMember]
  public DateTime Start { get; set; }
  [DataMember]
  public DateTime End { get; set; }
 
  public AddServiceAction(Guid id, Guid employeeCalendarId, DateTime start, DateTime end)
  {
    Id = id;
    EmployeeCalendarId = employeeCalendarId;
    Start = start;
    End = end;
  }
}

Der Command Handler des Befehls macht dann in etwa folgendes:

  public void Handle(AddServiceAction command)
  {
    var action_repo = new Repository<ServiceAction>();
    var calendar_repo = new Repository<EmployeeCalendar>();
    var action = ServiceActionFactory.Create(command.ID); // Die Zeiten haben hier nichts zu suchen - oder doch? Für Prüfungen?
    var calendar = calendar_repo.Get(command.EmployeeCalendarId);
    if (calendar == null) calendar = new EmployeeCalendar(command.EmployeeCalendarId); // oder Repository.GetOrCreate()?
    calendar.Schedule(command.ID, command.Start, command.End);
    action_repo.Save(action);
    calendar_repo.Save(calendar);
  }

Der Command Handler agiert als Domain Service. Wir gehen davon aus, dass der Terminkalender und alle Termine des Mitarbeiters immer zusammen liegen werden, und daher auch transaktional in einer UOW13) gespeichert werden können. Es wird nur ein Befehl an den Applicationsdienst gesendet, das Ergebnis ist immer ein terminierter Zeitdatensatz, bzw. eine Fehlermeldung wenn was schief läuft14).

Auch für eine Umterminierung wird ein Domain Service benötigt. Folgendes wäre möglich

  public void Handle(RescheduleServiceAction command)
  {
    var calendar_repo = new Repository<EmployeeCalendar>();
    var calendar = calendar_repo.Get(command.EmployeeCalendarId);
    calendar.Reschedule(command.ID, command.Start, command.End);
    calendar_repo.Save(calendar);
  }

Das Problem ist, dass der Status des Zeitdatensatzes geprüft werden muss. Wenn er nicht editierbar ist, sollte der Zeitdatensatz im Kalender auch nicht verschoben werden können. Das Read Model des Zeitdatensatzes zu prüfen reicht auch nicht aus, weil das keine Sicherheit bringt. Angenommen ein Zeitdatensatz ist editierbar und läuft von 08:00-10:00. User 1 setzt ihn auf nicht editierbar, User 2 ändert die Zeit auf 09:00-11:00. Das betrifft zwei unterschiedliche Aggregates, im ersten Fall die ServiceAction, im Zweiten den EmployeeCalendar. Ohne Domain Service würde beide Aktionen erfolgreich durch laufen. Das ist nicht was User 1 wollte15), und auch nicht was User 2 wollte16).

Alle Aktionen den Terminkalender des Mitarbeiters betreffend17) müssen also parallel den Zeitdatensatz betreffen. Im Grunde müssen alle Aktionen den Zeitdatensatz durchlaufen, und der Terminkalender sollte intern vom Zeitdatensatz verwendet werden - und zwar nur bei Bedarf.

Dieser Fall sieht also nicht so aus, als wäre ein Domain Service dafür am geeignetsten. Dann doch lieber eine Saga, die die gemeinsamen Änderungen am Zeitdatensatz und am Terminkalender mit dem Reservation Pattern vornimmt. Wenn man aber davon ausgehen kann, dass die Zeitdatensätze und der Terminkalender des Mitarbeiters nicht partitioniert werden, ist das meiner Meinung nach ein eindeutiger Fall für einen Sekundärindex.

1) Der Grund ist der, dass die Konten Fragen beantworten und somit keine Änderungen vornehmen, und somit nicht für den Zeitraum der Fragen gesperrt sind. Wenn die Konten nicht mal im Zugriff sind, müssen sie Remote gefragt werden, was die Aktualität der Daten noch weiter verringert - dran denken, das Aggregate Muster bündelt von Natur aus einen Methodenaufruf in eine Transaktion. Wir verwenden keine Verteilten Transaktionen.
2) das ist in den DDDCQRS Newsgroups manchmal echt frustrierend, dass alle komplexen Fragen einfach weg gewischt werden mit dem Argument, dass man set-based-rules immer nur schließlich konsistent lösen kann
3) weil das Read-Model nicht 100% und sofort konsistent sein muss
4) mit dem Reservation Pattern
5) nicht schön, aber im Grunde völlig OK
6) zig tausenden oder gar millionen
7) partitioniert
8) , 9) für eine Guid
10) DateTime
11) in 50 Jahren sicherlich auch ;-)
12) zudem könnte man es abweichend von DDD anders speichern, so dass man für Zeitüberlappungen nicht den gesamten Kalender laden muss, sondern SQL Abfragen verwenden kann. Oder man archiviert alle N Jahre Daten. So oder so gibt es da genügend Möglichkeiten, das Systemverhalten zu optimieren
13) Unit of Work
14) z.B. eine Zeitüberschneidung im Kalender
15) er wollte den Zeitdatensatz von 08:00-10:00 sperren
16) er hat jetzt einen gesperrten Zeitdatensatz von 09:00-11:00, kann ihn nicht weiter verarbeiten, und hat ihn nicht gesperrt
17) Hinzufügen, Verschieben, Löschen
technology/domainmodel/domainservice.txt · Last modified: 2013/01/28 12:11 by rtavassoli