Desenhe o modelo, depois o código.

Um modelo de dados costuma começar por um ORM. Mas um ORM descreve não o domínio do problema, e sim o banco de dados construído sobre ele; para NoSQL não chega a existir modelo de entidades algum. O AOA descreve o domínio tal como ele é — igual para SQL e para NoSQL.

Um banco é construído para armazenar com segurança e ler rápido, não para compreender: primeiro a normalização, depois a desnormalização por desempenho, e o domínio é distorcido duas vezes, em direções opostas. Aqui, em vez disso, ele é construído por etapas: primeiro o mapa de domínios, depois as entidades que o preenchem, as relações entre elas e um lifecycle para cada uma. Tudo isso é empacotado em um Resource — através dele, um banco de dados real é lido para dentro do modelo.

Documentação ↗

O mapa de domínios

Conhecer um sistema novo sempre começa com uma pergunta: para que ele serve e o que faz. São perguntas sobre sentido, não sobre construção. Mas a primeira resposta costuma ser a infraestrutura: controladores, serviços, repositórios, migrações. O domínio do problema não se enxerga por trás deles.

O mapa de domínios é a resposta à primeira pergunta. Nele há apenas as áreas de que o sistema é feito: StoreDomain — gestão de pedidos, BillingDomain — faturamento, MessagingDomain — notificações. Uma resposta sem detalhes abrange o sistema inteiro e cabe na cabeça: o primeiro ponto de apoio.

O mapa de domínios
Documentação completa ↗

Entidades expressam o domínio

Um domínio tem duas coisas principais: Entities — pedido, cliente, item — e Actions, as operações nomeadas sobre elas. Nem controladores, nem tabelas, nem o resto da infraestrutura. Isso permite descer um passo a mais sem perder o foco na intenção de negócio do sistema, em vez de na sua implementação técnica.

Uma Entity é uma classe comum: um nome, campos tipados e nada mais. Ela não sabe nada sobre um ORM, nada sobre tabelas, nada sobre de onde virão os dados — sua única tarefa é descrever um objeto do mundo real do jeito que se fala dele: um pedido tem um total e uma moeda, um cliente tem um nome e um e-mail. Os campos são de dois tipos: simples — texto, número, data — e referências a outras Entities, porque um pedido tem um cliente e tem itens. São os segundos que transformam um conjunto de classes soltas em um modelo, e é deles que trata a próxima seção. Já o comportamento não existe ali: uma Entity apenas descreve, e tudo o que se faz com o objeto vive em um Action.

O que é um Action →
CustomerEntityPKidstrnamestremailstrOrderEntityPKidstrtotalfloatcurrencystrstatusstr
Documentação completa ↗

Relações tornam a propriedade explícita

Já está claro do que um domínio é feito. Mas o que o mantém unido, e o que impede que suas relações se desencontrem?

Uma relação é descrita por duas coisas: com que força os objetos se seguram um ao outro — composição, agregação ou associação — e quantos objetos há de cada lado, um ou vários. Cada entidade declara a relação inteira: o seu próprio lado e o oposto, com tipo e cardinalidade em ambos. Por isso basta olhar para uma entidade para ver de uma vez todas as suas relações. Na inicialização, o sistema confere cada par: se os dois lados não batem, a inicialização para. Às vezes não há segundo lado: a relação aponta para fora do seu próprio armazenamento. Isso também é possível, mas precisa ser declarado.

OrderEntityPKidstrtotalfloatcurrencystrstatusstrFKcustomer→ CustomerEntityFKlines→ OrderLineEntityCustomerEntityPKidstrnamestremailstrFKorders→ OrderEntityOrderLineEntityPKidstrskustrFKorder→ OrderEntity
Documentação completa ↗

Lifecycle controla cada transição

Enquanto o status for apenas uma string, nada impede que um pedido pule de rascunho direto para entregue. O que é permitido e o que não é vive na cabeça do time, ou num comentário que ficou desatualizado há muito tempo.

No AOA essas regras deixam de ser orais: os estados de um pedido e as transições entre eles são declarados junto da própria entidade, como uma máquina de estados — rascunho, pago, enviado, entregue —, e o cancelamento só é possível a partir dos dois primeiros. Não é uma recomendação: uma transição que não está na lista não existe, e um pedido não pode acabar entregue sem ter sido pago. A própria máquina é verificada na inicialização: um estado inalcançável, ou uma transição para lugar nenhum, param a inicialização muito antes de o primeiro pedido real passar.

A máquina é declarada por si só: que estados um pedido pode assumir e que transições entre eles são permitidas. Uma vez, à parte de qualquer entidade.

draftpaidshippeddeliveredcancelled

Agora ele pode ser aplicado: em OrderEntity o campo status deixa de ser uma string e passa a ter o tipo OrderLifecycle. Uma string aceitava qualquer valor; este tipo aceita só aqueles até os quais existe um caminho.

OrderEntityPKidstrtotalfloatcurrencystrlifecycleOrderLifecycle
Documentação completa ↗

Resources definem a fronteira do armazenamento

O modelo descreve o domínio do problema, mas não trabalha com bancos de dados diretamente: ele não deve depender de como eles são feitos. Então outra coisa precisa buscar os dados de um banco real — e devolvê-los.

É disso que o Resource cuida. Ele guarda tudo o que precisa viver entre chamadas: a conexão, o pool, o cliente. Sua tarefa é abrir, executar e devolver; dentro não há regras de negócio. O que atravessa essa fronteira são as entidades. Um mesmo modelo pode estar representado em vários Resource ao mesmo tempo: um lê de SQL, outro de NoSQL, um terceiro do serviço HTTP de outra pessoa, e todos devolvem o mesmo OrderEntity. O código da lógica de negócio nunca fica sabendo qual deles atuou: nem nome de tabela, nem dialeto, nem formato de resposta chegam até ele. Por isso trocar de armazenamento significa trocar apenas o Resource.

códigoResourceOrderEntityNoSQLSQLserviço HTTPuma interface · mesma OrderEntity · fonte substituível
Três tipos de Resource: Storage, Gateway, Controller ↗Documentação completa ↗

Projeções dão forma a cada leitura

A mesma entidade é lida do banco ora inteira, ora parcialmente, ora apenas como um identificador. A resposta habitual a isso é um zoológico de DTOs, uma classe para cada caso.

A abordagem habitual é um DTO próprio para cada método do Resource. Diferente de devolver dicionários comuns, isso dá proteção estática contra erros de digitação nos nomes dos campos. Mas a abordagem tem outro lado: a entidade original se desfaz em um monte de DTOs pequenos, e o modelo do domínio sobrevive apenas na documentação e em diagramas que ficam desatualizados antes da primeira versão.

A ideia do AOA é não abrir mão do modelo inteiro: um método do Resource devolve os próprios objetos do modelo, tal como foram declarados. Os campos são lidos por atributos nomeados, como num DTO, mas nenhuma classe nova aparece — e colocar no modelo um campo que o modelo não tem torna-se impossível. Sua atualidade é confirmada pelo próprio código que o usa, e o ERD é desenhado diretamente a partir dele.

Por isso se introduz a noção de projeção de dados: quando um método do Resource lê a entidade parcialmente, ele devolve essa mesma entidade, mas os campos não lidos do banco ficam marcados como indisponíveis. Na primeira tentativa de usá-los o sistema lança uma exceção clara em vez de ficar calado.

O modelo ERD no Maxitor →

get_order(order_id) → OrderEntity

O caso óbvio — a entidade inteira, todos os campos carregados; a classe do modelo usada exatamente como declarada.

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"total149.90currency"RUB"status"paid"retorna

find_orders(query) → list[OrderEntity]

Uma coleção enxuta — apenas o id de cada pedido é carregado; ler qualquer outro campo levantaria um erro.

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"totalcurrencystatusretorna

get_full_order_info(order_id) → OrderEntity

O caso profundo — o pedido mais suas entidades aninhadas, cada uma carregada até uma profundidade parcial diferente (o id é sempre carregado, ele é a chave).

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"total149.90FKcustomer→ CustomerEntityFKlines→ OrderLineEntityCustomerEntityPKid"CUST-7"name"Ivan Petrov"emailOrderLineEntityPKid"LN-1"sku"SKU-55"quantity3unit_priceretorna
Documentação completa ↗
Desenhe o modelo, depois o código. · aoa.run