DDD-Aggregate und @DomainEvents

DDD-Aggregate und @DomainEvents

1. Überblick

In diesem Lernprogramm wird erläutert, wie Sie die Annotation@DomainEventsund die KlasseAbstractAggregateRootverwenden, um Domänenereignisse, die durch Aggregate erzeugt werden, bequem zu veröffentlichen und zu verarbeiten - eines der wichtigsten taktischen Entwurfsmuster im domänengesteuerten Entwurf.

Aggregates accept business commands, which usually results in producing an event related to the business domain – the Domain Event.

Wenn Sie mehr über DDD und Aggregate erfahren möchten, beginnen Sie am besten mit Eric Evans 'original book. Es gibt auch großeseries about effective aggregate designvon Vaughn Vernon. Auf jeden Fall lesenswert.

Es kann mühsam sein, manuell mit Domänenereignissen zu arbeiten. Zum Glück verwendenSpring Framework allows us to easily publish and handle domain events when working with aggregate rootsDatenrepositorys.

2. Maven-Abhängigkeiten

Spring Data führte@DomainEvents in Ingalls Release Train ein. Es ist für jede Art von Repository verfügbar.

Für diesen Artikel bereitgestellte Codebeispiele verwenden Spring Data JPA. The simplest way to integrate Spring domain events with our project is to use the Spring Boot Data JPA Starter:


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

3. Ereignisse manuell veröffentlichen

Versuchen wir zunächst, Domänenereignisse manuell zu veröffentlichen. Wir werden die Verwendung von@DomainEventsim nächsten Abschnitt erläutern.

Für die Anforderungen dieses Artikels verwenden wir eine leere Markierungsklasse für Domänenereignisse -DomainEvent.

Wir werden die Standardschnittstelle vonApplicationEventPublisherverwenden.

There’re two good places where we can publish events: service layer or directly inside the aggregate.

3.1. Service-Schicht

We can simply publish events after calling the repository save method inside a service method.

Wenn eine Servicemethode Teil einer Transaktion ist und wir die Ereignisse im Listener behandeln, die mit@TransactionalEventListener versehen sind, werden Ereignisse erst behandelt, nachdem die Transaktion erfolgreich festgeschrieben wurde.

Daher besteht kein Risiko, dass "gefälschte" Ereignisse behandelt werden, wenn die Transaktion zurückgesetzt wird und das Aggregat nicht aktualisiert wird:

@Service
public class DomainService {

    // ...
    @Transactional
    public void serviceDomainOperation(long entityId) {
        repository.findById(entityId)
            .ifPresent(entity -> {
                entity.domainOperation();
                repository.save(entity);
                eventPublisher.publishEvent(new DomainEvent());
            });
    }
}

Hier ist ein Test, der beweist, dass Ereignisse tatsächlich von serviceDomainOperation veröffentlicht werden:

@DisplayName("given existing aggregate,"
    + " when do domain operation on service,"
    + " then domain event is published")
@Test
void serviceEventsTest() {
    Aggregate existingDomainEntity = new Aggregate(1, eventPublisher);
    repository.save(existingDomainEntity);

    // when
    domainService.serviceDomainOperation(existingDomainEntity.getId());

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

3.2. Aggregat

We can also publish events directly from within the aggregate.

Auf diese Weise verwalten wir die Erstellung von Domain-Ereignissen innerhalb der Klasse, die sich diesbezüglich natürlicher anfühlen:

@Entity
class Aggregate {
    // ...
    void domainOperation() {
        // some business logic
        if (eventPublisher != null) {
            eventPublisher.publishEvent(new DomainEvent());
        }
    }
}

Leider funktioniert dies möglicherweise nicht wie erwartet, da Spring Data Entitäten aus Repositorys initialisiert.

Hier ist der entsprechende Test, der das tatsächliche Verhalten zeigt:

@DisplayName("given existing aggregate,"
    + " when do domain operation directly on aggregate,"
    + " then domain event is NOT published")
@Test
void aggregateEventsTest() {
    Aggregate existingDomainEntity = new Aggregate(0, eventPublisher);
    repository.save(existingDomainEntity);

    // when
    repository.findById(existingDomainEntity.getId())
      .get()
      .domainOperation();

    // then
    verifyZeroInteractions(eventHandler);
}

Wie wir sehen können, wird die Veranstaltung überhaupt nicht veröffentlicht. Abhängigkeiten innerhalb des Aggregats sind möglicherweise keine gute Idee. In diesem Beispiel wirdApplicationEventPublisher nicht automatisch von Spring Data initialisiert.

Das Aggregat wird durch Aufrufen des Standardkonstruktors erstellt. Damit es sich wie erwartet verhält, müssen wir Entitäten manuell neu erstellen (z. mit benutzerdefinierten Fabriken oder Aspektprogrammierung).

Außerdem sollten Sie vermeiden, Ereignisse unmittelbar nach Abschluss der Aggregatmethode zu veröffentlichen. Zumindest, wenn wir nicht zu 100{}icher sind, dass diese Methode Teil einer Transaktion ist. Andernfalls könnten „falsche“ Ereignisse veröffentlicht werden, wenn die Änderung noch nicht fortbesteht. Dies kann zu Inkonsistenzen im System führen.

Wenn wir dies vermeiden wollen, müssen wir daran denken, immer Aggregatmethoden innerhalb einer Transaktion aufzurufen. Leider koppeln wir auf diese Weise unser Design stark an die Persistenztechnologie. Wir müssen uns daran erinnern, dass wir nicht immer mit Transaktionssystemen arbeiten.

Therefore, it’s generally a better idea to let our aggregate simply manage a collection of domain events and return them when it’s about to get persisted.

Im nächsten Abschnitt wird erläutert, wie Sie die Veröffentlichung von Domänenereignissen mithilfe von @ DomainEvents- und@AfterDomainEvents-Anmerkungen einfacher verwalten können.

4. Veröffentlichen Sie Ereignisse mit @DomainEvents

Since Spring Data Ingalls release train we can use the @DomainEvents annotation to automatically publish domain events.

Eine mit@DomainEvents versehene Methode wird von Spring Data automatisch aufgerufen, wenn eine Entität mit dem richtigen Repository gespeichert wird.

Anschließend werden von dieser Methode zurückgegebene Ereignisse über dieApplicationEventPublisher-Schnittstelle veröffentlicht:

@Entity
public class Aggregate2 {

    @Transient
    private final Collection domainEvents;
    // ...
    public void domainOperation() {
        // some domain operation
        domainEvents.add(new DomainEvent());
    }

    @DomainEvents
    public Collection events() {
        return domainEvents;
    }
}

Hier ist das Beispiel, das dieses Verhalten erklärt:

@DisplayName("given aggregate with @DomainEvents,"
    + " when do domain operation and save,"
    + " then event is published")
@Test
void domainEvents() {

    // given
    Aggregate2 aggregate = new Aggregate2();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

Nachdem Domänenereignisse veröffentlicht wurden, wird die mit@AfterDomainEventsPublication annotierte Methode aufgerufen.

Der Zweck dieser Methode besteht normalerweise darin, die Liste aller Ereignisse zu löschen, damit sie in Zukunft nicht mehr veröffentlicht werden:

@AfterDomainEventPublication
public void clearEvents() {
    domainEvents.clear();
}

Fügen wir diese Methode der KlasseAggregate2hinzu und sehen, wie sie funktioniert:

@DisplayName("given aggregate with @AfterDomainEventPublication,"
    + " when do domain operation and save twice,"
    + " then an event is published only for the first time")
@Test
void afterDomainEvents() {

    // given
    Aggregate2 aggregate = new Aggregate2();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

Wir sehen deutlich, dass die Veranstaltung nur zum ersten Mal veröffentlicht wird. If we removed the @AfterDomainEventPublication annotation from the clearEvents method, then the same event would be published for the second time.

Es ist jedoch Sache des Implementierers, was tatsächlich passieren würde. Spring garantiert nur, dass Sie diese Methode aufrufen - mehr nicht.

5. Verwenden Sie die AbstractAggregateRoot-Vorlage

It’s possible to further simplify publishing of domain events thanks to the AbstractAggregateRoot template class. Alles, was wir tun müssen, ist, die Methoderegisteraufzurufen, wenn wir das neue Domänenereignis zur Ereignissammlung hinzufügen möchten:

@Entity
public class Aggregate3 extends AbstractAggregateRoot {
    // ...
    public void domainOperation() {
        // some domain operation
        registerEvent(new DomainEvent());
    }
}

Dies ist ein Gegenstück zu dem im vorherigen Abschnitt gezeigten Beispiel.

Nur um sicherzustellen, dass alles wie erwartet funktioniert - hier sind die Tests:

@DisplayName("given aggregate extending AbstractAggregateRoot,"
    + " when do domain operation and save twice,"
    + " then an event is published only for the first time")
@Test
void afterDomainEvents() {

    // given
    Aggregate3 aggregate = new Aggregate3();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

@DisplayName("given aggregate extending AbstractAggregateRoot,"
    + " when do domain operation and save,"
    + " then an event is published")
@Test
void domainEvents() {
    // given
    Aggregate3 aggregate = new Aggregate3();

    // when
    aggregate.domainOperation();
    repository.save(aggregate);

    // then
    verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}

Wie wir sehen können, können wir viel weniger Code produzieren und genau den gleichen Effekt erzielen.

6. Implementierungsvorbehalte

Während es auf den ersten Blick eine gute Idee sein mag, die@DomainEvents-Funktion zu verwenden, gibt es einige Fallstricke, die wir beachten müssen.

6.1. Unveröffentlichte Ereignisse

Bei der Arbeit mit JPA rufen wir nicht unbedingt die Speichermethode auf, wenn wir die Änderungen beibehalten möchten.

Wenn unser Code Teil einer Transaktion ist (z. Mit@Transactional kommentiert und Änderungen an der vorhandenen Entität vorgenommen, lassen wir die Transaktion normalerweise einfach festschreiben, ohne die Methodesave in einem Repository explizit aufzurufen. Selbst wenn unser Aggregat neue Domain-Ereignisse hervorbringt, werden diese niemals veröffentlicht.

We need also remember that @DomainEvents feature works only when using Spring Data repositories. Dies könnte ein wichtiger Designfaktor sein.

6.2. Verlorene Ereignisse

If an exception occurs during events publication, the listeners will simply never get notified.

Selbst wenn wir die Benachrichtigung der Ereignis-Listener irgendwie garantieren könnten, gibt es derzeit keinen Gegendruck, um die Publisher wissen zu lassen, dass etwas schief gelaufen ist. Wenn der Ereignis-Listener durch eine Ausnahme unterbrochen wird, bleibt das Ereignis unverbraucht und wird nie wieder veröffentlicht.

Dieser Designfehler ist dem Spring-Entwicklerteam bekannt. Einer der Hauptentwickler schlug sogar möglichesolution to this problemvor.

6.3. Lokaler Kontext

Domänenereignisse werden über eine einfacheApplicationEventPublisher-Schnittstelle veröffentlicht.

By default, when using ApplicationEventPublisher, events are published and consumed in the same thread. Alles passiert im selben Container.

Normalerweise möchten wir Ereignisse über einen Nachrichtenbroker senden, damit die anderen verteilten Clients / Systeme benachrichtigt werden. In diesem Fall müssen wir Ereignisse manuell an den Nachrichtenbroker weiterleiten.

Es ist auch möglich,Spring Integration oder Lösungen von Drittanbietern wieApache Camel zu verwenden.

7. Fazit

In diesem Artikel haben wir gelernt, wie aggregierte Domänenereignisse mithilfe der Annotation von@DomainEventsverwaltet werden.

This approach can greatly simplify events infrastructure so we can focus only on the domain logic. Wir müssen uns nur bewusst sein, dass es keine Silberkugel gibt und die Art und Weise, wie Spring mit Domain-Ereignissen umgeht, keine Ausnahme ist.

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