Bestehende DDD-Aggregate

Persistierende DDD-Aggregate

1. Überblick

In diesem Tutorial werden wir die Möglichkeiten untersuchen,DDD Aggregatesmit verschiedenen Technologien beizubehalten.

2. Einführung in Aggregate

An aggregate is a group of business objects which always need to be consistent. Daher speichern und aktualisieren wir Aggregate als Ganzes innerhalb einer Transaktion.

Aggregat ist ein wichtiges taktisches Muster in DDD, das dazu beiträgt, die Konsistenz unserer Geschäftsobjekte aufrechtzuerhalten. Die Idee des Aggregats ist jedoch auch außerhalb des DDD-Kontexts nützlich.

Es gibt zahlreiche Geschäftsfälle, in denen sich dieses Muster als nützlich erweisen kann. As a rule of thumb, we should consider using aggregates when there are multiple objects changed as part of the same transaction.

Lassen Sie uns einen Blick darauf werfen, wie wir dies bei der Modellierung eines Bestellkaufs anwenden können.

2.1. Bestellungsbeispiel

Nehmen wir also an, wir möchten eine Bestellung modellieren:

class Order {
    private Collection orderLines;
    private Money totalCost;
    // ...
}
class OrderLine {
    private Product product;
    private int quantity;
    // ...
}
class Product {
    private Money price;
    // ...
}

These classes form a simple aggregate. SowohlorderLines als auchtotalCost Felder derOrder müssen immer konsistent sein, dhtotalCost sollte immer den Wert haben, der der Summe allerorderLines entspricht.

Now, we all might be tempted to turn all of these into fully-fledged Java Beans. Beachten Sie jedoch, dass die Einführung einfacher Getter und Setter inOrder die Kapselung unseres Modells leicht aufheben und geschäftliche Einschränkungen verletzen kann.

Mal sehen, was schief gehen könnte.

2.2. Naives Aggregat-Design

Stellen wir uns vor, was passieren könnte, wenn wir uns entschließen, allen Eigenschaften der KlasseOrder, einschließlichsetOrderTotal, naiv Getter und Setter hinzuzufügen.

Nichts hindert uns daran, den folgenden Code auszuführen:

Order order = new Order();
order.setOrderLines(Arrays.asList(orderLine0, orderLine1));
order.setTotalCost(Money.zero(CurrencyUnit.USD)); // this doesn't look good...

In diesem Code setzen wir die EigenschafttotalCostmanuell auf Null, was gegen eine wichtige Geschäftsregel verstößt. Auf jeden Fall sollten die Gesamtkosten nicht null Dollar betragen!

Wir brauchen einen Weg, um unsere Geschäftsregeln zu schützen. Schauen wir uns an, wie Aggregate Roots helfen kann.

2.3. Gesamtwurzel

Einaggregate root ist eine Klasse, die als Einstiegspunkt für unser Aggregat dient. All business operations should go through the root. Auf diese Weise kann die Aggregatwurzel dafür sorgen, dass das Aggregat in einem konsistenten Zustand bleibt.

The root is what takes cares of all our business invariants.

In unserem Beispiel ist die KlasseOrderder richtige Kandidat für die aggregierte Wurzel. Wir müssen nur einige Änderungen vornehmen, um sicherzustellen, dass das Aggregat immer konsistent ist:

class Order {
    private final List orderLines;
    private Money totalCost;

    Order(List orderLines) {
        checkNotNull(orderLines);
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one order line item");
        }
        this.orderLines = new ArrayList<>(orderLines);
        totalCost = calculateTotalCost();
    }

    void addLineItem(OrderLine orderLine) {
        checkNotNull(orderLine);
        orderLines.add(orderLine);
        totalCost = totalCost.plus(orderLine.cost());
    }

    void removeLineItem(int line) {
        OrderLine removedLine = orderLines.remove(line);
        totalCost = totalCost.minus(removedLine.cost());
    }

    Money totalCost() {
        return totalCost;
    }

    // ...
}

Die Verwendung einer aggregierten Wurzel ermöglicht es uns nun,Product undOrderLine einfacher in unveränderliche Objekte umzuwandeln, bei denen alle Eigenschaften endgültig sind.

Wie wir sehen können, ist dies ein ziemlich einfaches Aggregat.

Und wir hätten einfach jedes Mal die Gesamtkosten berechnen können, ohne ein Feld zu verwenden.

Im Moment sprechen wir jedoch nur von aggregierter Persistenz, nicht von aggregiertem Design. Bleiben Sie dran, diese Domain wird Ihnen in Kürze weiterhelfen.

Wie gut spielt dies mit Persistenztechnologien? Lass uns mal sehen. Ultimately, this will help us to choose the right persistence tool for our next project.

3. JPA und Hibernate

In diesem Abschnitt versuchen wir, unserOrder-Aggregat mithilfe von JPA und Hibernate beizubehalten. Wir verwenden Spring Boot undJPA Starter:


    org.springframework.boot
    spring-boot-starter-data-jpa

Für die meisten von uns scheint dies die natürlichste Wahl zu sein. Schließlich haben wir jahrelang mit relationalen Systemen gearbeitet und kennen alle gängige ORM-Frameworks.

Probably the biggest problem when working with ORM frameworks is the simplification of our model design. Es wird manchmal auch alsObject-relational impedance mismatch bezeichnet. Überlegen wir uns, was passieren würde, wenn wir das Aggregat vonOrderbeibehalten möchten:

@DisplayName("given order with two line items, when persist, then order is saved")
@Test
public void test() throws Exception {
    // given
    JpaOrder order = prepareTestOrderWithTwoLineItems();

    // when
    JpaOrder savedOrder = repository.save(order);

    // then
    JpaOrder foundOrder = repository.findById(savedOrder.getId())
      .get();
    assertThat(foundOrder.getOrderLines()).hasSize(2);
}

Zu diesem Zeitpunkt würde dieser Test eine Ausnahme auslösen:java.lang.IllegalArgumentException: Unknown entity: com.example.ddd.order.Order. Obviously, we’re missing some of the JPA requirements:

  1. Hinzufügen von Zuordnungsanmerkungen

  2. Die KlassenOrderLine undProductmüssen Entitäten oder@Embeddableein, keine einfachen Wertobjekte

  3. Fügen Sie für jede Entität oder@Embeddable-Klasse einen leeren Konstruktor hinzu

  4. Ersetzen Sie die Eigenschaften vonMoneydurch einfache Typen

Hmm, wir müssen das Design vonOrder Aggregat ändern, um JPA verwenden zu können. Während das Hinzufügen von Anmerkungen keine große Sache ist, können die anderen Anforderungen viele Probleme verursachen.

3.1. Änderungen an den Wertobjekten

Das erste Problem beim Versuch, ein Aggregat in JPA einzufügen, besteht darin, dass wir das Design unserer Wertobjekte unterbrechen müssen: Ihre Eigenschaften können nicht mehr endgültig sein, und wir müssen die Verkapselung unterbrechen.

We need to add artificial ids to the OrderLine and Product, even if these classes were never designed to have identifiers. Wir wollten, dass sie einfache Wertobjekte sind.

Es ist möglich, stattdessen die Anmerkungen@Embedded und@ElementCollection zu verwenden. Dieser Ansatz kann jedoch die Verwendung eines komplexen Objektgraphen erheblich erschweren (z. B.@Embeddable Objekt mit einer anderen@Embedded-Eigenschaft usw. .).

Wenn Sie die Annotation@Embeddedverwenden, werden der übergeordneten Tabelle einfach flache Eigenschaften hinzugefügt. Mit der Ausnahme, dass grundlegende Eigenschaften (z. vom TypString) erfordern immer noch eine Setter-Methode, die das gewünschte Wertobjektdesign verletzt.

Empty constructor requirement forces the value object properties to not be final anymore, breaking an important aspect of our original design. Um ehrlich zu sein, kann Hibernate den privaten Konstruktor no-args verwenden, der das Problem ein wenig mildert, aber noch lange nicht perfekt ist.

Selbst wenn ein privater Standardkonstruktor verwendet wird, können wir unsere Eigenschaften entweder nicht als endgültig markieren oder müssen sie mit Standardwerten (häufig null) innerhalb des Standardkonstruktors initialisieren.

Wenn wir jedoch vollständig JPA-konform sein möchten, müssen wir für den Standardkonstruktor mindestens eine geschützte Sichtbarkeit verwenden. Dies bedeutet, dass andere Klassen im selben Paket Wertobjekte erstellen können, ohne Werte für ihre Eigenschaften anzugeben.

3.2. Komplexe Typen

Leider können wir nicht erwarten, dass JPA komplexe Typen von Drittanbietern automatisch Tabellen zuordnet. Sehen Sie nur, wie viele Änderungen wir im vorherigen Abschnitt vornehmen mussten!

Wenn wir beispielsweise mit dem AggregatOrderarbeiten, werden wir Schwierigkeiten haben, die Felder vonJoda Moneybeizubehalten.

In einem solchen Fall schreiben wir möglicherweise benutzerdefinierte Typen@Converter, die in JPA 2.1 verfügbar sind. Dies erfordert jedoch möglicherweise zusätzliche Arbeit.

Alternativ können wir die EigenschaftMoneyauch in zwei grundlegende Eigenschaften aufteilen. Zum BeispielString für die Währungseinheit undBigDecimal für den tatsächlichen Wert.

Während wir die Implementierungsdetails ausblenden und dennoch die KlasseMoneyüber die API für öffentliche Methoden verwenden können, zeigt die Praxis, dass die meisten Entwickler die zusätzliche Arbeit nicht rechtfertigen können und das Modell einfach degenerieren würden, um stattdessen der JPA-Spezifikation zu entsprechen.

3.3. Fazit

Während JPA eine der am häufigsten verwendeten Spezifikationen der Welt ist, ist es möglicherweise nicht die beste Option, um das Aggregat vonOrderbeizubehalten.

Wenn unser Modell die tatsächlichen Geschäftsregeln widerspiegeln soll, sollten wir es so gestalten, dass es keine einfache 1: 1-Darstellung der zugrunde liegenden Tabellen darstellt.

Grundsätzlich haben wir hier drei Möglichkeiten:

  1. Erstellen Sie eine Reihe einfacher Datenklassen und verwenden Sie diese, um das umfangreiche Geschäftsmodell beizubehalten und neu zu erstellen. Leider kann dies viel zusätzliche Arbeit erfordern.

  2. Akzeptieren Sie die Einschränkungen von JPA und wählen Sie den richtigen Kompromiss.

  3. Betrachten Sie eine andere Technologie.

Die erste Option hat das größte Potenzial. In der Praxis werden die meisten Projekte mit der zweiten Option entwickelt.

Betrachten wir nun eine andere Technologie, um Aggregate zu erhalten.

4. Dokumentenspeicher

Ein Dokumentenspeicher ist eine alternative Methode zum Speichern von Daten. Anstatt Relationen und Tabellen zu verwenden, speichern wir ganze Objekte. This makes a document store a potentially perfect candidate for persisting aggregates.

Für die Anforderungen dieses Tutorials konzentrieren wir uns auf JSON-ähnliche Dokumente.

Schauen wir uns genauer an, wie unser Problem mit der Bestellpersistenz in einem Dokumentenspeicher wie MongoDB aussieht.

4.1. Persistierendes Aggregat mit MongoDB

Now, there are quite a few databases which can store JSON data, one of the popular being MongoDB. MongoDB speichert tatsächlich BSON oder JSON in binärer Form.

Dank MongoDB können wir das Beispielaggregatas-is vonOrderpeichern.

Bevor wir fortfahren, fügen wir den Starter des Spring BootMongoDBhinzu:


    org.springframework.boot
    spring-boot-starter-data-mongodb

Jetzt können wir einen ähnlichen Testfall wie im JPA-Beispiel ausführen, diesmal jedoch mit MongoDB:

@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")
@Test
void test() throws Exception {
    // given
    Order order = prepareTestOrderWithTwoLineItems();

    // when
    repo.save(order);

    // then
    List foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List foundOrderLines = foundOrders.iterator()
      .next()
      .getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}

What’s important – we didn’t change the original Order aggregate classes at all; no muss Standardkonstruktoren, Setter oder benutzerdefinierte Konverter für die KlasseMoneyerstellen.

Und hier ist, was unserOrderAggregat im Store erscheint:

{
  "_id": ObjectId("5bd8535c81c04529f54acd14"),
  "orderLines": [
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "10.00"
          }
        }
      },
      "quantity": 2
    },
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "5.00"
          }
        }
      },
      "quantity": 10
    }
  ],
  "totalCost": {
    "money": {
      "currency": {
        "code": "USD",
        "numericCode": 840,
        "decimalPlaces": 2
      },
      "amount": "70.00"
    }
  },
  "_class": "com.example.ddd.order.mongo.Order"
}

Dieses einfache BSON-Dokument enthält das gesamte Aggregat vonOrderin einem Stück, was gut zu unserer ursprünglichen Vorstellung passt, dass all dies gemeinsam konsistent sein sollte.

Beachten Sie, dass komplexe Objekte im BSON-Dokument einfach als Satz regulärer JSON-Eigenschaften serialisiert werden. Dank dessen können auch Klassen von Drittanbietern (wieJoda Money) problemlos serialisiert werden, ohne dass das Modell vereinfacht werden muss.

4.2. Fazit

Das Persistieren von Aggregaten mit MongoDB ist einfacher als mit JPA.

This absolutely doesn’t mean MongoDB is superior to traditional databases. Es gibt viele legitime Fälle, in denen wir nicht einmal versuchen sollten, unsere Klassen als Aggregate zu modellieren und stattdessen eine SQL-Datenbank zu verwenden.

Wenn wir jedoch eine Gruppe von Objekten identifiziert haben, die gemäß den komplexen Anforderungen immer konsistent sein sollten, kann die Verwendung eines Dokumentenspeichers eine sehr ansprechende Option sein.

5. Fazit

In DDD enthalten Aggregate normalerweise die komplexesten Objekte im System. Die Arbeit mit ihnen erfordert einen ganz anderen Ansatz als in den meisten CRUD-Anwendungen.

Die Verwendung gängiger ORM-Lösungen kann zu einem vereinfachten oder überlasteten Domänenmodell führen, das häufig keine komplexen Geschäftsregeln ausdrücken oder durchsetzen kann.

Dokumentenspeicher können es einfacher machen, Aggregate beizubehalten, ohne die Komplexität des Modells zu beeinträchtigen.

Der vollständige Quellcode aller Beispiele ist inover on GitHub verfügbar.