Agregados DDD e @DomainEvents
*1. Visão geral *
Neste tutorial, explicaremos como usar a anotação _ @ DomainEvents_ e a classe AbstractAggregateRoot para publicar e manipular convenientemente eventos de domínio produzidos por agregado - um dos principais padrões de design tático no design orientado a domínio.
Os agregados aceitam comandos de negócios, o que geralmente resulta na produção de um evento relacionado ao domínio de negócios - o Evento de Domínio.
Se você quiser saber mais sobre DDD e agregados, é melhor começar com o original book de Eric Evans. Há também uma ótima série sobre design agregado eficaz escrita por Vaughn Vernon. Definitivamente vale a pena ler.
Pode ser complicado trabalhar manualmente com eventos de domínio. Felizmente, o Spring Framework nos permite publicar e manipular facilmente eventos de domínio ao trabalhar com raízes agregadas* usando repositórios de dados.
*2. Dependências do Maven *
A Spring Data introduziu _ @ DomainEvents_ no trem de lançamento da Ingalls. Está disponível para qualquer tipo de repositório.
As amostras de código fornecidas para este artigo usam o Spring Data JPA.* A maneira mais simples de integrar eventos do domínio Spring ao nosso projeto é usar o https://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-starter-data- jpa [Iniciador de JPA de dados de inicialização de primavera]: *
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
===* 3. Publicar eventos manualmente *
Primeiro, vamos tentar publicar eventos de domínio manualmente. Explicaremos o uso de _ @ DomainEvents_ na próxima seção.
Para as necessidades deste artigo, usaremos uma classe de marcador vazia para eventos de domínio - o DomainEvent.
Vamos usar a interface padrão ApplicationEventPublisher.
Existem dois bons lugares onde podemos publicar eventos: camada de serviço ou diretamente dentro do agregado.
3.1. Camada de serviço
*Podemos simplesmente publicar eventos após chamar o método _save_ do repositório dentro de um método de serviço* .
Se um método de serviço fizer parte de uma transação e manipularmos os eventos dentro do ouvinte anotados com _ @ TransactionalEventListener_, os eventos serão manipulados somente depois que a transação for confirmada com êxito.
Portanto, não há risco de ter eventos "falsos" manipulados quando a transação é revertida e o agregado não é atualizado:
@Service
public class DomainService {
//...
@Transactional
public void serviceDomainOperation(long entityId) {
repository.findById(entityId)
.ifPresent(entity -> {
entity.domainOperation();
repository.save(entity);
eventPublisher.publishEvent(new DomainEvent());
});
}
}
Aqui está um teste que prova que os eventos são realmente publicados por 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. Agregar
*Também podemos publicar eventos diretamente de dentro do agregado* .
Dessa forma, gerenciamos a criação de eventos de domínio dentro da classe, o que parece mais natural para isso:
@Entity
class Aggregate {
//...
void domainOperation() {
//some business logic
if (eventPublisher != null) {
eventPublisher.publishEvent(new DomainEvent());
}
}
}
Infelizmente, isso pode não funcionar como esperado, devido ao modo como o Spring Data inicializa entidades a partir de repositórios.
Aqui está o teste correspondente que mostra o comportamento real:
@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);
}
Como podemos ver, o evento não é publicado. Ter dependências dentro do agregado pode não ser uma boa ideia. Neste exemplo, ApplicationEventPublisher não é inicializado automaticamente pelo Spring Data.
O agregado é construído chamando o construtor padrão. Para que ele se comporte como esperávamos, precisaríamos recriar manualmente entidades (por exemplo, usando fábricas personalizadas ou programação de aspectos).
Além disso, devemos evitar a publicação de eventos imediatamente após a conclusão do método agregado. Pelo menos, a menos que tenhamos 100% de certeza de que esse método faz parte de uma transação. Caso contrário, podemos ter eventos "espúrios" publicados quando a mudança ainda não persistir. Isso pode levar a inconsistências no sistema.
Se queremos evitar isso, devemos lembrar sempre de chamar métodos agregados dentro de uma transação. Infelizmente, dessa maneira, associamos nosso design fortemente à tecnologia de persistência. Precisamos lembrar que nem sempre trabalhamos com sistemas transacionais.
*Portanto, geralmente é uma idéia melhor permitir que nosso agregado simplesmente gerencie uma coleção de eventos de domínio e os retorne quando estiver prestes a persistir* .
Na próxima seção, explicaremos como podemos tornar a publicação de eventos de domínio mais gerenciável usando as anotações @DomainEvents e _ @ AfterDomainEvents_.
4. Publicar eventos usando @DomainEvents
*Desde o lançamento do Spring Data Ingalls, podemos usar a anotação _ @ DomainEvents_ para publicar automaticamente eventos do domínio* .
Um método anotado com _ @ DomainEvents_ é automaticamente invocado pelo Spring Data sempre que uma entidade é salva usando o repositório correto.
Em seguida, os eventos retornados por esse método são publicados usando a interface ApplicationEventPublisher:
@Entity
public class Aggregate2 {
@Transient
private final Collection<DomainEvent> domainEvents;
//...
public void domainOperation() {
//some domain operation
domainEvents.add(new DomainEvent());
}
@DomainEvents
public Collection<DomainEvent> events() {
return domainEvents;
}
}
Aqui está o exemplo que explica esse comportamento:
@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));
}
Após a publicação dos eventos do domínio, o método anotado com _ @ AfterDomainEventsPublication_ é chamado.
O objetivo deste método é geralmente limpar a lista de todos os eventos, para que eles não sejam publicados novamente no futuro:
@AfterDomainEventPublication
public void clearEvents() {
domainEvents.clear();
}
Vamos adicionar este método à classe Aggregate2 e ver como funciona:
@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));
}
Vemos claramente que o evento é publicado apenas pela primeira vez. Se removermos a anotação _ @ AfterDomainEventPublication_ do método clearEvents, o mesmo evento será publicado pela segunda vez .
No entanto, cabe ao implementador o que realmente aconteceria. O Spring apenas garante chamar esse método - nada mais.
5. Usar o modelo AbstractAggregateRoot
*É possível simplificar ainda mais a publicação de eventos de domínio, graças à classe de modelo _AbstractAggregateRoot_* . Tudo o que precisamos fazer é chamar o método _register_ quando quisermos adicionar o novo evento de domínio à coleção de eventos:
@Entity
public class Aggregate3 extends AbstractAggregateRoot<Aggregate3> {
//...
public void domainOperation() {
//some domain operation
registerEvent(new DomainEvent());
}
}
Isso é uma contrapartida do exemplo mostrado na seção anterior.
Apenas para garantir que tudo funcione conforme o esperado - eis os testes:
@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));
}
Como podemos ver, podemos produzir muito menos código e obter exatamente o mesmo efeito.
*6. Advertências de implementação *
Embora possa parecer uma ótima idéia usar o recurso _ @ DomainEvents_ no início, há algumas armadilhas que precisamos estar cientes.
6.1. Eventos não publicados
Ao trabalhar com o JPA, não chamamos necessariamente o método save quando queremos persistir as alterações.
Se nosso código fizer parte de uma transação (por exemplo, anotada com _ @ Transactional_) e faz alterações na entidade existente, então geralmente deixamos a transação ser confirmada sem chamar explicitamente o método save em um repositório. Portanto, mesmo que nosso agregado produza novos eventos de domínio, eles nunca serão publicados.
*Também precisamos lembrar que o recurso _ @ DomainEvents_ funciona apenas ao usar os repositórios do Spring Data* . Isso pode ser um fator de design importante.
6.2. Eventos perdidos
*Se ocorrer uma exceção durante a publicação dos eventos, os ouvintes simplesmente nunca serão notificados* .
Mesmo que de alguma forma possamos garantir a notificação dos ouvintes do evento, atualmente não há contrapressão para informar aos editores que algo deu errado. Se o ouvinte de evento for interrompido por uma exceção, o evento permanecerá não consumido e nunca será publicado novamente.
Essa falha de design é conhecida pela equipe de desenvolvimento da Spring. Um dos principais desenvolvedores até sugeriu uma possível solution para esse problema.
6.3. Contexto local
Eventos de domínio são publicados usando uma interface simples ApplicationEventPublisher.
*Por padrão, ao usar _ApplicationEventPublisher_, os eventos são publicados e consumidos no mesmo encadeamento* . Tudo acontece no mesmo contêiner.
Normalmente, queremos enviar eventos através de algum tipo de intermediário de mensagens, para que outros clientes/sistemas distribuídos sejam notificados. Nesse caso, precisaríamos encaminhar eventos manualmente para o intermediário de mensagens.
Também é possível usar Spring Integration ou soluções de terceiros, como Apache Camel.
7. Conclusão
Neste artigo, aprendemos como gerenciar eventos de domínio agregados usando a anotação _ @ DomainEvents_.
*Essa abordagem pode simplificar bastante a infraestrutura de eventos, para que possamos focar apenas na lógica do domínio* . Só precisamos estar cientes de que não há uma bala de prata e a maneira como o Spring lida com eventos de domínio não é uma exceção.
O código fonte completo de todos os exemplos está disponível over no GitHub.