DDD集約と@DomainEvents

DDD集計と@DomainEvents

1. 概要

このチュートリアルでは、@DomainEventsアノテーションとAbstractAggregateRootクラスを使用して、ドメイン駆動設計の主要な戦術設計パターンの1つであるaggregateによって生成されたドメインイベントを便利に公開および処理する方法について説明します。

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

DDDと集計について詳しく知りたい場合は、Eric Evansのoriginal bookから始めるのが最善です。 Vaughn Vernonによって書かれた素晴らしいseries about effective aggregate designもあります。 間違いなく読む価値があります。

ドメインイベントを手動で操作するのは面倒です。 ありがたいことに、Spring Framework allows us to easily publish and handle domain events when working with aggregate rootsはデータリポジトリを使用しています。

2. Mavenの依存関係

Spring Dataは、Ingallsリリーストレインに@DomainEventsを導入しました。 あらゆる種類のリポジトリで利用できます。

この記事で提供するコードサンプルでは、​​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. イベントを手動で公開する

まず、ドメインイベントを手動で公開してみましょう。 次のセクションでは、@DomainEventsの使用法について説明します。

この記事のニーズに応じて、ドメインイベントに空のマーカークラスであるDomainEventを使用します。

標準のApplicationEventPublisherインターフェースを使用します。

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

3.1. サービス層

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

サービスメソッドがトランザクションの一部であり、@TransactionalEventListenerで注釈が付けられたリスナー内のイベントを処理する場合、イベントはトランザクションが正常にコミットされた後にのみ処理されます。

したがって、トランザクションがロールバックされ、集計が更新されない場合に、「偽の」イベントが処理されるリスクはありません。

@Service
public class DomainService {

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

イベントが実際にserviceDomainOperationによって公開されていることを証明するテストは次のとおりです。

@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. 集計

We can also publish events directly from within the aggregate

このようにして、クラス内でのドメインイベントの作成を管理します。

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

残念ながら、Spring Dataがリポジトリからエンティティを初期化する方法のため、これは期待どおりに機能しない可能性があります。

実際の動作を示す対応するテストは次のとおりです。

@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);
}

ご覧のとおり、イベントはまったく公開されていません。 集約内に依存関係を持つことは、素晴らしいアイデアではないかもしれません。 この例では、ApplicationEventPublisherはSpringDataによって自動的に初期化されません。

集計は、デフォルトのコンストラクターを呼び出すことによって構築されます。 期待どおりに動作させるには、エンティティを手動で再作成する必要があります(例: カスタムファクトリまたはアスペクトプログラミングを使用)。

また、集約メソッドが終了した直後にイベントを公開することは避けてください。 少なくとも、このメソッドがトランザクションの一部であると100%確信がない限り。 そうしないと、変更がまだ永続化されていないときに「偽の」イベントが発行される可能性があります。 これにより、システムに矛盾が生じる可能性があります。

これを回避したい場合は、トランザクション内で常に集約メソッドを呼び出すことを忘れないでください。 残念ながら、この方法では、設計を永続技術に大きく結び付けます。 常にトランザクションシステムを使用しているとは限らないことを覚えておく必要があります。

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

次のセクションでは、@ DomainEventsと@AfterDomainEventsアノテーションを使用して、ドメインイベントの公開をより管理しやすくする方法について説明します。

4. @DomainEventsを使用してイベントを公開する

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

エンティティが適切なリポジトリを使用して保存されるたびに、@DomainEventsアノテーションが付けられたメソッドがSpringDataによって自動的に呼び出されます。

次に、このメソッドによって返されるイベントは、ApplicationEventPublisherインターフェイスを使用して公開されます。

@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;
    }
}

この動作を説明する例を次に示します。

@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));
}

ドメインイベントが公開された後、@AfterDomainEventsPublicationアノテーションが付けられたメソッドが呼び出されます。

このメソッドの目的は通常、すべてのイベントのリストをクリアすることであるため、今後それらが再度公開されることはありません。

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

このメソッドをAggregate2クラスに追加して、どのように機能するかを見てみましょう。

@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));
}

イベントが初めて公開されたことがはっきりとわかります。 If we removed the @AfterDomainEventPublication annotation from the clearEvents method, then the same event would be published for the second time

ただし、実際に何が起こるかは実装者次第です。 Springでは、このメソッドを呼び出すことのみが保証されています。

5. AbstractAggregateRootテンプレートを使用する

It’s possible to further simplify publishing of domain events thanks to the AbstractAggregateRoot template class。 新しいドメインイベントをイベントのコレクションに追加する場合は、registerメソッドを呼び出すだけです。

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

これは、前のセクションで示した例に相当します。

すべてが期待どおりに機能することを確認するために、テストを次に示します。

@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));
}

ご覧のように、生成するコードを大幅に減らし、まったく同じ効果を達成できます。

6. 実装に関する警告

最初は@DomainEvents機能を使用するのは良い考えのように思えるかもしれませんが、注意が必要ないくつかの落とし穴があります。

6.1. 未公開のイベント

JPAを使用する場合、変更を永続化するときに必ずしもsaveメソッドを呼び出す必要はありません。

コードがトランザクションの一部である場合(例: @Transactional)でアノテーションが付けられ、既存のエンティティに変更を加える場合、通常、リポジトリでsaveメソッドを明示的に呼び出さずに、トランザクションをコミットさせます。 そのため、集計が新しいドメインイベントを生成した場合でも、それらは公開されません。

We need also remember that @DomainEvents feature works only when using Spring Data repositories。 これは重要な設計要素かもしれません。

6.2. 失われたイベント

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

どういうわけかイベントリスナーの通知を保証できたとしても、現在、パブリッシャーに何か問題が発生したことを知らせるためのバックプレッシャーはありません。 イベントリスナーが例外によって中断された場合、イベントは消費されずに残り、再び発行されることはありません。

この設計上の欠陥は、Spring開発チームに知られています。 リード開発者の1人は、solution to this problemの可能性さえ提案しました。

6.3. ローカルコンテキスト

ドメインイベントは、単純なApplicationEventPublisherインターフェイスを使用して公開されます。

By default, when using ApplicationEventPublisher, events are published and consumed in the same thread。 すべてが同じコンテナ内で発生します。

通常、何らかの種類のメッセージブローカーを介してイベントを送信するため、他の分散クライアント/システムに通知されます。 このような場合、イベントを手動でメッセージブローカーに転送する必要があります。

Spring IntegrationまたはApache Camelなどのサードパーティソリューションを使用することもできます。

7. 結論

この記事では、@DomainEventsアノテーションを使用して集約ドメインイベントを管理する方法を学習しました。

This approach can greatly simplify events infrastructure so we can focus only on the domain logic。 特効薬はなく、Springがドメインイベントを処理する方法も例外ではないことに注意する必要があります。

すべての例の完全なソースコードは、over on GitHubで入手できます。