Agregados DDD persistentes

Agregados DDD persistentes

1. Visão geral

Neste tutorial, vamos explorar as possibilidades de persistirDDD Aggregates usando diferentes tecnologias.

2. Introdução aos agregados

An aggregate is a group of business objects which always need to be consistent. Portanto, salvamos e atualizamos agregados como um todo em uma transação.

O agregado é um padrão tático importante no DDD, que ajuda a manter a consistência de nossos objetos de negócios. No entanto, a ideia de agregado também é útil fora do contexto DDD.

Existem vários casos de negócios em que esse padrão pode ser útil. As a rule of thumb, we should consider using aggregates when there are multiple objects changed as part of the same transaction.

Vamos dar uma olhada em como podemos aplicar isso ao modelar um pedido de compra.

2.1. Exemplo de pedido de compra

Então, vamos supor que queremos modelar um pedido de compra:

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. Os camposorderLines etotalCost deOrder devem ser sempre consistentes, ou seja,totalCost sempre deve ter o valor igual à soma de todos osorderLines.

Now, we all might be tempted to turn all of these into fully-fledged Java Beans. Mas, observe que introduzir getters e setters simples emOrder poderia facilmente quebrar o encapsulamento de nosso modelo e violar as restrições de negócios.

Vamos ver o que pode dar errado.

2.2. Design agregado ingênuo

Vamos imaginar o que poderia acontecer se decidíssemos ingenuamente adicionar getters e setters a todas as propriedades na classeOrder, incluindosetOrderTotal.

Não há nada que nos proíba de executar o seguinte código:

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

Neste código, definimos manualmente a propriedadetotalCost para zero, violando uma importante regra de negócios. Definitivamente, o custo total não deve ser zero dólar!

Precisamos de uma maneira de proteger nossas regras de negócios. Vejamos como o Aggregate Roots pode ajudar.

2.3. Raiz Agregada

Umaggregate root é uma classe que funciona como um ponto de entrada para nosso agregado. All business operations should go through the root. Dessa forma, a raiz do agregado pode cuidar de manter o agregado em um estado consistente.

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

E em nosso exemplo, a classeOrder é a candidata certa para a raiz agregada. Só precisamos fazer algumas modificações para garantir que o agregado seja sempre consistente:

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

    // ...
}

Usar uma raiz agregada agora nos permite transformar mais facilmenteProducteOrderLine em objetos imutáveis, onde todas as propriedades são finais.

Como podemos ver, este é um agregado bastante simples.

E poderíamos simplesmente ter calculado o custo total de cada vez sem usar um campo.

No entanto, agora estamos apenas falando sobre persistência agregada, não design agregado. Fique atento, pois esse domínio específico será útil em um momento.

Quão bem isso funciona com as tecnologias de persistência? Vamos dar uma olhada. Ultimately, this will help us to choose the right persistence tool for our next project.

3. JPA e Hibernate

Nesta seção, vamos tentar persistir nosso agregadoOrder usando JPA e Hibernate. Usaremos Spring Boot eJPA starter:


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

Para a maioria de nós, essa parece ser a escolha mais natural. Afinal, passamos anos trabalhando com sistemas relacionais e todos conhecemos estruturas ORM populares.

Probably the biggest problem when working with ORM frameworks is the simplification of our model design. Às vezes também é referido comoObject-relational impedance mismatch. Vamos pensar no que aconteceria se quiséssemos persistir nosso agregadoOrder:

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

Nesse ponto, esse teste lançaria uma exceção:java.lang.IllegalArgumentException: Unknown entity: com.example.ddd.order.Order. Obviously, we’re missing some of the JPA requirements:

  1. Adicionar anotações de mapeamento

  2. As classesOrderLineeProduct devem ser entidades ou classes@Embeddable, não objetos de valor simples

  3. Adicione um construtor vazio para cada entidade ou classe@Embeddable

  4. Substitua as propriedadesMoney por tipos simples

Hmm, precisamos modificar o design do agregadoOrder para poder usar JPA. Embora adicionar anotações não seja um grande problema, os outros requisitos podem apresentar muitos problemas.

3.1. Alterações nos objetos de valor

A primeira questão de tentar ajustar um agregado à JPA é que precisamos interromper o design de nossos objetos de valor: suas propriedades não podem mais ser finais e precisamos interromper o encapsulamento.

We need to add artificial ids to the OrderLine and Product, even if these classes were never designed to have identifiers. Queríamos que eles fossem objetos de valor simples.

É possível usar anotações@Embeddede@ElementCollection, mas esta abordagem pode complicar muito as coisas ao usar um gráfico de objeto complexo (por exemplo, objeto@Embeddable com outra propriedade@Embedded etc. .).

Usar a anotação@Embedded simplesmente adiciona propriedades planas à tabela pai. Exceto que, propriedades básicas (por exemplo, do tipoString) ainda requerem um método setter, que viola o projeto do objeto de valor desejado.

Empty constructor requirement forces the value object properties to not be final anymore, breaking an important aspect of our original design. Verdade seja dita, o Hibernate pode usar o construtor privado no-args, que atenua um pouco o problema, mas ainda está longe de ser perfeito.

Mesmo ao usar um construtor padrão privado, não podemos marcar nossas propriedades como finais ou precisamos inicializá-las com valores padrão (geralmente nulos) dentro do construtor padrão.

No entanto, se queremos ser totalmente compatíveis com JPA, devemos usar pelo menos visibilidade protegida para o construtor padrão, o que significa que outras classes no mesmo pacote podem criar objetos de valor sem especificar valores de suas propriedades.

3.2. Tipos complexos

Infelizmente, não podemos esperar que o JPA mapeie automaticamente tipos complexos de terceiros em tabelas. Veja quantas mudanças tivemos que introduzir na seção anterior!

Por exemplo, ao trabalhar com nosso agregadoOrder, encontraremos dificuldades para persistir os camposJoda Money.

Nesse caso, podemos acabar escrevendo o tipo personalizado@Converter disponível no JPA 2.1. Isso pode exigir algum trabalho adicional, no entanto.

Alternativamente, também podemos dividir a propriedadeMoney em duas propriedades básicas. Por exemplo,String para a unidade monetária eBigDecimal para o valor real.

Embora possamos ocultar os detalhes de implementação e ainda usar a classeMoney por meio da API de métodos públicos, a prática mostra que a maioria dos desenvolvedores não pode justificar o trabalho extra e simplesmente degeneraria o modelo para estar em conformidade com a especificação JPA.

3.3. Conclusão

Embora JPA seja uma das especificações mais adotadas no mundo, pode não ser a melhor opção para persistir nosso agregadoOrder.

Se quisermos que nosso modelo reflita as verdadeiras regras de negócios, devemos projetá-lo para não ser uma representação 1: 1 simples das tabelas subjacentes.

Basicamente, temos três opções aqui:

  1. Crie um conjunto de classes de dados simples e use-as para persistir e recriar o rico modelo de negócios. Infelizmente, isso pode exigir muito trabalho extra.

  2. Aceite as limitações da JPA e escolha o compromisso certo.

  3. Considere outra tecnologia.

A primeira opção tem o maior potencial. Na prática, a maioria dos projetos é desenvolvida usando a segunda opção.

Agora, vamos considerar outra tecnologia para persistir agregados.

4. Armazenamento de Documentos

Um armazenamento de documentos é uma maneira alternativa de armazenar dados. Em vez de usar relações e tabelas, salvamos objetos inteiros. This makes a document store a potentially perfect candidate for persisting aggregates.

Para as necessidades deste tutorial, vamos nos concentrar em documentos do tipo JSON.

Vamos dar uma olhada mais de perto em como nosso problema de persistência de pedido se parece em um armazenamento de documentos como o MongoDB.

4.1. Agregando persistente usando o MongoDB

Now, there are quite a few databases which can store JSON data, one of the popular being MongoDB. MongoDB realmente armazena BSON ou JSON na forma binária.

Graças ao MongoDB, podemos armazenar o agregado de exemploOrderas-is.

Antes de prosseguirmos, vamos adicionar o iniciador Spring BootMongoDB:


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

Agora podemos executar um caso de teste semelhante, como no exemplo JPA, mas desta vez usando o 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 precisa criar construtores padrão, configuradores ou conversor personalizado para a classeMoney.

E aqui está o que nosso agregadoOrder aparece na loja:

{
  "_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"
}

Este simples documento BSON contém todo o agregadoOrder em uma parte, combinando perfeitamente com nossa noção original de que tudo isso deve ser consistente em conjunto.

Observe que objetos complexos no documento BSON são simplesmente serializados como um conjunto de propriedades JSON regulares. Graças a isso, até mesmo classes de terceiros (comoJoda Money) podem ser serializadas facilmente sem a necessidade de simplificar o modelo.

4.2. Conclusão

Agregar persistentes usando o MongoDB é mais simples do que usar o JPA.

This absolutely doesn’t mean MongoDB is superior to traditional databases. Existem muitos casos legítimos em que não devemos nem mesmo tentar modelar nossas classes como agregados e usar um banco de dados SQL.

Ainda assim, quando identificamos um grupo de objetos que deve ser sempre consistente de acordo com os requisitos complexos, usar um armazenamento de documentos pode ser uma opção muito atraente.

5. Conclusão

No DDD, agregados geralmente contêm os objetos mais complexos do sistema. Trabalhar com eles precisa de uma abordagem muito diferente da maioria dos aplicativos CRUD.

O uso de soluções ORM populares pode levar a um modelo de domínio simplista ou superexposto, que muitas vezes é incapaz de expressar ou impor regras comerciais complexas.

Os armazenamentos de documentos podem facilitar a persistência de agregados sem sacrificar a complexidade do modelo.

O código-fonte completo de todos os exemplos está disponívelover on GitHub.