Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Aggregatmengen

Es tritt oft das Problem auf, dass der Code im Usecase Controller, der die Dämonenlogik beinhaltet, unnötig komplex ist. Dies kann auf eine suboptimale Aufteilung zurückgeführt werden, bei der eine Verlagerung bestimmter Code-Teile in die eigentliche Dämonenlogik, entsprechend dem Prinzip der Separation of Concerns, vorteilhaft wäre. Eine solche Umstrukturierung würde nicht nur die Lesbarkeit erhöhen, sondern auch die Testbarkeit von Controller und Domänenlogik verbessern. Ein bewährtes Refactoring, das in diesem Kontext hilfreich sein kann, ist das Verschieben der Iteration von Entitäten vom Controller in die Domänenlogik.

Die Nutzung im Usecase Controller ändert sich durch dieses Muster von

public IEnumerable<Ausleihe> LeiheAus(string benutzerSid, IEnumerable<BuchId> buecherIds)
{
    IEnumerable<Buch> buecher = buchRepository.QueryById(buecherIds);
    foreach (Buch buch in buecher)
    {
        buch.MarkiereAlsAusgeliehen();
    }

    //...
}

zu

public AusleiheListe LeiheAus(string benutzerSid, IEnumerable<BuchId> buecherIds)
{
    IBuchListe buecher = buchRepository.QueryById(buecherIds);
    buecher.MarkiereAlsAusgeliehen();
    // ...
}

Diese verbessert die Les- und Testbarkeit.

Für Objekte im Hauptspeicher ist es bzgl. der Performance nicht signifikant nachteilig, für jede fachliche Operation einzeln zu iterieren. Also sollte man nicht in einer Schleife Validieren, Status setzen, Funktionalität ausführen, sondern hierfür drei Schleifen verwenden und diese in einer Listenklassen in je einer Operation ausprägen. Vgl. hierzu Refactoring: Improving the Design of Existing Code.

Design

Das Design ist im Grundprinzip trivial. Im Standardfall wird pro Aggregate-Root eine Listenklasse mit Namen entsprechend der Ubiquitous Language gewählt: Stornos, Marktmeldungen, BuchList, wobei stets das Aggregate-Root im Singular vorkommt.

Anstelle von IEnumerator<AggregateRoot>, AggregateRoot[] o.ä. wird der entsprechende Listentyp ersetzt: das Repository liefert diese Liste entsprechend zurück, der Assembler ebenfalls.

Die Klasse wird im Domain-Projekt angelegt.

Die Listenklasse implementiert keine IEnumerator<>-Schnittstelle o.ä., da diese in der Listenklasse selber erfolgen soll. Im Assembler kann dabei über IData-Schnittstelle auf den Inhalt zugegriffen werden.

Implementierung

Der Anschluss der Listenklasse wird zunächst gezeigt, um aufzuzeigen, wie der Code sich verschiebt. Im Usecase Controller erfolgt dies wie folgt - hierbei liefert das Repository den Listentyp - hier die Buecher - anstelle von IEnumerable<Buch> zurück:

Die Listen-Klassen sollte auch hier hinter einer Schnittstelle verborgen werden - das resultiert in flexibleren Code, insbesondere bei Unittests.

public AusleiheListe LeiheAus(string benutzerSid, IEnumerable<BuchId> buecherIds)
{
    IBuchListe buecher = buchRepository.QueryById(buecherIds);
    buecher.MarkiereAlsAusgeliehen();
    // ...
}

D.h., dass Code beispielsweise für das Iterieren von Bücher in die Listenklasse verschoben worden ist (siehe unten).

Das Repository hat entsprechend eine andere Schnittstelle als früher (wiederum nicht basierend auf IEnumerable<Buch>):

public interface IBuchRepository
{
    IBuecher QueryById(IEnumerable<Guid> buchIds);

    IBuecher Add(IBuchListe buecher);

    IBuecher Attach(IBuchListe buecher);

    // ...
}

Die Implementierung ist trivial - das Abfrageresultat wird einfach in den Konstruktor der Liste gegeben.

Die Methode MarkiereAlsAusgeliehen(), die im Controller angeschlossen ist, wird entsprechend in Buecher implementiert. Die Listenklasse kapselt sämtliches Iterieren etc.:

public partial class Buecher : IBuecher
{
    private readonly List<Buch> items = new List<Buch>();

    // ...

    public Buecher(IEnumerable<Buch> items)
    {
        this.items.AddRange(items);
    }

    public void MarkiereAlsAusgeliehen(IAusleiheListe ausleihen)
    {
        if (ausleihen == null)
        {
            return;
        }

        foreach (Ausleihe ausleihe in ausleihen.Items)
        {
            MarkiereAlsAusgeliehen(ausleihe.Buch);
        }
    }

    private void MarkiereAlsAusgeliehen(BuchId buchId)
    {
        Buch buch = Selektiere(buchId);
        buch?.MarkiereAlsAusgeliehen();
    }

    private Buch Selektiere(BuchId buchId)
    {
        return Items.First(x => x.Id == buchId.Id);
    }
}

Der Assembler assembliert ebenfalls anstelle von IEnumerable<Buch> zu IBuecher [Rückgabetyp in der Methode ToDomainObject()].

Abgrenzung

Dieses Muster ist keine Ersetzung von Domain Service (sind ungefähr so wie Usecase Controller) und Domain Events.

Stärken und Schwächen

Stärken
  • Ausdrucksstärke - es wird gefördert, dass zum einen ein fachlicher Begriff gemäß für den Typ verwendet wird (z.B. Buecher, Lastprofile) und zum anderen diese Funktionalität fachlich als Operation abgebildet wird
  • Testbarkeit - durch Verwendung einer Schnittstelle, kann hier ein Testdouble für das Listenobjekt verwendet werden
  • Einfachheit - die Anwendung des Muster ist recht einfach, da der Code lediglich an eine andere Stelle verschoben werden muss
    • Ggf. muss aber zunächst eine Schleife in zwei aufgeteilt werden
Schwächen

keine

Benutzte Muster

Cookie Consent mit Real Cookie Banner