Aggregate

Ein wichtiges in Schleupen.CS 3.0 angewendetes DDD-Muster sind Aggregate. Betrachten wir hierzu ein "klassisch" modelliertes Domänenmodell. Hierbei sind alle Klassen per Assoziation miteinander verknüpft. Um dem Problem zu entgehen, potenziell das gesamte Domänenmodell in den Hauptspeicher zu laden und die zugehörigen Klassen beliebig miteinander zu verweben, wird das Konzept des Aggregats verwendet. Hinzu kommt, dass in den Services nur ein Teil dieses Modells geladen wird, so dass bei Programmerweiterung ohne Aggregate schnell SELECT N+1-Probleme aufgrund von unbeabsichtigtem Lazy Loadings entstehen können. Entsprechend wurde in klassischen 2-Schicht-Architekturen festgestellt, dass die Verwendung eines OR-Mappers bei großen Modellen sehr zeitaufwendig und schlecht zu warten ist. Insbesondere Kollisionen beim Speichern sind hier problematisch. Neben diesen Problemen fehlt eine Zuordnung von Entitäten zu Business Entities auf der Makroebene. 

Ausgangspunkt: "Klassisches" objektorientiertes Modell

Als Lösung für diese Probleme dient das Aggregatmuster, das eine Strukturierung der Domäne in handhabbare Einheiten insbesondere bzgl. der Persistenz ist. Dabei ist ein Aggregat ein Cluster von Domänenobjekten die als Einheit behandelt werden: es werden explizite Grenzen definiert!

Schritt 1: Aufteilung des Modells in Einheiten

Diese Grenze ist eine Konsistenzgrenze und wird meistens durch Transaktionen definiert. Damit ist gemeint, dass das Aggregate zum Zeitpunkt des Schreibens in die Datenbank bzgl. der Geschäftsregeln valide, d.h. in sich konsistent ist.

Die Identifikation von Aggregaten ist dementsprechend wichtig und verdeutlicht das Konzept: Hierzu unterteilt man das Modell in die Bereiche des Modells, die zu einem Zeitpunkt gleichzeitig geändert oder angelegt werden. D.h. der Zeitpunkt ist neben der Kohäsion ein Kriterium für den Schnitt von Aggregaten.

In obigem Beispiel wird ein Benutzerkonto zu einem anderen Zeitpunkt angelegt wie eine Ausleihe oder ein Buch. Daher ist es sinnvoll eine Ausleihe als eigenes Aggregat von dem Benutzerkonto zu trennen. Ein Buch wird auch zunächst durch das System erfasst, und kann dann erst ausgeliehen werden. Somit erhalten wir obigen Schnitt der Aggregate. Dieser Schnitt sorgt dann dafür, dass weniger Kollisionen konkurrierender Bearbeitung entstehen können!

Wichtig bei der Modellierung ist, dass Aggregate möglichst klein sind, da ansonsten die Kollisionsgefahr steigt, das Ladeverhalten schlechter und die Konsistenz verwässert wird. Dieses Vorgehen der Nutzung des Aggregatkonzepts ist konform zum Konzept der Microservices, bei denen die Services zum einen eine klare Kontextgrenze besitzen und zum anderen Aggregatänderungen über Events propagiert werden.

Es gelten folgende Regeln für Aggregate:

  • Aggregate werden immer vollständig geladen!
  • Eine Aggregate Root bezeichnet die Klasse, die als Einstiegspunkt in das Aggregat dient.
  • Ein Aggregate hat globale Identität! Daher ist das Aggregate Root immer eine Entity.
  • Das Aggregate Root ist verantwortlich für die Prüfung von Invarianten, es stellt die Konsistenz sicher. Daher implementiert diese die Schleupen-Schnittstelle ISupportsValidation die in der Schleupen-Repository-Implementierung beim Persistieren zur Prüfung auf Konsistenz aufgerufen wird.
  • Assoziierte Objekte werden nur über das Aggregat Root ausgehändigt.
  • Nichts außerhalb der Aggregat-Grenze kann eine Referenz zu inneren Objekten halten.
  • Aggregate müssen überschneidungsfrei sein!

Typisierte Verknüpfung von Aggregaten

Verschiedene Aggregate werden im Code durch typisierte Ids anstelle von Objektreferenzen verbunden - die sogenannten EntityIds. Hierbei ist die Verwendung von typisierten Ids anstelle von Guids wichtig, um zum Einen eine gute Codenavigation zu erhalten und zum Anderen das Verwechseln von Ids zu vermeiden.

Dieses Vorgehen weicht von der reinen Objektorientierung ab, sorgt aber dafür, dass die Konsistenzgrenze einfach eingehalten wird. Zudem wird hierdurch die Persistenz einfacher und vermeidet Fehler aufgrund fehlender oder nicht verfügbarer Referenzen oder SELECT N+1-Problemen. 

Domänenmodell gemäß DDD

In obigem Beispiel der Klasse Ausleihe kann man die Implementierung der Aggregat-Verknüpfung mit der BuchId als EntityId sehen. Das folgende Beispiel zeigt die Implementierung der BuchId:

public class BuchId : EntityId
{ 
  public static BuchId New()
  {
    return new BuchId(Guid.NewGuid());
  }

  public static readonly BuchId Empty = new BuchId(Guid.Empty);

  public BuchId(Guid id)
    : base(id){ }
}

Aggregate sollten nicht über Fremdschlüssel in der Persistierung verknüpft werden! Denn ansonsten hat man eine Aggregat-übergreifende Kopplung auf Datenbank-Ebene, die insbesondere beim Testen sehr nachteilig ist (mehr Aufwand, unnötige Komplexität, etc.).

Listenklassen - Weiteres Muster zur Verschiebung von Code in die Domäne 

Ein weiteres Muster, welches in Schleupen.CS für eine bessere Code-Strukturierung und besser testbaren Code sorgt, sind die Aggregatlisten. Das Konzept ist sehr einfach: Für jedes Aggregat gibt es eine Klasse, die n Aggregate kapselt. Die Namensgebung sollte auch hier möglichst durch die Fachlichkeit gewählt werden, also eher Ausleihen anstelle von AusleiheCollection.

Diese Implementierung wird hinter einer Schnittstelle verborgen, also per Strategiemuster angebunden. Damit ist auch hier ein sehr gute Testbarkeit beispielsweise mit Mocks möglich.

Häufig werden Schleifen im Usecase Controller für das Iterieren über Domänenobjekte implementiert. Diese können so extrahiert werden, dass reiner Domänen-Code extrahiert und in die Domäne verschoben werden kann. 

Der folgende Code zeigt das Muster:

public partial class Ausleihen : IAusleihen
{
  private readonly List<Ausleihe> items = new();

  public Ausleihen(Ausleihe ausleihe)
  {
    if (ausleihe == null) { return; }
    items.Add(ausleihe);
  }

  public Ausleihen(IEnumerable<Ausleihe> ausleihen)
  {
    if (ausleihen == null) { return; }
    items.AddRange(ausleihen);
  }

  public void LeiheAus(IBuecher buecher, Benutzerkonto benutzerkonto)
  {
    if(buecher == null) { throw new ArgumentNullException(nameof(buecher)); }
    if(benutzerkonto == null) { throw new ArgumentNullException(nameof(benutzerkonto)); }

    foreach(Buch buch in buecher.SelectNichtAusgelieheneBuecher().Items)
    {
      Ausleihe neueAusleihe = CreateNeueAusleihe(buch,benutzerkonto);
      Add(neueAusleihe);
    }
  }
  ...
}
Cookie Consent mit Real Cookie Banner