A Onion Architecture se apoia em um paradigma que 9 em cada 10 desenvolvedores afirmam ter domínio,esse paradigma se chama orientação a objetos.
O uso de uma linguagem orientada a objetos não é suficiente para que o sistema desenvolvido seja de fato orientado a objetos. Eu gosto de dizer que existem 2 tipos de sistemas desenvolvidos nesse paradigma, que são: os sistemas orientados a objetos e os sistemas com objetos, e logo vamos entender o que isso significa na prática.
Uma breve análise sobre falhas na aplicação da OOP
O código abaixo eu tendo a chamar de consequência vertical pois em minhas observações eu percebi que os métodos crescem de forma vertical, além de perder expressividade pois os contextos não são apresentados nas interações dos objetos:
//O quê?
public void registerAuditingOf(Negotiation negotiation, User user) {
//Como?
Optional<Auditing> lastVersionAudit = repository.findLastVersion(negotiation)
Audinting currentVersion = new Auditing(negotiation);
if (!lastVersionAudit.equals(currentVersion)) {
currentVersion.setUser(user);
repository.save(currentVersion)
}
}
Observem que esse método não é tão claro em relação ao objetivo dele, o método recebe 2 parâmetros que não responde à pergunta "O quê?" e no corpo não é muito claro o que significa esse currentVersion.setUser(user), quem é user o que ele faz? Nem esse tal de repository.save(currentVersion), a pergunta que fica é: qual a interação entre os objetos? Qual o contexto dessa interação? Esse é um típico código "com objetos" e não orientado a eles. Isso é o início daqueles códigos ininteligíveis que só o dono entende, e olhe lá!
E como ficaria o mesmo código aplicando o OOP?
Consequência horizontal é mais um termo que cunhei observando sistemas realmente orientados a objetos, e baseado no exemplo abaixo, do mesmo método exemplificado acima, observem a diferença na expressividade.
//O quê?
1 - public void register(NewAuditingRequest request) {
//Como?
2- User user = request.getUser();
3- Negotiation negotiation = request.getNegotiation();
4- Optional<Auditing> lastVersionAuditing = repository.findOne(LastVersion.of(negotiation))
5- Boolean itWasChanged=negotiation.wasChanged().whenComparedWith(lastVersionAuditing);
6- if (itWasChanged) {
7- auditing().addNewVersionOf(negotiation.changedBy(user))
}
}
Na linha 5 vemos a "consequência horizontal" funcionando e com isso vem a interação dos objetos, o resultado é uma linguagem clara e objetiva.
E para finalizar na linha 6 é verificado se a negociação foi alterada em comparação com a última versão, se sim, o objeto auditoria recebe a nova versão de negociação que foi alterada pelo usuário… Olha como ficou mais claro, agora sei que o usuário alterou a negociação e que a auditoria guarda versões de negociações alteradas :)
Como fazer isso funcionar?
Quando pensamos em algo fácil de se manusear e de se entender pensamos em organização e o básico desse princípio une dois outros que conhecemos e são o SRP (single responsibility principle) e o KISS (Keep It Simple, Stupid) eu poderia dizer que o SRP é a tão famosa Bounded Context que é bastante apreciado por quem estuda DDD.
Infelizmente o SRP, o Bounded Context e o KISS são pouco aplicados ou tendem a ser aplicados de forma muito micro, e quando digo micro, digo que é apenas em nível de classe ou de método, o que não é suficiente.
Hoje com o advento dos micro serviços os desenvolvedores acordaram para a necessidade de modularizar seus sistemas de uma forma que cada módulo trate de um assunto específico, entretanto a aplicação de tudo isso tende a desrespeitar o KISS, pois as soluções tendem a ser complexas demais.
Uma boa arquitetura sempre está desacoplada da técnica e tende a ser modulável, assim como funciona no mundo real.
Imaginemos que compramos um carro e não gostamos do escapamento, então levamos a um mecânico e pedimos para trocá-lo. No mundo dos sistemas quando pedimos para extrair somente vendas do projeto esbarramos num: "não dá, o projeto terá que ser reescrito".
O problema de reescrever é que isso irá custar uma Ferrari e o que me garante que não farão da mesma forma que fizeram o primeiro?
Com isso é muito comum você ver micro serviços que são monólitos pequenos.
Já questionou alguma vez a arquitetura tradicional?
A base de 99,99% dos projetos os quais passei e que já estavam em produção usavam o modelo tradicional de arquitetura, como mostrado na imagem abaixo. Reparem que a infraestrutura passa a ser a camada mais conhecida dentro dessa arquitetura com isso é comum você ver a parte técnica espalhada por todo o sistema ao ponto de ofuscar o negócio.
E como eu disse acima, esse é outro gerador de anomalias, como métodos enormes, falta de coesão, serviços chamando outros serviços, entidades do hibernate ou qualquer outro framework ORM espalhados por todas as camadas, etc.
Figura 1: Arquitetura tradicional
Uma outra maneira
Já há algum tempo vinha-se discutindo modelos diferentes de arquiteturas.
Em 2008 um cara chamado Jeffrey Palermo propôs uma arquitetura chamada Onion Architecture também conhecida como Hexagonal Architecture ou Ports and Adapters, acabou ganhando o gosto dos desenvolvedores.
Não que isso se tornou ultra popular até porque existe uma certa dificuldade no entendimento da técnica, porém é muito usado quando se fala de DDD.
O projeto DDD Sample que é um esforço da empresa do Erick Evans (autor do famoso livro da capa azul que trata sobre o tema) tem por objetivo apresentar uma forma de modelar usando a Onion Architecture. Alguns pontos dessa implementação deixam a desejar, mas ainda assim é uma boa referência.
A Cebola pode salvar o seu projeto (Onion Architecture)
A imagem abaixo mostra as divisões dessa arquitetura e uma diferença que se nota em relação à arquitetura tradicional está na orientação da dependência que as camadas têm.
A imagem mostra que a infraestrutura conhece a camada de aplicação e a camada de domínio. A camada de aplicação não conhece a infraestrutura, porém conhece o domínio. Por último o domínio não conhece nada além dele. Isso é desenvolver orientado ao negócio, pois o mesmo é o núcleo da sua arquitetura.
Figura 2: Onion Architecture
No projeto, essa estrutura talvez não se mostre desconhecida por você. Na imagem abaixo as camadas são representadas.
Figura 3: Camadas representadas em pacotes
Cada pacote tem sua relevância dentro da arquitetura e abaixo descrevo um pouco melhor:
- View - Camada de interação com o cliente (endpoint, controllers);
- Application (orquestrador de domínio) - Responsável por facilitar o acesso ao domínio;
- Domain (atores) - Camada onde reside a lógica de negócio;
- infraestrutura (adaptador) - Provê acesso aos sistemas externos, configurações etc.
Como aplicar?
Agora que entendemos um pouco, e para muitos isso não era novidade, iremos aplicar o que foi dito. Nada melhor do que construir um projeto de exemplo para ver a aplicação de todas as técnicas.
Mãos na massa
Todo projeto inicia com uma conversa com o domain expert ou com o product owner. Como estou acostumado com Kanban e Scrum irei adotar o termo P.O. (product owner) para me referenciar à pessoa de negócio que tem o entendimento do produto que será desenvolvido.
Bora conversar com o P.O. e descobrir o que ele quer que seja feito.
P.O. apresentando o projeto: "Iremos desenvolver um sistema para melhorar as vendas e assim substituir o legado. Nele será possível ao vendedor criar a negociação de produtos utilizando nossa base de clientes.
Quando a negociação for concluída iremos gerar uma venda que irá para o sistema ERP, sistema responsável por fazer o faturamento"
Tendo o que foi falado, iremos fazer um mapeamento básico das fronteiras do projeto, pelo menos no primeiro momento, claro que no decorrer do desenvolvimento isso pode mudar e o seu projeto tem que ser flexível o suficiente para suportar essas mudanças (para quem trabalha com métodos ágeis nunca se esqueçam que ser ágil é responder rápido às mudanças).
Figura 4: Módulos do projeto
Até o momento isso foi o que conseguimos reproduzir a partir do que o P.O. falou. Pode acontecer que no decorrer do projeto algumas fronteiras sejam alteradas, entretanto isso irá depender de como o negócio irá evoluir e o quão claro ele é nesse momento.
Visto que nosso desenho comporta a estrutura apresentada pelo P.O., vamos replicar essa estrutura em nosso projeto.
Cada uma dessas bolinhas será um módulo em nosso sistema e os módulos nada mais são do que as fronteiras que delimitam os contextos (Bounded Context, SRP, Micro Service etc.), então a forma mais simples de se trabalhar com módulos em qualquer linguagem que tenha o conceito de pacotes é nomear os pacotes de acordo, veja o exemplo:
Figura 5: Pacotes do projeto
A estrutura da Onion Architecture nos dá um modelo para os módulos do sistema, nessa estrutura eu tenho uma infraestrutura geral, ela é responsável por conter configurações dos frameworks que serão utilizados no projeto.
Dentro de cada tema iremos tratá-los de forma separada e única, e é aí que está a grande jogada, tratar no mesmo projeto temas variados. Permitir que que os temas se relacionem, sem gerar um acoplamento forte e que permita que um módulo seja extraído para outro projeto quando necessário.
Vamos começar por negociação e o P.O. veio falar com a gente: "Eu gostaria que um vendedor conseguisse criar uma negociação de produtos para um cliente de nossa base e essa negociação gerasse uma venda".
O desenvolvimento do módulo de negociação será a base para os demais e nele iremos aplicar o máximo possível de técnicas de modelagem para tornar nossos módulos independentes, apesar de estarem no mesmo projeto, mesmo jar, mesmo repositório no git. O mais importante é o KISS (Keep it Simple, Stupid) e a medida que o negócio for crescendo, o mesmo irá sinalizar outras necessidades. Complexidade não faz sentido no primeiro momento porque comprometeria a velocidade do desenvolvimento e não sabemos se terá 1 ou 1 milhão de clientes usando, contudo, como disse acima, temos que manter a capacidade de reagir às mudanças.
Iniciando a construção do módulo de negociação
Figura 6: Modelos de domínio do projeto
Quando o P.O apresentou esse módulo, ele mostrou quais eram os domínios e foram esses que colocamos no domain.model no caso o Customer, Negotiation, Product e Seller.
Figura 7: Modelos de domínio protagonistas e coadjuvantes
Observe que o domínio tem dois tipos de representações:
- Protagonistas: São os modelos os quais têm seu ciclo de vida controlado pelo módulo, ou seja, o módulo de negociação irá criar, atualizar e deletar uma negociação ou um item da negociação.
- Coadjuvantes: São modelos que aparecem no domínio porém não têm seu ciclo de vida controlado pelo módulo, ou seja, eu não posso criar, atualizar ou deletar um Customer, Seller ou Product no módulo de negociação. Esses domínios só tem seus IDs conhecidos, nada mais (faz sentido, não?).
Criando os repositories (ports)
Agora que temos nossos modelos de negócio precisamos ter a capacidade de listar, salvar, deletar, e para isso usamos o repository. O repository é somente a interface dentro do nosso domínio e é essa interface que será a porta (lembram do Ports and Adapters?) de comunicação com o mundo externo, com isso ele se torna uma das partes mais importantes dentro da nossa arquitetura.
Figura 8: Repository
Nesse momento temos a modelagem do Core Domain do nosso módulo (bounded context).
É muito importante notar que o DI (dependency injection) é fundamental para que essa estratégia funcione sem gerar um acoplamento forte entre o domínio e a infraestrutura que conterá os adapters (implementação dos repositories), isso ficará mais claro adiante.
Camada de Aplicação (Application layer)
Essa camada é conhecida como serviço, porém ela tem um conceito diferente do serviço que conhecemos da arquitetura tradicional. A principal diferença está em como usar, no serviço tradicional você pode colocar uma regra de negócio nela, porém aqui não. Uma regra de negócio estará confinada somente ao domínio, que foi a camada que já desenvolvemos e que vamos revisitá-la em breve.
Em OOP a interface define o comportamento que uma implementação deve ter e é por esse motivo que eu coloco o nome da interface com o sufixo façade, porque o desenvolvedor irá olhar a interface e entender que a implementação deverá se comportar como um façade (pelo menos é isso que se espera). Um façade não tem regra de negócio e sua finalidade é facilitar o acesso a algo, que no nosso caso é o domínio.
Figura 9: Interfaces de serviço
Agora que criamos nossas interfaces e classes da nossa Application vamos implementar os primeiros métodos.
Figura 10: Negotiation Service
Observe que o domínio fala exatamente o que a pessoa de negócio (P.O.) diz. Isso é orientar o desenvolvimento ao negócio e a application layer é onde irá aparecer o fluxo de negócio, é nessa camada que o fluxo é desenhado. Então pense no domínio como as regras e na application layer como os fluxos.
Continuando:
Figura 11: Negotiation Service - Busca de Negociações
Finalizamos a negociação e iremos implementar o Item da negociação:
Figura 12: Item Service
Isso é muito diferente de um simples CRUD, estamos desenvolvendo o projeto orientado ao negócio e não à técnica, dessa forma a comunicação do código tem menos ruídos e suposições. Dessa forma o nível de comunicação entre o time de desenvolvimento e o P.O. se torna fluida.
View Layer (Endpoints/Controllers)
Essa camada irá possibilitar o acesso às regras de negócio ao cliente da aplicação. A view é uma camada que está no mesmo nível da infraestrutura, portanto ela pode acessar todas as camadas abaixo: domain model, application service.
Figura 13: View Layer
O Endpoint irá expor um JSON e iremos usar REST usando o media-type application/hal+json, e ter um media- type faz todo o sentido semântico. Observe o payload abaixo, nós temos as informações sobre negociação, porém nesse módulo nós não temos informações sobre o customer, apenas o ID, assim como o seller. Para o consumidor da sua api poder carregar as informações de modelos que não são gerenciados pelo módulo de negociação ele deverá pegar o link e buscar as informações no módulo responsável, dessa forma meu módulo de negociação irá falar somente sobre negociação (princípio de responsabilidade única) e utilizar RESTFul, com um nível de maturidade elevado, irá te ajudar a alcançar isso.
Figura 14: Endpoint - .../negotiations
Os itens da negociação estão em ItemEndpoint e os mesmos estão representados no JSON abaixo:
Figura 15: Itens da negociação
Nós finalizamos o desenvolvimento do sistema, conseguimos testar (mockando os repositories) e a única pessoa que escutamos foram as pessoas de negócio. Não falamos qual o banco de dados iremos usar. Lembram que o P.O falou que iríamos substituir o legado? Então teremos que usar uma base de dados já existente e dentro desse modelo de desenvolvimento não importa se vou usar um banco orientado a relação, ou a documento ou a grafo. Isso acontece porque o sistema passou a não ser orientado ao dado. Isso muda muita coisa.
Agora iremos entrar numa parte crucial para o projeto e essa parte se chama Infrastructure. Essa é a camada que dá vida ao negócio, que permite ao sistema acessar coisas no mundo externo, como um banco de dados, um serviço HTTP, SOAP etc.
Seguindo nosso projeto, vamos à empresa falar com o pessoal responsável pelo banco de dados, afinal tudo terá que funcionar usando a estrutura já existente.
Sr. DBA qual a estrutura das tabelas de negociação?
R: Não existe esse termo em nossa estrutura de dados, porém existem 3 tabelas que fazem parte da estrutura de pré pedido, imagino que essa seria a negociação que você está falando.
Figura 16: Tabelas de pré-pedido
Temos 3 tabelas que representam um pedido em um banco de dados relacional e não uma. Por quê? O DBA tem que refatorar o banco? Não! Não importa como o dado está, talvez um dia isso tenha feito todo o sentido do mundo e hoje não mais, porém fazer um refactoring no banco tem um custo extremamente elevado.
É comum que muitas equipes culpem a estrutura de dados pelos seus erros arquitetônicos, porque os sistemas são orientados ao dado, por esse motivo sempre iniciam o desenvolvimento modelando tabelas num banco de dados.
Para completar o DBA disse que o item do pré pedido, que seria o item da negociação, está no MongoDB.
Figura 17: Itens de pré pedido em documentos
Entendido como a estrutura de dados funciona vamos fazer essa estrutura funcionar com o nosso domínio.
Infrastructure Layer (Adaptadores)
Eu falei que a Onion Architecture tem outros nomes e eu particularmente gosto de chamá-la de Ports and Adapters. Em nossa implementação adicionamos a interface do repository no Domain Model e essa interface representa as portas, agora na infraestrutura colocaremos os adaptadores àquelas interfaces.
Na infraestrutura eu costumo criar essa estrutura de pacote onde tenho persistence, dentro desse pacote coloco entities que são entidades do hibernate (ORM), springdata que são interfaces do Spring Data e translate que são os caras que irão converter o modelo do banco para o modelo de domínio. Como estou falando de um adaptador observe que o NegotiationRepositoryOracle implementa a interface que está no domínio e Oracle no sufixo mostra qual o drive dela. Em item acontece a mesma coisa. Como o hibernate abstrai os bancos, esse sufixo poderia ser trocado para Hibernate.
Figura 18: Infrastructure Layer
Observe que na imagem abaixo eu tenho as entidades do hibernate que irão representar o mundo do banco de dados e as interfaces do Spring Data que irão fazer o fetch das informações. Dentro de Spring Data eu tenho Hibernate e Mongo.
Figura 19: Interfaces em springdata
Vamos ver a implementação do adaptador NegotiationRepository e logo percebemos que ele recebe os repositories (técnicos) e no método findOne eu recupero as informações das 3 tabelas e converto para o meu domínio. Isso é chamado de Anti Corruption Layer, por proteger o meu domínio da estrutura do dado. Imagine que o banco mudou e agora será tudo MongoDB o que precisaríamos fazer? Trocar a implementação (adaptador) de NegotiationRepositoryOracle para uma implementacao que use o MongoDB, simples não?
Figura 20: Implementação de repository - camada de anti corrupção
Assim como trocamos a placa de rede do nosso computador, e não o computador inteiro, para se comunicar via wireless ao invés de cabo, em nosso sistema seguimos o mesmo princípio.
É comum encontrar resistências com relação a essa arquitetura porque parece que o CRUD tradicional é mais rápido de ser desenvolvido, contudo ser mais rápido no início não significa que você se manterá rápido ao longo do projeto, isso porque as coisas vão se misturando e se tornando uma grande bola de lama. E dependendo do tipo de negócio as coisas tendem a mudar com grande velocidade, hoje estamos usando um banco relacional amanhã poderemos precisar de uma engine de dados diferente, e baseada em uma estrutura que dê uma performance melhor. Olhando os projetos que você trabalha hoje seria fácil trocar de uma estrutura relacional para uma orientada a Documentos, e sem que o negócio seja afetado por isso?
Mais uma vez digo: "Ser ágil é responder rápido à mudanças". Tenha isso como estilo de vida!
Comunicação entre módulos
Aprendemos a nos comunicar com o mundo externo, agora precisamos aprender a nos comunicar com os outros módulos, porque os módulos têm regras que devem ser seguidas, caso essas regras sejam violadas, o projeto se tornará um monólito com cara de algo modulável.
Imagine que ao salvar uma negociação eu precise fazer algumas validações e uma dessas validações é verificar se o vendedor está ativo, mas lembre que no módulo de negociação eu não tenho o status do vendedor, somente o ID, como faríamos isso?
Figura 21: Validador de negociação
Esse validador será chamado antes da criação de uma negociação, observe o repository e a DSL sobre o Seller, isso é minha DSL de negócio que irá perguntar para o repository se o vendedor está ativo ou não, caso ele não esteja irá retornar um Option empty.
O repository é uma coleção e podemos perguntar qualquer coisa a essa coleção, quando estamos operando sobre atributos de modelos (coadjuvantes) que não seja o ID temos que manter isso dentro da infraestrutura e somente perguntar ao repository como no exemplo:
sellerRepository.findOne(when(seller).isActive()).isPresent()
Quando o resultado é um Optional com um Seller com o id preenchido, dizemos que esse vendedor está ativo.
Vamos implementar a interface de Seller (adaptador)
Figura 22: Seller Repository
O SellerRepositoryModule recebe a injeção do SellerService que está no módulo de Seller. O sufixo module é para indicar que esse adaptador (implementação) se comunica com outro módulo do mesmo sistema.
Um serviço sendo injetado dentro de um repository? Sim!
Pode parecer estranho mas a implementação do meu Repository chama um serviço externo, que poderia ser um HTTP, SOAP, File System etc. Como está no mesmo projeto a forma mais fácil para eu chamar o serviço de Seller, que irá retornar um Seller, com todas as informações, inclusive o status de ativo ou não, é injetar na implementação do meu repository. E amanhã, se eu tirar o módulo de seller do projeto o que precisaria ser feito no módulo de negociação? Apenas remover o SellerService e trocar por uma chamada HTTP, por exemplo. Simple não?
Reactive (pub-sub)
Agora que sabemos como se comunicar com o mundo externo temos que entender que as chamadas diretas tornam nosso módulo menos resiliente e isso se resume a fragilidade. Nesse ponto iremos usar um pouco do poder do reativo para minimizar a dependência do módulo de negociação com o módulo de vendas.
Temos um requisito que é gerar uma venda após uma negociação concluída e para resolver esse problema iremos usar outra técnica.
A imagem abaixo mostra que estamos alterando o status da negociação, que pode ser frio, morno, quente ou concluído. Quando a negociação alcançar o concluído uma venda deverá ser gerada. A Venda está em outro módulo e se eu chamá-la eu tenho que representar a venda no domínio de negociação para justificar a criação de um repository. Outro ponto é, que se eu quiser, além da venda, gerar uma notificação, enviar uma e-mail etc, como ficaria?
É muito comum ver sistemas com a injeção de vários serviços, isso quebra a coesão, desrespeita o princípio de responsabilidade única e faz com que seu módulo seja lembrado, não só pelo tema dele, mas pelas diversas coisas que ele se tornou responsável.
Figura 23: Atualização de status de negociação
É nesse momento que ao invés de eu chamar o módulo de vendas para gerar uma venda ou o módulo de comunicação para enviar um alerta no admin, ou um e-mail, eu somente digo o que fiz. No código abaixo o sistema envia uma mensagem para o BUS dizendo que aquela negociação foi fechada com sucesso.
Figura 24: Disparando evento
A interface EventHandler está no dominio e a implementação, como sempre, está na infraestrutura.
Observe que estou usando o EventBus, porque o KISS tem que ser respeitado, como os módulos estão no mesmo projeto não preciso de um RabbitMQ, ActiveMQ ou qualquer outro sistema de mensageria. O EventBus está no pacote do Guava e o mesmo cria um BUS onde a mensagem passa, e o mesmo envia para os assinantes do tópico, para entender um pouco mais clique aqui
Figura 25: Implementação de disparo de eventos
O tópico é o objeto que está sendo disparado, nesse caso NegotiationClosedWon. O módulo de vendas tem um Subscriber para esse tópico e o framework é responsável por invocar o método passando o objeto que foi enviado.
Figura 26: Inscrição em eventos de negociação
Lembre-se que assim como o módulo de negociação não pode conhecer vendas, vendas não pode conhecer mais que o ID de negociação, então é o repository que irá fazer essa ponte e converterá a negociação em venda. Para mais detalhes baixe o projeto ou poste suas dúvidas nos comentários.
Observações finais
Desenvolver aplicações orientadas ao negócio, delimitando o contexto, minimizando o acoplamento e tudo isso no mesmo projeto não tende a ser uma tarefa fácil, pois demanda um bom conhecimento de todas as técnicas envolvidas. A vantagem é que você terá menos complexidade no início do projeto e tenderá a conhecer melhor os domínios e suas relações para quando chegar a necessidade de separar, por exemplo: poderíamos remover negociações e vendas e entregar para um time continuar o desenvolvimento. Essas possibilidades são essenciais e evita que iniciemos o projeto partindo de uma estrutura complexa de micro serviços em um momento que ainda temos dúvidas com relação ao negócio e suas subdivisões gerando assim os "micro monólitos" que é um termo pejorativo para aqueles micro serviços sem propósito.
Em resumo eu não preciso quebrar meu projeto de forma desnecessária para dizer que não estou produzindo um monólito. Apesar de estar no mesmo classpath eu posso dizer que o projeto é modular, mesmo não usando os recursos de módulo do Java 9.
E é modular porque eu consigo extrair partes e colocar em outros projetos, então vou dizer que isso é uma cultura de micro serviços.
Mantenha em mente que a base do projeto é determinante para o todo. Projetos bem estruturados tendem a dar ao desenvolvedor mais ferramentas para a resolução de problemas e tudo isso funciona como a teoria da casa limpa, se a casa está limpa você toma cuidados ao entrar e se está suja e bagunçada ninguém irá se preocupar com alguma coisa, ao entrar.
E por aqui termino e até a próxima.