Você está na página 1de 168

Machine Translated by Google

Machine Translated by Google

Suje as mãos na limpeza


Arquitetura

Crie aplicativos ‘limpos’ com exemplos de código em Java

Tom Hombergs

BIRMINGHAM-MUMBAI
Machine Translated by Google

Suje as mãos na arquitetura limpa


Direitos autorais © 2023 Packt Publishing

Todos os direitos reservados. Nenhuma parte deste livro pode ser reproduzida, armazenada em um sistema de
recuperação ou transmitida de qualquer forma ou por qualquer meio, sem a permissão prévia por escrito do editor, exceto
no caso de breves citações incorporadas em artigos críticos ou resenhas.

Todos os esforços foram feitos na preparação deste livro para garantir a precisão das informações apresentadas. Contudo,
as informações contidas neste livro são vendidas sem garantia, expressa ou implícita. Nem o autor, nem a Packt Publishing
ou seus revendedores e distribuidores serão responsabilizados por quaisquer danos causados ou supostamente causados
direta ou indiretamente por este livro.

A Packt Publishing tem se esforçado para fornecer informações de marcas registradas sobre todas as empresas e
produtos mencionados neste livro através do uso apropriado de letras maiúsculas. No entanto, a Packt Publishing não
pode garantir a exatidão desta informação.

Gerente de Produto do Grupo: Gebin George

Gerente de produto editorial: Kunal Sawant


Editora Sênior: Ruvika Rao

Editor Técnico: Jubit Pincy

Editor de texto: Edição Safis

Coordenador do Projeto: Manisha Singh

Revisor: Safis Editing

Indexador: Tejal Daruwale Soni

Designer de Produção: Joshua Misquitta

Coordenadora de Marketing: Sonia Chauhan

Publicado pela primeira vez: setembro de 2019

Segunda edição: julho de 2023

Referência de produção: 1300623

Publicado por Packt Publishing Ltd.

Lugar de libré

Rua Livery, 35

Birmingham
B3 2PB, Reino Unido.

ISBN 978-1-80512-837-3

www.packtpub.com
Machine Translated by Google

À minha esposa, Rike, e aos meus filhos, Nora e Niklas, por me lembrarem regularmente que existe vida fora do
desenvolvimento de software.

–Tom Hombergs
Machine Translated by Google

Prefácio
É o paraíso dos desenvolvedores: é fácil testar a lógica do domínio, é fácil simular infraestrutura e
tecnologia, há uma separação clara entre código de domínio e código técnico, e até mesmo migrar de
uma tecnologia para outra parece fácil. Chega de discussões intermináveis sobre em qual parte do seu
código você deve implementar esse novo recurso complicado que os empresários precisarão amanhã.
Chama -se Arquitetura Limpa”, e Tom irá guiá-lo em sua jornada rumo a isso.

Por alguns anos, a base da arquitetura limpa foi documentada sob vários nomes (Arquitetura Hexagonal, portas e
adaptadores, arquitetura cebola e arquitetura limpa). A ideia básica parece simples: dois círculos concêntricos separando o
conteúdo do domínio e o técnico do software.
As dependências fluem para dentro, da tecnologia para o domínio. As classes de domínio não podem depender de classes
técnicas.

Pena que a maioria das fontes originais tenha perdido a explicação de como os pacotes e o código deveriam ser
organizados. O livro de Tom preenche perfeitamente essa lacuna. Ele usa um exemplo ilustrativo para guiá-lo em direção a
uma estrutura arquitetônica clara e altamente sustentável.

Faça um favor a si mesmo e aos seus colegas de desenvolvimento e dê uma chance à abordagem de arquitetura
limpa. Eu prometo que você não vai se arrepender!

Gernot Starke

Colônia, junho de 2023

Arquiteto de software pragmático desde 1990, fundador do arc42, cofundador do iSAQB e nerd
Machine Translated by Google

Colaboradores

Sobre o autor
Tom Hombergs é engenheiro de software, autor e nerd da simplicidade. A complexidade é a sua criptonita,
então ele trabalha duro para quebrar coisas complexas em pedaços simples que ele possa entender. Se ele
consegue entender, todos os outros também conseguem. Ele simplifica o código e também o texto, criando
artigos, livros e documentação para desenvolvedores que são agradáveis de ler. Atualmente, Tom trabalha na
Atlassian em Sydney, Austrália, onde é responsável pela Developer Experience (DX) das pilhas de tecnologia
usadas por outros desenvolvedores da Atlassian.
Machine Translated by Google

Sobre os revisores

Alexandros Trifyllis é engenheiro de software freelancer com 15 anos de experiência. Ele fez parte
de grandes projetos empresariais para os setores público, privado e europeu.

Suas áreas de interesse incluem desenvolvimento backend (Spring Boot), desenvolvimento frontend
(Angular) e diversas práticas de arquitetura (hexagonal/DDD). Ele também gosta de se envolver com
tarefas de DevOps (AWS, Terraform e Kubernetes). Finalmente, nos últimos anos, ele se interessou por
DX e Engenharia de Produtividade do Desenvolvedor (DPE) e, em geral, por pensar em como tornar
o trabalho dos desenvolvedores mais fácil e agradável.

Artem Gorbounov é um desenvolvedor Java full-stack apaixonado por arquitetura limpa e 5 anos de
experiência no setor. Atualmente trabalhando na OneUp, ele é especialista na construção de aplicações
web robustas e escaláveis. Artem possui certificação Amazon, demonstrando sua experiência em tecnologias
de computação em nuvem. Ele acredita que um verdadeiro programador full stack deve ter uma compreensão
abrangente de toda a pilha tecnológica, desde o banco de dados até a infraestrutura, com uma compreensão
clara da arquitetura da aplicação.

Gernot Starke é coach e consultor de arquitetura de software, bolsista do INNOQ,


cofundador do arc42 e iSAQB, fundador do aim42, ex-diretor técnico do Sun Microsystems
Object-Reality-Center e um nerd que gosta café à prova de balas.

Jonas Havers é engenheiro de software freelance full stack com mais de 15 anos de experiência profissional
trabalhando para empresas internacionais de comércio eletrônico. Como arquiteto de soluções e aplicativos, ele
ajuda os clientes a projetar e construir sistemas de software empresariais personalizados em grande escala que
os ajudam a responder rapidamente às mudanças e a ter mais sucesso em seu mercado. Ele é especialista no
uso de uma variedade de ferramentas, metodologias e linguagens de programação, incluindo Java, Kotlin e
JavaScript. Jonas explica, discute e implementa regularmente vários projetos e arquiteturas de software e adora
compartilhar todo o seu conhecimento e experiência com os membros de sua equipe de projeto, bem como com
seus alunos como um requisitado professor universitário convidado.

Jörg Gellien ajuda equipes de uma empresa inovadora a projetar e desenvolver aplicativos modernos e altamente
escaláveis para atender às necessidades comerciais certas. Ele é especialista em arquitetura de software e Java/
Machine Translated by Google

Desenvolvimento da primavera. As ideias de responsabilidade de ponta a ponta por um produto e o uso de serviços baseados em
nuvem são fortes impulsionadores de seu trabalho.

Jo Vanthournout é desenvolvedor e arquiteto Java há quase 20 anos. Ele teve a sorte de iniciar sua
carreira como desenvolvedor em um dos primeiros projetos de programação extrema na Bélgica. Desde
então, ele tenta viver e respirar os valores do desenvolvimento ágil. Jo tem um grande interesse em DDD
e usa seus princípios e técnicas diariamente. Ele nunca será o melhor desenvolvedor da equipe, mas ter
uma visão pragmática do domínio do problema em questão, fazer perguntas desagradáveis e responsabilizar
os membros pelos valores da equipe são tarefas que você pode confiar a ele. Ele tem uma esposa
maravilhosa e duas filhas. Quando não está programando, ele está correndo na floresta, visitando um
campo de batalha da Segunda Guerra Mundial ou jogando Minecraft com seus filhos.

K. Siva Prasad Reddy é arquiteto de software com mais de 18 anos de experiência na construção de sistemas de
software escaláveis, principalmente usando a plataforma Java.

Ele é um ávido seguidor de práticas ágeis e adota uma abordagem pragmática ao design e arquitetura de software. Ele compartilha
seu aprendizado e pensamentos em https://sivalabs.in.

Lorenzo Bettini (https://www.lorenzobettini.it) é professor associado de ciência da computação


na DISIA, Università di Firenze, Itália. Sua pesquisa abrange design, teoria e implementação de
linguagens de programação, com suporte de IDE.

É autor de mais de 90 artigos de pesquisa, publicados em conferências e revistas internacionais , de duas


edições do livro Implementing Domain-Specific Languages with Xtext and Xtend
(Packt Publishing), e o livro Test-Driven Development, Build Automation, Continuous Integration (com
Java, Eclipse e amigos) (Leanpub).

Maria Luisa Della Vedova é uma desenvolvedora de software apaixonada, dedicada a criar soluções significativas
e centradas no usuário, aprendendo e colaborando continuamente para ter um impacto positivo na vida das pessoas.

Matt Penning fornece às empresas orientação técnica e desenvolvimento de software há mais de três
décadas. Ele tem um histórico comprovado de criação de arquiteturas bem definidas e inovadoras que
resolvem problemas do mundo real e atualmente trabalha como líder técnico sênior na Cisco Systems, Inc.,
onde está imerso em desenvolvimento de microsserviços Java, qualidade de software e desenvolvimento. produtividade.
Machine Translated by Google

Mike Davidson é desenvolvedor líder e arquiteto de aplicativos. Ele trabalha com start-ups e instituições
financeiras sediadas na Nova Zelândia, no Canadá e nos EUA para ajudá-las a construir software estruturado
de forma limpa e sustentável.

Octavian Nita se interessou profissionalmente e por diversão em Java por mais de 18 anos, passando da
implementação de linguagens e automação de software para aplicativos de desktop e baseados na Web. Ele ainda
gosta muito de fazer isso.

Atualmente baseado em Bruxelas, ele ajuda órgãos da administração pública europeia a implementar as chamadas
“aplicações empresariais” usando estilos de arquitetura centrados no domínio.

Sven Woltmann é desenvolvedor Java desde os primeiros dias. Ele trabalha como desenvolvedor independente,
coach e instrutor de cursos, especializado em aplicativos corporativos Java altamente escaláveis, otimização
de algoritmos, código limpo e arquitetura limpa. Ele também compartilha seu conhecimento por meio de vídeos,
um boletim informativo e seu blog, HappyCoders.eu.

Thomas Buss é consultor de TI da codecentric na Alemanha. Ele ajuda as equipes a reduzir a complexidade dos
produtos de software e, assim, acelerar o processo de desenvolvimento. Tendo experiência em Java, ele também gosta
de explorar outros paradigmas e linguagens. Ele também está interessado em modelagem orientada por domínio,
tecnologias sem servidor e formas de reduzir a pegada de carbono dos sistemas. Além disso, ele gosta de programas
de TV que começam com “Star”.

Vivek Ravikumar atualmente trabalha como membro da equipe técnica do PayPal Índia e tem quase uma década de
experiência no desenvolvimento de aplicativos web empresariais. Ele realizou vários seminários e palestras em
instituições educacionais e universidades na Índia, defendendo a importância e as melhores práticas envolvidas no
ciclo de vida de desenvolvimento de software, orientando estudantes e promovendo o conhecimento industrial.

Recentemente, ele foi reconhecido como uma lenda de Jakarta EE, MicroProfile e da plataforma Payara por
garantir o primeiro lugar no primeiro hackathon global Payara na construção de um aplicativo web corporativo.

Wim Deblauwe é um desenvolvedor Java freelancer com mais de 20 anos de experiência em Java. Ele é o autor de
Taming Thymeleaf e Guia prático para construir um back-end de API com Spring Boot. Ele também iniciou e contribuiu
para vários projetos de código aberto, como error-handling-spring-boot-starter e testcontainers-cypress.
Machine Translated by Google

Índice

Prefácio xv

1
Capacidade de manutenção 1

O que a manutenção mesmo A manutenibilidade apoia a tomada de


significar? 1 decisões 6

A manutenibilidade permite Mantendo a manutenibilidade 7


funcionalidade 2

A manutenibilidade gera alegria ao


desenvolvedor 4

2
O que há de errado com camadas? 9

Eles promovem design orientado a Eles escondem os casos 13


banco de dados 10 14
de uso Eles dificultam o trabalho
Eles são propensos a atalhos 12
paralelo Como isso me ajuda a
Eles ficam difíceis de testar 12 construir software sustentável? 15

3
Invertendo Dependências 17

O Princípio da Responsabilidade Única 17 Arquitetura Limpa 20

Uma história sobre efeitos colaterais 18 Arquitetura Hexagonal Como 22

A inversão de dependência isso me ajuda a construir software


Princípio 19 sustentável? 24
Machine Translated by Google

x Índice

4
Código de organização 25

Organizando por Camada 25 A função da injeção de dependência 30

Organizando por recurso 26 Como isso me ajuda a construir


software sustentável? 32
Uma estrutura de pacote
arquitetonicamente expressiva 27

5
Implementando um caso de uso 33

Implementando o modelo de Modelo de domínio rico versus


domínio 33 anêmico 44

Um caso de uso em poucas palavras 35 Diferentes modelos de saída para


37 diferentes casos de uso 45
Validando entrada

O poder dos construtores 40 E quanto ao uso somente leitura


casos? 45
Diferentes modelos de entrada
para diferentes casos de uso 41 Como isso me ajuda a construir
software sustentável? 46
Validando regras de negócios 42

6
Implementando um Adaptador Web 47

Inversão de dependência 47 Como isso me ajuda a construir


49 software sustentável? 53
Responsabilidades de um adaptador web

Controladores de fatiamento 50

7
Implementando um adaptador de persistência 55

Inversão de dependência 55 Fatiamento de adaptadores de persistência 58

Responsabilidades de um adaptador de Um exemplo com Spring Data


persistência 56 APP 60

Fatiamento de interfaces de porta 57


Machine Translated by Google

Índice XI

E o banco de dados Como isso me ajuda a construir


transações? 65 software sustentável? 66

8
Testando Elementos de Arquitetura 67

A pirâmide de teste 67 Testando um adaptador de


persistência com testes de integração 73
Testando uma entidade de domínio com unidade
testes 69 Testando caminhos principais com sistema
70 testes 75
Testando um caso de uso com testes unitários
Quantos testes são suficientes? 79
Testando um Web Adaptor com
Testes de Integração 71 Como isso me ajuda a construir
software sustentável? 80

9
Mapeamento entre limites 81

A estratégia “Sem Mapeamento” 82 Quando usar qual estratégia de


83 mapeamento? 86
A estratégia de mapeamento “bidirecional”

A estratégia de mapeamento “completo” 84 Como isso me ajuda a construir


software sustentável? 87
A estratégia de mapeamento “unidirecional” 85

10
Montando o Aplicativo 89

Por que se preocupar com a montagem? 89 Montando via Java Config do Spring
91 Como 94
Montagem via código simples

Montagem via varredura de classpath do isso me ajuda a construir software


92 sustentável? 96
Spring

11
Tomando atalhos conscientemente 97

Por que os atalhos são como janelas A responsabilidade de começar limpo


quebradas 97 98
Machine Translated by Google

xii Índice

Compartilhando modelos entre casos de uso 99 Ignorando serviços 102

Usando entidades de domínio como Como isso me ajuda a criar


modelo de entrada ou saída 100 software sustentável? 103

Ignorando portas de entrada 101

12
Aplicando Limites de Arquitetura 105

Limites e dependências 105 Construir artefatos 112

Modificadores de visibilidade 107 Como isso me ajuda a construir


109 software sustentável? 114
Função de fitness pós-compilação

13
Gerenciando vários contextos limitados 117

Um hexágono por limite Apropriadamente acoplado e limitado


contexto? 118 contextos 121

Contextos limitados desacoplados 120 Como isso me ajuda a construir


software sustentável? 123

14
Uma abordagem baseada em componentes para arquitetura de software 125

Modularidade através de componentes 126 Aplicando limites de


componentes 130
Estudo de caso – construindo um “Check
Componente do motor” 128 Como isso me ajuda a construir
software sustentável? 132

15
Decidindo sobre um estilo de arquitetura 133

Comece simples 133 Confie na sua experiência 134

Evolua o domínio 134 Depende 135


Machine Translated by Google

Índice xiii

Índice 137

Outros livros que você pode gostar 142


Machine Translated by Google
Machine Translated by Google

Prefácio

Se você leu este livro, você se preocupa com a arquitetura do software que está construindo. Você
deseja que seu software não apenas atenda aos requisitos explícitos do cliente, mas também aos
requisitos ocultos de manutenção e aos seus próprios requisitos relativos à estrutura e à estética.

É difícil cumprir esses requisitos porque os projetos de software (ou projetos em geral, nesse caso) geralmente
não saem como planejado. Os gerentes estabelecem prazos para toda a equipe do projeto1, parceiros externos
constroem suas APIs de maneira diferente do que prometeram, e os produtos de software dos quais dependemos não
funcionam conforme o esperado.

E há nossa própria arquitetura de software. Foi tão legal no começo. Tudo estava claro e lindo. Então os prazos nos
pressionaram a tomar atalhos. Agora, os atalhos são tudo o que resta da arquitetura e leva cada vez mais tempo para
entregar novos recursos.

Nossa arquitetura baseada em atalhos torna difícil reagir a uma API que teve que ser alterada porque um parceiro externo
estragou tudo. Parece mais fácil simplesmente enviar nosso gerente de projeto para a batalha com aquele parceiro para
dizer-lhe para entregar a API que combinamos.

Agora, desistimos de todo o controle sobre a situação. Muito provavelmente, uma das seguintes coisas acontecerá:

• O gerente do projeto não é forte o suficiente para vencer a batalha contra o parceiro externo

• O parceiro externo encontra uma lacuna nas especificações da API, provando que estão certos

• O parceiro externo precisa de mais <insira o número aqui> meses para corrigir a API

Tudo isso leva ao mesmo resultado – temos que mudar nosso código rapidamente porque o prazo está se aproximando.

Adicionamos outro atalho.

Em vez de permitir que fatores externos governem o estado de nossa arquitetura de software, este livro assume a postura
de assumirmos o controle por nós mesmos. Ganhamos esse controle criando uma arquitetura que torna o software flexível,
como “flexível”, “extensível” e “adaptável”. Essa arquitetura facilitará a reação a fatores externos e aliviará muita pressão.

1 A palavra “prazo” provavelmente tem origem no século XIX e descrevia uma linha traçada em
torno de uma prisão ou de um campo de prisioneiros. Um prisioneiro que cruzou essa linha
foi baleado. Pense nesta definição na próxima vez que alguém “traçar um prazo” perto de
você... certamente abrirá novas perspectivas. Consulte https://www.merriam-webster.com/
words-at-play/your expired-wont-kill-you.
Machine Translated by Google

xvi Prefácio

O objetivo deste livro


Escrevi este livro porque fiquei desapontado com a praticidade dos recursos disponíveis em estilos de
arquitetura centrados em domínio, como a Arquitetura Limpa de Robert C. Martin e a Arquitetura
Hexagonal de Alistair Cockburn.

Muitos livros e recursos online explicam conceitos valiosos, mas não como podemos realmente implementá-los.

Provavelmente porque existe mais de uma maneira de implementar qualquer estilo de arquitetura.

Com este livro, estou tentando preencher essa lacuna fornecendo uma discussão prática sobre código sobre como criar
uma aplicação web no estilo Arquitetura Hexagonal ou “Portas e Adaptadores”. Para atingir esse objetivo, os exemplos
de código e conceitos discutidos neste livro fornecem minha interpretação de como implementar uma Arquitetura
Hexagonal. Certamente existem outras interpretações por aí, e não afirmo que a minha seja oficial.

Eu certamente espero, entretanto, que você se inspire nos conceitos deste livro para que possa criar sua própria
interpretação da Arquitetura Hexagonal/Limpa.

Para quem é este livro

Este livro é destinado a desenvolvedores de software de todos os níveis de experiência envolvidos na criação de aplicações web.

Como desenvolvedor júnior, você aprenderá como projetar componentes de software e concluir aplicativos de maneira
limpa e sustentável. Você também aprenderá alguns argumentos sobre quando aplicar uma determinada técnica. No
entanto, você deve ter participado da construção de uma aplicação web no passado para aproveitar ao máximo este livro.

Se você é um desenvolvedor experiente, vai gostar de comparar os conceitos do livro com sua própria maneira
de fazer as coisas e, esperançosamente, incorporar pedaços em seu próprio estilo de desenvolvimento de software.

Os exemplos de código neste livro estão em Java e Kotlin, mas todas as discussões são igualmente aplicáveis a outras
linguagens de programação orientadas a objetos. Se você não é um programador Java, mas consegue ler código
orientado a objetos em outras linguagens, você ficará bem. Nos poucos lugares onde precisarmos de algumas
especificações de Java ou de estrutura, irei explicá-las.

O aplicativo de exemplo
Para ter um tema recorrente ao longo do livro, a maioria dos exemplos de código mostra o código de um exemplo de
aplicativo da Web para transferência de dinheiro on-line. Vamos chamá-lo de “BuckPal”.2

2 BuckPal: uma rápida pesquisa online revelou que uma empresa chamada PayPal roubou minha ideia e até copiou
parte do nome. Brincadeiras à parte: tente encontrar um nome parecido com “PayPal” que não seja o nome de uma
empresa existente. Isto é hilário!
Machine Translated by Google

Prefácio xvii

O aplicativo BuckPal permite ao usuário registrar uma conta, transferir dinheiro entre contas e visualizar as atividades
(depósitos e retiradas) da conta.

Não sou especialista em finanças, portanto, não julgue o código de exemplo com base na correção legal ou funcional. Em
vez disso, julgue pela estrutura e capacidade de manutenção.

A maldição dos exemplos de aplicativos para livros de engenharia de software e recursos on-line é que eles são simples
demais para destacar os problemas do mundo real com os quais lutamos todos os dias. Por outro lado, um exemplo de
aplicação deve permanecer simples o suficiente para transmitir eficazmente os conceitos discutidos.

Espero ter encontrado um equilíbrio entre “muito simples” e “muito complexo” à medida que discutimos os casos de uso
do aplicativo BuckPal ao longo deste livro.

O código do aplicativo de exemplo pode ser encontrado no GitHub.3

Baixe as imagens coloridas


Também fornecemos um arquivo PDF que contém imagens coloridas das capturas de tela e diagramas usados neste livro.
Verifique as notas no final do livro.4

Entrar em contato

Se você tem algo a dizer sobre este livro, eu adoraria ouvir! Entre em contato comigo diretamente por e-mail para
tom@reflectoring.io ou no Twitter via @TomHombergs.

Feedback geral: Se você tiver dúvidas sobre qualquer aspecto deste livro, envie-nos um e-mail para customercare@
packtpub.com e mencione o título do livro no assunto da sua mensagem.

Errata: Embora tenhamos tomado todos os cuidados para garantir a precisão do nosso conteúdo, erros acontecem.
Se você encontrou um erro neste livro, ficaríamos gratos se você nos relatasse isso. Visite www.packtpub.com/support/
errata e preencha o formulário.

Pirataria: Se você encontrar cópias ilegais de nossos trabalhos, sob qualquer forma, na Internet, ficaríamos gratos se
você nos fornecesse o endereço do local ou o nome do site. Entre em contato conosco em copyright@packt.com com um
link para o material.

Se você estiver interessado em se tornar um autor: Se houver um tópico no qual você tenha
experiência e estiver interessado em escrever ou contribuir para um livro, visiteauthors.packtpub.com.

3 O repositório BuckPal GitHub: https://github.com/thombergs/buckpal.


4 PDF com imagens coloridas utilizadas neste livro: https://packt.link/eBKMn.
Machine Translated by Google

XVIII Prefácio

Compartilhe seus pensamentos

Depois de ler Suja as mãos na arquitetura limpa – segunda edição, adoraríamos ouvir sua
opinião! Clique aqui para ir direto para a página de resenhas deste livro na Amazon e compartilhar
seus comentários.

Sua avaliação é importante para nós e para a comunidade de tecnologia e nos ajudará a garantir que estamos entregando
conteúdo de excelente qualidade.
Machine Translated by Google

Prefácio XIX

Baixe uma cópia gratuita em PDF deste livro


Obrigado por adquirir este livro!

Você gosta de ler em qualquer lugar, mas não consegue levar seus livros impressos para qualquer lugar?

A compra do seu e-book não é compatível com o dispositivo de sua escolha?

Não se preocupe, agora com cada livro Packt você obtém uma versão em PDF sem DRM desse livro, sem nenhum custo.

Leia em qualquer lugar, em qualquer lugar, em qualquer dispositivo. Pesquise, copie e cole códigos de seus livros técnicos
favoritos diretamente em seu aplicativo.

As vantagens não param por aí, você pode obter acesso exclusivo a descontos, newsletters e ótimo conteúdo gratuito em sua
caixa de entrada diariamente

Siga estas etapas simples para obter os benefícios:

1. Digitalize o código QR ou visite o link abaixo

https://packt.link/free-ebook/9781805128373

2. Envie seu comprovante de compra. 3.

Pronto! Enviaremos seu PDF grátis e outros benefícios diretamente para seu e-mail
Machine Translated by Google
Machine Translated by Google

1
Capacidade de manutenção

Este livro é sobre arquitetura de software. Uma das definições de arquitetura é a estrutura de um sistema
ou processo. No nosso caso, é a estrutura de um sistema de software.

A arquitetura está projetando esta estrutura com um propósito. Estamos projetando conscientemente nosso
sistema de software para atender a determinados requisitos. Existem requisitos funcionais que o software deve
cumprir para criar valor para seus usuários. Sem funcionalidade, o software não vale nada, porque não produz valor.

Existem também requisitos de qualidade (também chamados de requisitos não funcionais) que o software deve cumprir
para ser considerado de alta qualidade pelos seus usuários, desenvolvedores e partes interessadas. Um desses requisitos
de qualidade é a capacidade de manutenção.

O que você diria se eu lhe dissesse que a manutenibilidade como atributo de qualidade, de certa forma, é mais
importante que a funcionalidade e que devemos projetar nosso software para manutenibilidade acima de todo o resto?
Depois de estabelecermos a manutenibilidade como uma qualidade importante, usaremos o restante deste livro para
explorar como podemos melhorar a manutenibilidade de nosso software aplicando os conceitos de Arquitetura Limpa
e Hexagonal.

O que significa manutenção?


Antes de você me considerar um lunático e começar a procurar opções para devolver este livro, deixe-me explicar o
que quero dizer com manutenibilidade.

A manutenibilidade é apenas um dos muitos requisitos de qualidade que potencialmente constituem uma arquitetura
de software. Pedi ao ChatGPT uma lista de requisitos de qualidade e este é o resultado:

• Escalabilidade

• Flexibilidade

• Capacidade de manutenção

• Segurança

• Confiabilidade
Machine Translated by Google

2 Capacidade de manutenção

• Modularidade

• Desempenho

• Interoperabilidade

• Testabilidade

• Custo-benefício

A lista não termina aqui.1

Como arquitetos de software, projetamos nosso software para atender aos requisitos de qualidade mais importantes
para o software. Para uma aplicação comercial de alto rendimento, podemos nos concentrar na escalabilidade e na
confiabilidade. Para uma aplicação que lide com informações de identificação pessoal na Alemanha, talvez queiramos
nos concentrar na segurança.

Acho que é errado agrupar a manutenibilidade com o restante dos requisitos de qualidade porque a manutenibilidade é
especial. Se o software puder ser mantido, isso significa que é fácil alterá-lo. Se for fácil de mudar, é flexível e
provavelmente modular. Provavelmente também é econômico, porque mudanças fáceis significam mudanças baratas.
Se for sustentável, provavelmente poderemos evoluí-lo para ser escalonável, seguro, confiável e de alto desempenho,
caso seja necessário. Podemos alterar o software para ser interoperável com outros sistemas porque é fácil de alterar.
Por último, mas não menos importante, a manutenção implica testabilidade porque o software sustentável é
provavelmente projetado a partir de componentes menores e mais simples que facilitam os testes.

Você pode ver o que eu fiz aqui. Pedi à IA uma lista de requisitos de qualidade e, em seguida, vinculei todos eles à
capacidade de manutenção. Eu provavelmente poderia vincular muitos outros requisitos de qualidade à capacidade de
manutenção com argumentos igualmente plausíveis. É um pouco simplista, claro, mas o cerne da questão é verdade:
se o software for sustentável, será mais fácil evoluir em qualquer direção, funcional ou não-funcionalmente. E todos
sabemos que mudanças são comuns durante a vida de um sistema de software.

A manutenibilidade permite funcionalidade


Agora, voltando à minha afirmação de que a manutenibilidade é mais importante que a funcionalidade do início deste
capítulo.

Se você perguntar a um responsável pelo produto o que é mais importante em um projeto de software, ele lhe dirá que
o valor que o software oferece aos seus usuários é o mais importante. Software que não agrega valor aos seus usuários
significa que os usuários não pagam por ele. E sem usuários pagantes, não temos um modelo de negócios funcional,
que é a principal medida de sucesso no mundo dos negócios.

1 Para obter inspiração sobre qualidade de software (que foi criado por humanos, e não por um modelo de linguagem), dê
uma olhada em https://quality.arc42.org/.
Machine Translated by Google

A manutenibilidade permite funcionalidade 3

Portanto, nosso software precisa agregar valor. Mas não deve agregar valor às custas da capacidade de manutenção.2
Pense em como é muito mais eficiente e divertido adicionar funcionalidade a um sistema de software que é facilmente
alterável em comparação com um sistema de software onde você tem que abrir caminho através de uma linha de
código por vez! Tenho certeza de que você trabalhou em um daqueles projetos de software onde há tanto lixo e ritual
que leva dias ou semanas para construir um recurso que você acha que não deve levar mais do que algumas horas
para ser concluído.

Dessa forma, a manutenibilidade é um importante apoiador da funcionalidade. A má manutenção significa que as


alterações na funcionalidade se tornam cada vez mais caras ao longo do tempo, como mostra a Figura 1.1:

Figura 1.1 – Um sistema de software sustentável tem um custo de vida útil


menor do que um sistema de software não tão sustentável

Em um sistema de software cuja manutenção não é tão fácil, as mudanças na funcionalidade logo se tornarão tão caras
que a mudança será um incômodo. O pessoal do produto reclamará com os engenheiros sobre o custo das mudanças.
Os engenheiros se defenderão dizendo que o envio de novos recursos sempre teve maior prioridade do que aumentar
a capacidade de manutenção. A probabilidade de conflito aumenta com o custo da mudança.

A manutenibilidade é uma chupeta. É inversamente proporcional ao custo da mudança e, portanto, à probabilidade de


conflito. Você já pensou em adicionar capacidade de manutenção a um sistema de software para evitar conflitos? Acho
que é um bom investimento por si só.

2 No contexto deste livro, uso o termo “manutenção” como sinônimo de “mutabilidade de uma base de código”. Consulte
também https://quality.arc42.org/qualities/maintainability
para algumas definições de manutenibilidade (todas relacionadas à alteração do software).
Machine Translated by Google

4 Capacidade de manutenção

Mas e aqueles grandes sistemas de software que são bem-sucedidos apesar da má manutenção? É verdade
que existem sistemas de software comercialmente bem-sucedidos que dificilmente são mantidos. Trabalhei em
sistemas em que adicionar um único campo a um formulário é um projeto que leva semanas do tempo do
desenvolvedor, e o cliente pagou felizmente um prêmio pelo meu tempo.

Esses sistemas geralmente se enquadram em uma (ou ambas) de duas categorias:

• Eles estão no fim de sua vida, onde as mudanças no sistema são poucas e espaçadas

• Eles são apoiados por uma empresa financeiramente abastada que está disposta a investir dinheiro no problema

Mesmo no caso de uma empresa ter muito dinheiro para gastar, a empresa percebe que pode reduzir a taxa de manutenção investindo
em manutenibilidade. Então, normalmente, já existem iniciativas em andamento para tornar o software mais sustentável.

Devemos sempre nos preocupar com a capacidade de manutenção do software que estamos criando para que ele não se degrade e
se transforme na temida grande bola de lama, mas se o nosso software não se enquadrar em uma das duas categorias mencionadas
anteriormente, devemos nos preocupar ainda mais .

Isso significa que temos que gastar muito tempo planejando uma arquitetura sustentável antes mesmo de
começarmos a programar? Temos que fazer um grande design antecipado (BDUF), que muitas vezes é
considerado sinônimo de metodologia em cascata? Não, nós não. Mas precisamos fazer algum design antecipadamente
(devemos chamá-lo de SDUF?) para inculcar uma semente de manutenção no software, o que pode facilitar a
evolução da arquitetura até onde ela precisa estar ao longo do tempo.

Parte desse design inicial é escolher um estilo de arquitetura que defina as proteções do software que estamos construindo. Este livro
irá ajudá-lo a decidir se uma arquitetura Limpa – ou Portas e Adaptadores/Hexagonal – é adequada para o seu contexto.

A manutenibilidade gera alegria ao desenvolvedor


Como desenvolvedor, você prefere trabalhar em software onde as mudanças são fáceis ou em software onde as mudanças são
difíceis? Não responda; é uma pergunta retórica.

Além da influência direta no custo da mudança, a manutenibilidade tem outro benefício: deixa os desenvolvedores felizes (ou,
dependendo do projeto atual em que estão trabalhando, pelo menos os deixa menos tristes).

O termo que quero usar para descrever essa felicidade é alegria do desenvolvedor. Também é conhecido como experiência do
desenvolvedor ou capacitação do desenvolvedor. Seja qual for o nome que chamamos, significa que fornecemos o contexto de
que os desenvolvedores precisam para fazer bem seu trabalho.

A alegria do desenvolvedor está diretamente relacionada à produtividade do desenvolvedor. Em geral, se os desenvolvedores


estiverem satisfeitos, eles trabalharão melhor. E se fizerem um bom trabalho, serão mais felizes. Há uma correlação bidirecional entre
a alegria do desenvolvedor e a produtividade do desenvolvedor:
Machine Translated by Google

A manutenibilidade gera alegria ao desenvolvedor 5

Figura 1.2 – A alegria do desenvolvedor influencia a produtividade do desenvolvedor e vice-versa

Essa correlação foi reconhecida na estrutura SPACE para produtividade do desenvolvedor.3 Embora o SPACE
não forneça uma resposta fácil sobre como medir a produtividade do desenvolvedor, ele fornece cinco categorias
para tais métricas, para que possamos escolher conscientemente um conjunto de métricas que cubra todas essas
categorias. para melhor medir a produtividade do desenvolvedor no contexto de nossa empresa e projetos. Uma
dessas categorias (o S no ESPAÇO) é satisfação e bem-estar, que traduzi como alegria do desenvolvedor neste capítulo.

A alegria do desenvolvedor não só leva a uma melhor produtividade, mas naturalmente também leva a uma melhor retenção.
Um desenvolvedor que gosta de seu trabalho permanecerá na empresa. Ou melhor, um desenvolvedor que não gosta de
seu trabalho tem maior probabilidade de partir para pastagens mais verdes.

Então, onde entra a sustentabilidade em cena? Bem, se nosso sistema de software puder ser mantido, precisaremos de
menos tempo para implementar uma mudança, então seremos mais produtivos. Além disso, se nosso sistema de software
puder ser mantido, teremos mais prazer em fazer alterações porque é mais eficiente e podemos nos orgulhar mais dele.
Mesmo que nosso software não seja tão sustentável quanto gostaríamos (o que é uma tautologia, para ser honesto), mas
tivermos a oportunidade de melhorar a capacidade de manutenção ao longo do tempo, seremos mais felizes e mais
produtivos. Se estivermos felizes, é mais provável que fiquemos.

Expresso em um diagrama, fica assim:

Figura 1.3 – A sustentabilidade influencia diretamente a alegria e a produtividade do

desenvolvedor, enquanto a alegria do desenvolvedor influencia a retenção

3 The SPACE of Developer Productivity por Nicole Forsgren et al., 6 de março de 2021. “SPACE” significa satisfação e bem-estar, desempenho,

atividade, comunicação e colaboração, e eficiência e fluxo. Consulte https://queue.acm.org/detail.cfm?id=3454124.


Machine Translated by Google

6 Capacidade de manutenção

A manutenibilidade apoia a tomada de decisões


Ao construir um sistema de software, resolvemos problemas todos os dias. Para a maioria dos problemas que
enfrentamos, existe mais de uma solução. Temos que tomar decisões para escolher entre essas soluções.

Copiamos esse trecho de código para o novo recurso que estamos construindo? Nós mesmos criamos nossos
objetos ou usamos uma estrutura de injeção de dependência? Usamos um construtor sobrecarregado para criar
este objeto ou criamos um construtor?

Muitas dessas decisões nem tomamos conscientemente. Apenas aplicamos um padrão ou princípio que usamos
antes e que nossa intuição diz que funcionará na situação atual, como segue:

• Aplicamos não se repita (DRY) quando encontramos duplicação de código

• Usamos injeção de dependência para tornar o código mais testável

• Apresentamos um construtor para simplificar a criação de um objeto

Se dermos uma olhada nesses e em muitos outros padrões bem conhecidos, qual é o seu efeito? Em muitos casos,
o principal efeito é que eles tornam o código mais fácil de alterar no futuro (ou seja, tornam-no mais sustentável). A
capacidade de manutenção está incorporada em muitas das decisões que tomamos automaticamente todos os dias!

Podemos tirar vantagem disso mesmo quando enfrentamos decisões mais difíceis que exigem mais do que apenas
aplicar um padrão predefinido. Sempre que tivermos que decidir entre várias opções, podemos escolher aquela que
facilita a alteração do código no futuro. 4 Não há mais agonia entre diferentes opções. Apenas
escolhemos aquele que mais aumenta a capacidade de manutenção. Expresso como um diagrama, é muito simples:

Figura 1.4 – A manutenibilidade influencia a tomada de decisão

4 Numa palestra de 2022 com o mesmo nome, (Pragmático) Dave Thomas chamou o princípio de tomar decisões
com base na mutabilidade de “Uma regra para governar todos”. Não encontrei a palestra online, mas espero que
ele a adicione ao seu site em algum momento. Consulte https://pragdave.me/
talks-and-interviews.html.
Machine Translated by Google

Mantendo a manutenibilidade 7

Como a maioria dos princípios, esta é uma generalização, é claro. Num determinado contexto, a decisão correta
pode ser escolher a opção que não melhora a manutenibilidade ou mesmo reduz a manutenibilidade. Mas, como
regra padrão à qual recorrer, escolher a sustentabilidade é um guia que simplifica a tomada de decisões diárias.

Mantendo a manutenibilidade
Tudo bem, presumo que você acredita em mim que a capacidade de manutenção influencia positivamente a alegria, a produtividade
e a tomada de decisões do desenvolvedor. Como sabemos que as alterações que fazemos em nossa base de código aumentam (ou
pelo menos não diminuem) a capacidade de manutenção? Como gerenciamos a manutenção ao longo do tempo?

A resposta a essa pergunta é criar e manter uma arquitetura que facilite a criação de código sustentável. Uma boa arquitetura facilita
a navegação na base de código. Em uma base de código facilmente navegável, é muito fácil modificar recursos existentes ou
adicionar novos recursos. As dependências entre os componentes do nosso aplicativo são claras e não confusas. Em resumo, uma
boa arquitetura aumenta a capacidade de manutenção:

Figura 1.5 – Arquitetura de software influencia a manutenibilidade

Por extensão, uma boa arquitetura aumenta a alegria e a produtividade do desenvolvedor, a retenção do desenvolvedor e a tomada
de decisões. Poderíamos continuar e encontrar ainda mais coisas influenciadas direta ou indiretamente pela arquitetura de software.
Machine Translated by Google

8 Capacidade de manutenção

Essa correlação significa que devemos pensar um pouco em como estruturamos nosso código. Como agrupamos
nossos arquivos de código em componentes? Como gerenciamos as dependências entre esses componentes?
Quais dependências são necessárias e quais devem ser desencorajadas para manter a base de código flexível
para mudanças? Isso nos leva ao propósito deste livro. Este livro mostra uma maneira de estruturar uma base
de código para torná-la sustentável. O estilo de arquitetura descrito neste livro é uma forma de implementar uma
Arquitetura Limpa/Hexagonal. No entanto , esse estilo de arquitetura não é uma solução mágica para resolver
todos os problemas de construção de software. Como aprenderemos no Capítulo 15, Decidindo sobre um estilo
de arquitetura, ele não é adequado para todos os tipos de aplicativos de software.

Encorajo você a pegar o que aprender neste livro, brincar com as ideias, modificá-las para torná-las suas e depois
adicioná-las à sua caixa de ferramentas para aplicá-las quando parecerem adequadas em um determinado contexto.
Cada um dos capítulos a seguir termina com uma seção intitulada Como isso me ajuda a construir software sustentável?
Esta seção resumirá as ideias principais de cada capítulo e esperamos ajudá-lo a tomar decisões relativas à
arquitetura de seus projetos de software atuais ou futuros.
Machine Translated by Google

2
O que há de errado com camadas?

Provavelmente você já desenvolveu um aplicativo em camadas (web). Você pode até estar
fazendo isso em seu projeto atual agora.

O pensamento em camadas foi ensinado em aulas de ciência da computação, tutoriais e práticas recomendadas.
Foi até ensinado em livros.1

Figura 2.1 – Uma arquitetura de aplicação web convencional consiste em uma

camada web, uma camada de domínio e uma camada de persistência

A Figura 2.1 mostra uma visão de alto nível da arquitetura de três camadas muito comum. Temos uma camada web
que recebe solicitações e as encaminha para um serviço na camada de domínio. 2 O serviço executa alguma
lógica de negócios e chama componentes da camada de persistência para consultar ou modificar o estado atual
de nossas entidades de domínio no banco de dados.

1 Camadas como padrão são, por exemplo, ensinadas em Padrões de Arquitetura de Software por Mark Richards,
O'Reilly, 2015.
2 Domínio versus negócio: neste livro, uso os termos “domínio” e “negócio” como sinônimos.
A camada de domínio ou camada de negócios é o local no código que resolve os problemas de negócios, em
oposição ao código que resolve problemas técnicos, como persistir coisas em um banco de dados ou processar
solicitações da web.
Machine Translated by Google

10 O que há de errado com camadas?

Você sabe o que? As camadas são um padrão de arquitetura sólido! Se acertarmos, seremos capazes de construir
uma lógica de domínio que seja independente da web e das camadas de persistência. Podemos trocar as tecnologias
da web ou de persistência sem afetar nossa lógica de domínio, se necessário. Também podemos adicionar novos
recursos sem afetar os recursos existentes.

Com uma boa arquitetura em camadas, mantemos nossas opções abertas e somos capazes de nos adaptar rapidamente às
mudanças nos requisitos e aos fatores externos (como o fato de nosso fornecedor de banco de dados dobrar seus preços da
noite para o dia). Uma boa arquitetura em camadas pode ser mantida.

Então, o que há de errado com as camadas?

Na minha experiência, uma arquitetura em camadas é muito vulnerável a mudanças, o que dificulta sua manutenção.
Ele permite que dependências ruins se insinuem e tornem o software cada vez mais difícil de alterar ao longo do tempo.
As camadas não fornecem proteções suficientes para manter a arquitetura no caminho certo. Precisamos confiar demais na
disciplina e diligência humana para mantê-lo sustentável.

Nas seções a seguir, direi por quê.

Eles promovem design orientado a banco de dados


Pela sua própria definição, a base de uma arquitetura convencional em camadas é o banco de dados. A camada web
depende da camada de domínio, que por sua vez depende da camada de persistência e, portanto, do banco de dados. Tudo
se baseia na camada de persistência. Isto é problemático por vários motivos.

Vamos dar um passo atrás e pensar no que estamos tentando alcançar com quase todos os aplicativos que estamos
construindo. Normalmente, tentamos criar um modelo de regras ou “políticas” que regem os negócios para facilitar a interação
dos usuários com eles.

Estamos principalmente tentando modelar o comportamento, não o estado. Sim, o estado é uma parte importante de qualquer
aplicação, mas o comportamento é o que muda o estado e, portanto, impulsiona o negócio!

Então, por que estamos fazendo do banco de dados a base da nossa arquitetura e não da lógica do domínio?

Pense nos últimos casos de uso que você implementou em qualquer aplicativo. Você começou implementando a lógica de
domínio ou a camada de persistência? Provavelmente, você pensou em como seria a estrutura do banco de dados e só
então passou a implementar a lógica do domínio sobre ela.

Isso faz sentido em uma arquitetura convencional em camadas, já que seguimos o fluxo natural de dependências. Mas não
faz absolutamente nenhum sentido do ponto de vista empresarial! Deveríamos construir a lógica do domínio antes de construir
qualquer outra coisa! Queremos descobrir se entendemos as regras de negócios corretamente. E somente quando soubermos
que estamos construindo a lógica de domínio correta é que devemos prosseguir para construir uma persistência e uma
camada web em torno dela.
Machine Translated by Google

Eles promovem design orientado a banco de dados 11

Uma força motriz nessa arquitetura centrada em banco de dados é o uso de estruturas de mapeamento objeto-
relacional (ORM) . Não me interpretem mal, adoro essas estruturas e trabalho com elas regularmente. Mas se
combinarmos uma estrutura ORM com uma arquitetura em camadas, seremos facilmente tentados a misturar regras
de negócios com aspectos de persistência.

Figura 2.2 – Usar as entidades de banco de dados na camada de domínio leva a um

forte acoplamento com a camada de persistência

Normalmente, temos entidades gerenciadas por ORM como parte da camada de persistência, conforme mostrado na Figura
2.2. Como uma camada pode acessar as camadas abaixo dela, a camada de domínio tem permissão para acessar essas
entidades. E se for permitido usá-los, ele irá usá-los em algum momento.

Isso cria um forte acoplamento entre a camada de domínio e a camada de persistência. Nossos serviços de negócios usam o
modelo de persistência como modelo de negócios e precisam lidar não apenas com a lógica do domínio, mas também com
carregamento rápido versus carregamento lento, transações de banco de dados, limpeza de caches e tarefas de limpeza
semelhantes.3

O código de persistência está virtualmente fundido no código de domínio e, portanto, é difícil alterar um sem o outro. Isso é o
oposto de ser flexível e manter as opções abertas, que deveria ser o objetivo da nossa arquitetura.

3 Em seu livro seminal Refactoring (Pearson, 2018), Martin Fowler chama esse sintoma de “ mudança divergente”: ter que
alterar partes aparentemente não relacionadas do código para implementar um único recurso. Este é um cheiro de código que
deve desencadear uma refatoração.
Machine Translated by Google

12 O que há de errado com camadas?

Eles são propensos a atalhos


Em uma arquitetura convencional em camadas, a única regra global é que, a partir de uma determinada camada, só
podemos acessar componentes da mesma camada ou de uma camada abaixo. Pode haver outras regras com as quais
uma equipe de desenvolvimento tenha concordado e algumas delas podem até ser aplicadas por ferramentas, mas o
próprio estilo de arquitetura em camadas não nos impõe essas regras.

Portanto, se precisarmos de acesso a um determinado componente em uma camada acima da nossa, podemos simplesmente empurrar
o componente para baixo em uma camada e teremos permissão para acessá-lo. Problema resolvido. Fazer isso uma vez pode ser bom.
Mas fazer isso uma vez abre a porta para fazer uma segunda vez. E se outra pessoa foi autorizada a fazer isso, eu também posso, certo?

Não estou dizendo que, como desenvolvedores, consideramos esses atalhos levianamente. Mas se houver uma
opção para fazer algo, alguém o fará, especialmente em combinação com um prazo iminente. E se algo já foi
feito antes, a probabilidade de alguém fazer isso novamente aumentará drasticamente. Este é um efeito
psicológico chamado Teoria das Janelas Quebradas – mais sobre isso no Capítulo 11, Tomando Atalhos Conscientemente.

Figura 2.3 – Como qualquer camada pode acessar tudo na camada de persistência, ela tende a engordar com o tempo

Ao longo de anos de desenvolvimento e manutenção de um projeto de software, a camada de persistência pode muito
bem acabar como na Figura 2.3.

A camada de persistência (ou, em termos mais genéricos, a camada mais inferior) crescerá à medida que empurramos os
componentes para baixo através das camadas. Candidatos perfeitos para isso são componentes auxiliares ou utilitários ,
pois não parecem pertencer a nenhuma camada específica.

Portanto, se quisermos desabilitar o modo de atalho para nossa arquitetura, as camadas não são a melhor opção, pelo
menos não sem impor algum tipo de regra adicional de arquitetura. E por impor, não me refiro a um desenvolvedor sênior
fazendo revisões de código, mas a regras aplicadas automaticamente que fazem a construção falhar quando são
quebradas.

Eles ficam difíceis de testar


Uma evolução comum em uma arquitetura em camadas é que as camadas são ignoradas. Acessamos a camada de
persistência diretamente da camada web, pois estamos manipulando apenas um único campo de uma entidade, e para
isso não precisamos incomodar a camada de domínio, certo?
Machine Translated by Google

Eles escondem os casos de uso 13

Figura 2.4 – Ignorar a camada de domínio tende a espalhar a lógica do domínio pela base de código

A Figura 2.4 mostra como estamos ignorando a camada de domínio e acessando a camada de persistência diretamente da
camada web.

Novamente, isso parece bom nas primeiras vezes, mas tem duas desvantagens se acontecer com frequência (e acontecerá,
assim que alguém der o primeiro passo).

Primeiro, estamos implementando lógica de domínio na camada web, mesmo que seja manipulando apenas um único campo.
E se o caso de uso se expandir no futuro? Provavelmente adicionaremos mais lógica de domínio à camada da web,
misturando responsabilidades e espalhando a lógica de domínio essencial em todas as camadas.

Em segundo lugar, nos testes unitários da nossa camada web, não só temos que gerenciar as dependências na camada de
domínio, mas também as dependências na camada de persistência. Se estivermos usando simulações em nossos testes,
isso significa que teremos que criar simulações para ambas as camadas. Isso adiciona complexidade aos testes. E uma
configuração de teste complexa é o primeiro passo para não haver nenhum teste, porque não temos tempo para eles. À
medida que o componente web cresce ao longo do tempo, ele pode acumular muitas dependências em diferentes
componentes de persistência, aumentando a complexidade do teste. Em algum ponto, leva mais tempo para entendermos as
dependências e criarmos simulações para elas do que realmente escrever o código de teste.

Eles escondem os casos de uso


Como desenvolvedores, gostamos de criar novos códigos que implementem novos casos de uso. Mas geralmente gastamos
muito mais tempo alterando o código existente do que criando um novo código. Isso não é verdade apenas para aqueles
temidos projetos legados nos quais estamos trabalhando em uma base de código com décadas de existência, mas também
para um novo projeto greenfield após a implementação dos casos de uso iniciais.

Como frequentemente procuramos o lugar certo para adicionar ou alterar funcionalidades, nossa arquitetura deve nos ajudar
a navegar rapidamente pela base de código. Como uma arquitetura em camadas se comporta nesse aspecto?

Como já discutido anteriormente, em uma arquitetura em camadas, acontece facilmente que a lógica do domínio esteja
espalhada pelas camadas. Pode existir na camada web se ignorarmos a lógica do domínio para um caso de uso “fácil”. E
pode existir na camada de persistência se tivermos empurrado um determinado componente para baixo, de modo que ele
Machine Translated by Google

14 O que há de errado com camadas?

pode ser acessado a partir das camadas de domínio e de persistência. Isso já torna difícil encontrar o local
certo para adicionar novas funcionalidades.

Mas há mais. Uma arquitetura em camadas não impõe regras sobre a “largura” dos serviços de domínio. Com o tempo, isso
geralmente leva a serviços muito amplos que atendem a vários casos de uso (veja a Figura 2.5).

Figura 2.5 – Serviços “amplos” tornam difícil encontrar um determinado caso de uso dentro da base de código

Um serviço amplo tem muitas dependências da camada de persistência e muitos componentes da camada web dependem
dele. Isso não apenas dificulta o teste do serviço, mas também dificulta a localização do código responsável pelo caso de uso
no qual queremos trabalhar.

Quão mais fácil seria se tivéssemos serviços de domínio restrito e altamente especializados, cada um atendendo a um único
caso de uso? Em vez de procurar o caso de uso de registro de usuário em UserService, simplesmente abriríamos
RegisterUserService e começaríamos a hackear.

Eles dificultam o trabalho paralelo


A administração geralmente espera que concluamos a construção do software que eles patrocinam em uma determinada
data. Na verdade, eles até esperam que a gente faça dentro de um determinado orçamento também, mas não vamos
complicar as coisas aqui.

Além do fato de eu nunca ter visto software “pronto” em minha carreira como engenheiro de software, estar “pronto” em uma
determinada data geralmente implica que várias pessoas tenham que trabalhar em paralelo.

Você provavelmente conhece esta famosa conclusão de “O Mítico Homem-Mês”, mesmo que não tenha lido o livro: adicionar
mão de obra a um projeto de software atrasado o torna mais tarde. 4

4 O Mítico Homem-Mês: Ensaios sobre Engenharia de Software por Frederick P. Brooks, Jr., Addison-Wesley, 1995.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 15

Isto também se aplica, até certo ponto, a projetos de software que (ainda) não estão atrasados. Você não pode esperar que
um grande grupo de 50 desenvolvedores seja 5 vezes mais rápido do que uma equipe menor de 10 desenvolvedores. Se eles
estiverem trabalhando em um aplicativo muito grande, onde possam se dividir em subequipes e trabalhar em partes separadas
do software, isso pode funcionar, mas na maioria dos contextos, eles pisarão uns nos pés dos outros.

Mas numa escala saudável, podemos certamente esperar ser mais rápidos com mais pessoas no projeto. E a
administração está certa em esperar isso de nós.

Para atender a essa expectativa, nossa arquitetura deve suportar trabalho paralelo. Isso não é fácil. E uma arquitetura
em camadas realmente não nos ajuda aqui.

Imagine que estamos adicionando um novo caso de uso ao nosso aplicativo. Temos três desenvolvedores disponíveis.
Pode-se adicionar os recursos necessários à camada web, um à camada de domínio e o terceiro à camada de
persistência, certo?

Bem, geralmente não funciona assim em uma arquitetura em camadas. Como tudo se baseia na camada de
persistência, a camada de persistência deve ser desenvolvida primeiro. Depois vem a camada de domínio e,
finalmente, a camada da web. Assim, apenas um desenvolvedor pode trabalhar no recurso por vez!

“Ah, mas os desenvolvedores podem definir as interfaces primeiro”, você diz, “e então cada desenvolvedor pode
trabalhar nessas interfaces sem ter que esperar pela implementação real”.

Claro, isso é possível, mas apenas se não misturarmos nossa lógica de domínio e persistência conforme discutido
anteriormente, impedindo-nos de trabalhar em cada aspecto separadamente.

Se tivermos serviços amplos em nossa base de código, pode até ser difícil trabalhar em diferentes recursos em paralelo.
Trabalhar em diferentes casos de uso fará com que o mesmo serviço seja editado em paralelo, o que leva a conflitos
de mesclagem e potencialmente regressões.

Como isso me ajuda a construir software sustentável?


Se você construiu arquiteturas em camadas no passado, provavelmente poderá se identificar com algumas das
questões discutidas neste capítulo e talvez até adicionar algumas mais.

Se feita corretamente, e se algumas regras adicionais forem impostas a ela, uma arquitetura em camadas pode ser
muito fácil de manter e pode facilitar a alteração ou adição à base de código.

Contudo, a discussão mostra que uma arquitetura em camadas permite que muitas coisas dêem errado. Sem uma
boa autodisciplina, ele tende a se degradar e a se tornar menos sustentável com o tempo. E nossa autodisciplina
geralmente sofre um golpe cada vez que um membro da equipe entra ou sai da equipe, ou um gerente estabelece um
novo prazo para a equipe de desenvolvimento.

Manter em mente as armadilhas da arquitetura em camadas nos ajudará na próxima vez que argumentarmos contra
o atalho e, em vez disso, construirmos uma solução mais sustentável – seja em uma arquitetura em camadas ou em
um estilo de arquitetura diferente.
Machine Translated by Google
Machine Translated by Google

3
Invertendo Dependências

Após a conversa sobre arquitetura em camadas no capítulo anterior, você está certo em esperar que este capítulo
discuta uma abordagem alternativa. Começaremos discutindo dois dos princípios do SOLID1 e depois os
aplicaremos para criar uma Arquitetura Limpa ou Hexagonal que resolva os problemas de uma arquitetura em camadas.

O Princípio da Responsabilidade Única


Todos no desenvolvimento de software provavelmente conhecem o Princípio da Responsabilidade Única (SRP) ou pelo
menos presumem conhecê-lo. Uma interpretação comum deste princípio é esta:

Um componente deve fazer apenas uma coisa e da maneira certa.

Esse é um bom conselho, mas não é a intenção real do SRP.

Fazer apenas uma coisa é, na verdade, a interpretação mais óbvia de “responsabilidade única”, por isso não é de admirar
que o SRP seja frequentemente interpretado desta forma. Observemos apenas que o nome do SRP é enganoso.

Aqui está a definição real do SRP:

Um componente deve ter apenas um motivo para mudar.

Como vemos, “responsabilidade” deveria na verdade ser traduzida como “razão para mudar” em vez de “fazer apenas uma
coisa”. Talvez devêssemos renomear o SRP para “Princípio da Razão Única para Mudar”.

Se um componente tiver apenas um motivo para mudar, ele poderá acabar fazendo apenas uma coisa, mas a parte mais
importante é que ele tem apenas esse motivo para mudar.

O que isso significa para nossa arquitetura?

1 SOLID significa Princípio de Responsabilidade Única, Princípio Aberto-Fechado, Princípio de Substituição de Liskov,
Princípio de Segregação de Interface e Princípio de Inversão de Dependência. Você pode ler mais sobre estes Princípios
de Arquitetura Limpa de Robert C. Martin ou na Wikipedia em https://
en.wikipedia.org/wiki/SOLID.
Machine Translated by Google

18 Invertendo Dependências

Se um componente tiver apenas um motivo para ser alterado, não precisamos nos preocupar com esse componente
se alterarmos o software por qualquer outro motivo, porque sabemos que ele ainda funcionará conforme o esperado.

Infelizmente, é muito fácil, por algum motivo, alterar a propagação através do código através das dependências de um
componente para outros componentes (veja a Figura 3.1).

Figura 3.1 – Cada dependência de um componente é um possível motivo para alterar este
componente, mesmo que seja apenas uma dependência transitiva (setas tracejadas)

Na figura anterior, o componente A depende de muitos outros componentes (direta ou transitivamente), enquanto o
componente E não tem nenhuma dependência.

A única razão para alterar o componente E é quando a funcionalidade de E precisa ser alterada devido a algum
novo requisito. O componente A, entretanto, pode ter que ser alterado quando qualquer um dos outros
componentes for alterado, porque depende deles.

Muitas bases de código ficam mais difíceis – e, portanto, mais caras – de serem alteradas ao longo do tempo porque o
SRP é violado. Com o tempo, os componentes coletam cada vez mais motivos para mudar. Tendo coletado muitos
motivos para alterar, alterar um componente pode causar falha em outro componente.

Uma história sobre efeitos colaterais

Certa vez, participei de um projeto em que minha equipe herdou uma base de código de dez anos construída por outra
loja de software. O cliente decidiu substituir a equipe de desenvolvimento para reduzir os custos contínuos de
manutenção e melhorar a velocidade de desenvolvimento de novos recursos. Então, conseguimos o contrato.

Como era de se esperar, não foi fácil entender o que o código realmente fazia, e as alterações que fizemos em uma
área da base de código muitas vezes tiveram efeitos colaterais em outras áreas. Mas conseguimos testar exaustivamente,
adicionar testes automatizados e refatorar bastante.
Machine Translated by Google

O Princípio da Inversão de Dependência 19

Depois de algum tempo mantendo e estendendo com sucesso a base de código, o cliente solicitou um novo
recurso. E eles queriam que o construíssemos de uma forma que fosse muito estranha para os usuários do software.
Portanto, propus fazê-lo de uma forma mais amigável e ainda mais barata de implementar, uma vez que exigia
menos mudanças gerais. No entanto, era necessária uma pequena mudança num determinado componente muito central.

O cliente recusou e solicitou a solução mais complicada e cara. Quando perguntei o motivo, eles disseram que tinham medo
dos efeitos colaterais porque as alterações feitas naquele componente pela equipe de desenvolvimento anterior sempre
quebraram alguma outra coisa no passado.

Infelizmente, este é um exemplo de como você pode doutrinar seu cliente a pagar mais pela modificação de software mal
arquitetado. Felizmente, a maioria dos clientes não concorda com esse jogo, então, em vez disso, vamos tentar construir
um software bem arquitetado.

O Princípio da Inversão de Dependência


Em nossa arquitetura em camadas, as dependências entre camadas sempre apontam para a próxima camada. Quando
aplicamos o Princípio da Responsabilidade Única em alto nível, notamos que as camadas superiores têm mais motivos para
mudar do que as camadas inferiores.

Assim, devido à dependência da camada de domínio da camada de persistência, cada mudança na camada de
persistência requer potencialmente uma mudança na camada de domínio. Mas o código do domínio é o código
mais importante da nossa aplicação! Não queremos ter que alterá-lo quando algo mudar no código de persistência!

Então, como podemos nos livrar dessa dependência?

O Princípio de Inversão de Dependência (DIP) fornece a resposta.

Em contraste com o SRP, o DIP significa o que o nome sugere:

Podemos inverter (inverter) a direção de qualquer dependência dentro do nosso código base2

Como isso funciona? Vamos tentar inverter a dependência entre nosso código de domínio e de persistência para que o
código de persistência dependa do código de domínio, reduzindo o número de motivos para alterar o código de domínio.

Começamos com uma estrutura como a da Figura 2.2 do Capítulo 2, O que há de errado com as camadas? Temos um
serviço na camada de domínio que funciona com entidades e repositórios da camada de persistência.

Primeiro de tudo, queremos colocar as entidades na camada de domínio porque elas representam nossos objetos de
domínio e nosso código de domínio gira basicamente em torno da mudança do estado dessas entidades.

2 Na verdade, só podemos inverter dependências quando tivermos controle sobre o código em ambas as extremidades da
dependência. Se dependemos de uma biblioteca de terceiros, não podemos invertê-la, pois não controlamos o código
dessa biblioteca.
Machine Translated by Google

20 Invertendo Dependências

Mas agora temos uma dependência circular entre as duas camadas, pois o repositório da camada de
persistência depende da entidade, que agora está na camada de domínio. É aqui que aplicamos o DIP.
Criamos uma interface para o repositório na camada de domínio e deixamos o repositório real na camada
de persistência implementá-la. O resultado é algo parecido com o da Figura 3.2.

Figura 3.2 – Ao introduzir uma interface na camada de domínio, podemos inverter a


dependência para que a camada de persistência dependa da camada de domínio

Com esse truque, liberamos nossa lógica de domínio da dependência opressiva do código de persistência.
Esta é uma característica central dos dois estilos arquitetônicos que discutiremos nas próximas seções.

Arquitetura Limpa
Robert C. Martin cunhou o termo “Arquitetura Limpa” em seu livro de mesmo nome.3 In a Clean
Arquitetura, em sua opinião, as regras de negócios são testáveis por design e independentes de estruturas,
bancos de dados, tecnologias de UI e outras aplicações ou interfaces externas.

Isso significa que o código do domínio não deve ter nenhuma dependência externa. Em vez disso, com a
ajuda do DIP, todas as dependências apontam para o código do domínio.

A Figura 3.3 mostra como seria essa arquitetura em um nível abstrato.

3 Arquitetura Limpa por Robert C. Martin, Prentice Hall, 2017, Capítulo 22.
Machine Translated by Google

Arquitetura Limpa 21

Figura 3.3 – Em uma Arquitetura Limpa, todas as dependências apontam para dentro,

em direção à lógica do domínio (Fonte: Arquitetura Limpa de Robert C. Martin)

As camadas nesta arquitetura estão envolvidas umas nas outras em círculos concêntricos. A regra principal em tal
arquitetura é a “Regra de Dependência”, que afirma que todas as dependências entre essas camadas devem apontar
para dentro.

O núcleo da arquitetura contém as entidades de domínio, que são acessadas pelos casos de uso circundantes. Os casos
de uso são o que chamamos de serviços anteriormente, mas são mais refinados para terem uma única responsabilidade
(ou seja, um único motivo para mudar), evitando assim o problema de serviços amplos, como discutimos anteriormente.

Em torno deste núcleo, podemos encontrar todos os outros componentes da nossa aplicação que suportam as regras de
negócio. Esse suporte pode significar fornecer persistência ou fornecer uma UI, por exemplo. Além disso, as camadas
externas podem fornecer adaptadores para qualquer outro componente de terceiros.

Como o código do domínio não sabe nada sobre qual persistência ou estrutura de UI é usada, ele não pode
conter nenhum código específico para essas estruturas e se concentrará nas regras de negócios. Temos toda a
liberdade que podemos desejar para modelar o código de domínio. Poderíamos, por exemplo, aplicar o Domain-
Driven Design (DDD) em sua forma mais pura. Não ter que pensar em persistência ou em problemas específicos
da UI torna isso muito mais fácil.

Como seria de esperar, a Arquitetura Limpa tem um custo. Como a camada de domínio é completamente dissociada das
camadas externas, como as camadas de persistência e UI, temos que manter um modelo das entidades de nosso
aplicativo em cada uma das camadas.
Machine Translated by Google

22 Invertendo Dependências

Vamos supor, por exemplo, que estejamos usando uma estrutura de mapeamento objeto-relacional (ORM) em nossa
camada de persistência. Uma estrutura ORM geralmente espera classes de entidades específicas que contenham metadados
que descrevem a estrutura do banco de dados e o mapeamento de campos de objetos para colunas do banco de dados.
Como a camada de domínio não conhece a camada de persistência, não podemos usar as mesmas classes de entidade na
camada de domínio e temos que criá-las em ambas as camadas. Isto significa que a camada de persistência precisa mapear
as entidades do domínio para sua própria representação. Um mapeamento semelhante se aplica entre a camada de domínio
e outras camadas externas.

Mas isso é uma coisa boa! Essa dissociação é exatamente o que queríamos alcançar para libertar o código do domínio de
problemas específicos da estrutura. A API Java Persistence (a API objeto-relacional padrão no mundo Java), por exemplo,
exige que as entidades gerenciadas por ORM tenham um construtor padrão sem argumentos que possamos querer evitar
em nosso modelo de domínio. No Capítulo 9, Mapeamento entre Fronteiras, falaremos sobre diferentes estratégias de
mapeamento, incluindo uma estratégia sem mapeamento que apenas aceita o acoplamento entre as camadas de domínio e
de persistência.

Como a Arquitetura Limpa de Robert C. Martin é um tanto abstrata, vamos nos aprofundar e examinar a Arquitetura
Hexagonal, que dá aos princípios da Arquitetura Limpa uma forma mais concreta.

Arquitetura Hexagonal
O termo Arquitetura Hexagonal deriva de Alistair Cockburn e já existe há algum tempo.4 Ele
aplica os mesmos princípios que Robert C. Martin descreveu posteriormente em termos mais
gerais em Clean Architecture.

Figura 3.4 – Uma arquitetura hexagonal também é chamada de arquitetura de “Portas e


Adaptadores”, pois o núcleo da aplicação fornece portas específicas para cada adaptador interagir.

4 A fonte primária do termo "Arquitetura Hexagonal" parece ser um artigo no site de Alistair Cockburn em https://
alistair.cockburn.us/hexagonal-architecture/.
Machine Translated by Google

Arquitetura Hexagonal 23

A Figura 3.4 mostra como seria uma Arquitetura Hexagonal. O núcleo do aplicativo é representado como um hexágono, dando
nome a esse estilo arquitetônico. A forma do hexágono não tem significado, portanto, poderíamos muito bem desenhar um
octógono e chamá-lo de “Arquitetura Octogonal”. Segundo a lenda, o hexágono foi simplesmente usado em vez do retângulo
comum para mostrar que uma aplicação pode ter mais de quatro lados conectando-a a outros sistemas ou adaptadores.

Dentro do hexágono, encontramos nossas entidades de domínio e os casos de uso que funcionam com essas entidades.
Observe que o hexágono não tem dependências de saída, portanto a Regra de Dependência da Arquitetura Limpa de Martin é
válida. Em vez disso, todas as dependências apontam para o centro.

Fora do hexágono, encontramos diversos adaptadores que interagem com a aplicação. Pode haver um adaptador
da web que interaja com um navegador da web, alguns adaptadores interagindo com sistemas externos e um
adaptador que interaja com um banco de dados para persistência.

Os adaptadores do lado esquerdo são adaptadores que orientam nosso aplicativo (porque chamam o núcleo do nosso aplicativo),
enquanto os adaptadores do lado direito são acionados pelo nosso aplicativo (porque são chamados pelo núcleo do nosso
aplicativo).

Para permitir a comunicação entre o núcleo do aplicativo e os adaptadores, o núcleo do aplicativo fornece
portas específicas . Para acionar adaptadores, essa porta pode ser uma interface implementada por uma
das classes de caso de uso no núcleo e chamada pelo adaptador. Para um adaptador controlado, pode
ser uma interface implementada pelo adaptador e chamada pelo núcleo. Poderíamos até ter vários
adaptadores implementando a mesma porta: um para comunicação com um sistema externo real e outro
para comunicação com um mock para ser usado em testes, por exemplo.

Para destacar claramente um atributo central da Arquitetura Hexagonal, o núcleo do aplicativo (o hexágono)
define e possui a interface com o exterior (as portas). Os adaptadores então funcionam com esta interface.
Este é o Princípio de Inversão de Dependência aplicado no nível da arquitetura.

Devido aos seus conceitos centrais, este estilo de arquitetura também é conhecido como arquitetura de Portas e Adaptadores .

Assim como a Arquitetura Limpa, podemos organizar essa Arquitetura Hexagonal em camadas. A camada mais externa consiste
nos adaptadores que fazem a conversão entre o aplicativo e outros sistemas.

A seguir, podemos combinar as portas e as implementações de casos de uso para formar a camada de aplicação, pois elas
definem a interface de nossa aplicação. A camada final contém as entidades de domínio que implementam as regras de negócios.

A lógica de negócios é implementada nas classes e entidades de casos de uso. As classes de casos de uso são serviços de
domínio restrito, implementando apenas um único caso de uso. Podemos optar por combinar vários casos de uso em um serviço
de domínio mais amplo, é claro, mas, idealmente, fazemos isso apenas quando os casos de uso são frequentemente usados
juntos, para aumentar a capacidade de manutenção.

Potencialmente, desejaremos introduzir também o conceito de serviços de aplicação. Um serviço de aplicativo


é um serviço que coordena chamadas para casos de uso (serviços de domínio), conforme mostrado na Figura 3.5.
Machine Translated by Google

24 Invertendo Dependências

Figura 3.5 – Uma Arquitetura Hexagonal utilizando os conceitos DDD de aplicações e serviços de domínio

Aqui, os serviços de aplicação são traduzidos entre as portas de entrada e saída e os serviços de
domínio, protegendo os serviços de domínio do mundo exterior e potencialmente coordenando entre os
serviços de domínio. As caixas Serviço de Domínio são sinônimos das caixas Caso de Uso da Figura
3.4; agora estamos usando a terminologia emprestada do DDD.

Como esta discussão implica, somos livres para projetar o código do nosso aplicativo como acharmos adequado dentro do hexágono.

Podemos ser simples ou sofisticados, combinando com a complexidade e o tamanho da nossa aplicação. Aprenderemos
mais sobre como gerenciar código dentro de nosso hexágono no Capítulo 13, Gerenciando múltiplos contextos limitados.

No próximo capítulo, discutiremos uma maneira de organizar tal arquitetura em código.

Como isso me ajuda a construir software sustentável?


Chame isso de “Arquitetura Limpa”, “Arquitetura Hexagonal” ou “Arquitetura de Portas e Adaptadores” – invertendo nossas dependências para que

o código de domínio não tenha dependências externas, podemos desacoplar nossa lógica de domínio de todos aqueles problemas de persistência

e UI. problemas específicos e reduzir o número de motivos para fazer alterações em toda a base de código. E menos motivos para mudar levam a

uma melhor capacidade de manutenção.

O código de domínio é livre para ser modelado da maneira que melhor se adapta aos problemas de negócios, enquanto a persistência e o código

da UI são livres para serem modelados da maneira que melhor se adapta aos problemas de persistência e da UI.

No restante deste livro, aplicaremos o estilo de Arquitetura Hexagonal a uma aplicação web. Começaremos criando a estrutura de pacotes do nosso

aplicativo e discutindo a função da injeção de dependência.


Machine Translated by Google

4
Código de organização

Não seria bom reconhecer a arquitetura apenas olhando o código?

Neste capítulo, examinaremos diferentes formas de organizar código e apresentaremos uma estrutura de pacote
expressiva que reflete diretamente uma Arquitetura Hexagonal.

Em projetos de software greenfield, a primeira coisa que tentamos acertar é a estrutura do pacote. Montamos uma
estrutura bonita que pretendemos usar no restante do projeto. Então, durante o projeto, as coisas ficam agitadas e
percebemos que em muitos lugares a estrutura do pacote é apenas uma bela fachada para uma confusão de código
não estruturado. As classes em um pacote importam classes de outros pacotes que não devem ser importadas.

Discutiremos diferentes opções para estruturar o código do aplicativo de exemplo BuckPal que foi apresentado no
Prefácio. Mais especificamente, veremos o caso de uso Enviar dinheiro, que permite a um usuário transferir dinheiro
de sua conta para outra.

Organizando por camada


A primeira abordagem para organizar nosso código é por camadas. Poderíamos organizar o código assim:
Machine Translated by Google

26 Código de organização

Para cada uma de nossas camadas – web, domínio e persistência – temos um pacote dedicado. Conforme discutido no
Capítulo 2, O que há de errado com as camadas?, camadas simples podem não ser a melhor estrutura para nosso código
por vários motivos, portanto, já aplicamos o Princípio de Inversão de Dependência aqui, permitindo apenas dependências
em relação ao código de domínio no pacote de domínio . Fizemos isso introduzindo a interface AccountRepository no
pacote de domínio e implementando-a no pacote de persistência .

No entanto, podemos encontrar pelo menos três razões pelas quais esta estrutura de pacote não é ideal:

• Primeiro, não temos limite de pacote entre fatias funcionais ou recursos de nossa aplicação.
Se adicionarmos um recurso para gerenciamento de usuários, adicionaremos um UserController
ao pacote web ; um UserService, UserRepository e User para o pacote de domínio ; e um
UserRepositoryImpl para o pacote de persistência . Sem estrutura adicional, isso pode
rapidamente se tornar uma confusão de classes, levando a efeitos colaterais indesejados entre
recursos supostamente não relacionados do aplicativo.

• Segundo, não podemos ver quais casos de uso nosso aplicativo oferece. Você pode dizer quais
casos de uso as classes AccountService ou AccountController implementam? Se estamos
procurando um determinado recurso, temos que adivinhar qual serviço o implementa e então
procurar o método responsável nesse serviço.

• Finalmente, não podemos ver nossa arquitetura alvo dentro da estrutura do pacote. Podemos
adivinhar que seguimos o estilo da Arquitetura Hexagonal e então navegar pelas classes na web
e pacotes de persistência para localizar os adaptadores web e de persistência. Mas não podemos
ver rapidamente qual funcionalidade é chamada pelo adaptador web e qual funcionalidade o
adaptador de persistência fornece à camada de domínio. As portas de entrada e saída estão ocultas no código.

Vamos tentar abordar algumas questões da abordagem “organizar por camadas”.

Organizando por recurso


A próxima abordagem é organizar nosso código por recurso:

Em essência, colocamos todo o código relacionado às contas no pacote de alto nível, account. Também
removemos os pacotes de camadas .
Machine Translated by Google

Uma estrutura de pacote arquitetonicamente expressiva 27

Cada novo grupo de recursos receberá um novo pacote de alto nível próximo à conta e podemos impor limites de
pacote entre os recursos usando a visibilidade package-private para as classes que não devem ser acessadas de fora.

Os limites do pacote, combinados com a visibilidade privada do pacote, nos permitem evitar dependências indesejadas
entre recursos.

Também renomeamos AccountService SendMoneyService para restringir sua responsabilidade (na


verdade, poderíamos ter feito isso também na abordagem pacote por camada). Agora podemos ver que
o código implementa o caso de uso Enviar dinheiro apenas observando o nome da classe. Tornar a
funcionalidade do aplicativo visível no código é o que Robert Martin chama de “Arquitetura Gritante”,
porque ela grita sua intenção para nós.1

No entanto, a abordagem pacote por recurso torna nossa arquitetura ainda menos evidente no código do
que a abordagem pacote por camada. Não temos nomes de pacotes para identificar nossos adaptadores
e ainda não vemos as portas de entrada e saída. Além do mais, embora tenhamos invertido as
dependências entre o código de domínio e o código de persistência para que SendMoneyService conheça
apenas a interface AccountRepository e não sua implementação, não podemos usar a visibilidade privada
do pacote para proteger o código de domínio de dependências acidentais no código de persistência. .

Então, como podemos tornar nossa arquitetura alvo visível à primeira vista? Seria bom se pudéssemos apontar o dedo
para uma caixa em um diagrama de arquitetura como a Figura 3.4 e saber instantaneamente qual parte do código é
responsável por essa caixa.

Vamos dar mais um passo para criar uma estrutura de pacote que seja expressiva o suficiente para suportar isso.

Uma estrutura de pacote arquitetonicamente expressiva


Em uma Arquitetura Hexagonal, temos entidades, casos de uso, portas de entrada e saída e adaptadores de entrada
e saída (ou “condutores” e “dirigidos”) como nossos principais elementos arquitetônicos. Vamos encaixá-los em
uma estrutura de pacote que expresse esta arquitetura:

1 Screaming Architecture: Clean Architecture por Robert C. Martin, Prentice Hall, 2017, Capítulo 21.
Machine Translated by Google

28 Código de organização

Podemos mapear cada elemento da arquitetura diretamente para um dos pacotes. No nível mais alto, temos os
pacotes de adaptadores e aplicativos .

O pacote do adaptador contém os adaptadores de entrada que chamam as portas de entrada do aplicativo e os
adaptadores de saída que fornecem implementações para as portas de saída do aplicativo. No nosso caso, estamos
construindo uma aplicação web simples com os adaptadores web e de persistência , cada um com seu próprio
subpacote.

Mover o código dos adaptadores para seus próprios pacotes tem a vantagem de podermos facilmente substituir um
adaptador por outra implementação, caso seja necessário. Imagine que começamos a implementar um adaptador
de persistência em um banco de dados de valores-chave simples porque pensávamos que conhecíamos os padrões
de acesso necessários, mas esses padrões mudaram e estaríamos melhor com um banco de dados SQL agora.
Simplesmente implementamos todas as portas de saída relevantes em um novo pacote de adaptador e depois
removemos o pacote antigo.

O pacote do aplicativo contém o “hexágono”, como em nosso código do aplicativo. Este código consiste em nosso
modelo de domínio, que reside no pacote de domínio , e nas interfaces de porta, que residem no pacote de porta .

Por que as portas estão dentro do pacote de aplicativos e não próximas a ele? As portas são a nossa forma de
aplicar o Princípio de Inversão de Dependência. A aplicação define essas portas para se comunicar com o mundo
exterior. Colocar o pacote port dentro do pacote da aplicação expressa que a aplicação possui as portas.
Machine Translated by Google

Uma estrutura de pacote arquitetonicamente expressiva 29

O pacote de domínio contém nossas entidades de domínio e serviços de domínio que implementam as portas
de entrada e coordenam entre as entidades de domínio.

Finalmente, existe um pacote comum , que contém algum código que é compartilhado pelo resto da base
de código.

Ufa, são muitos pacotes que parecem técnicos. Isso não é confuso?

Imagine que temos uma visão de alto nível de nossa arquitetura hexagonal pendurada na parede do escritório e
estamos conversando com um colega sobre como modificar um cliente para uma API de terceiros que estamos
consumindo. Ao discutir isso, podemos apontar para o adaptador de saída correspondente no pôster para nos entendermos melhor.
Então, quando terminarmos de conversar, sentamos em frente ao nosso IDE e podemos começar a trabalhar
no cliente imediatamente, pois o código do cliente API de que falamos pode ser encontrado no adaptador/
pacote out/<nome do adaptador> . Bastante útil em vez de confuso, não acha?

Esta estrutura de pacotes é um elemento poderoso na luta contra a chamada lacuna de arquitetura/código
2 Esses
ou lacuna de modelo/código. termos descrevem o fato de que na maioria dos projetos de desenvolvimento
de software, a arquitetura é apenas um conceito abstrato que não pode ser mapeado diretamente no código.
Com o tempo, se a estrutura do pacote (entre outras coisas) não refletir a arquitetura, o código normalmente
se desviará cada vez mais da arquitetura alvo.

Além disso, esta expressiva estrutura de pacote promove o pensamento ativo sobre a arquitetura. Temos
que decidir ativamente em qual pacote colocar nosso código. Mas tantos pacotes não significam que tudo
tem que ser público para permitir o acesso entre pacotes?

Para os pacotes de adaptadores, pelo menos, isso não é verdade. Todas as classes que eles contêm podem ser
privadas do pacote, uma vez que não são chamadas pelo mundo externo, exceto pelas interfaces de porta, que
residem dentro do pacote do aplicativo . Portanto, não há dependências acidentais da camada de aplicação com
as classes do adaptador.

Dentro do pacote de aplicativos , entretanto, algumas classes realmente precisam ser públicas. As portas
devem ser públicas porque devem ser acessíveis aos adaptadores por design. O modelo de domínio deve ser
público para ser acessível aos serviços e, potencialmente, aos adaptadores. Os serviços não precisam ser
públicos porque podem ficar ocultos atrás das interfaces de porta de entrada.

Então, sim, uma estrutura de pacote refinada como essa exige que tornemos públicas algumas classes que
podem ser privadas de pacote em uma estrutura de pacote refinada. Veremos maneiras de capturar acesso
indesejado a essas classes públicas no Capítulo 12, Aplicando limites de arquitetura.

Você pode notar que esta estrutura de pacote contém apenas um domínio, ou seja, o domínio que trata as
transações da conta. Entretanto, muitos aplicativos conterão código de mais de um domínio.

2 Lacuna de modelo/código: Just Enough Architecture por George Fairbanks, Marshall & Brainerd, 2010, página 167.
Machine Translated by Google

30 Código de organização

Como aprenderemos no Capítulo 13, Gerenciando Múltiplos Contextos Delimitados, a Arquitetura Hexagonal não
nos diz realmente como gerenciar múltiplos domínios. Podemos, é claro, colocar o código de cada domínio em
seu próprio subpacote no pacote de domínio e separar os domínios desta forma. Porém , se você está pensando
em separar as portas e os adaptadores por domínio, tenha cuidado, pois isso rapidamente se transforma em um
pesadelo de mapeamento. Mais sobre isso no Capítulo 13.

Como acontece com toda estrutura, é preciso disciplina para manter essa estrutura de pacote durante a vida útil
de um projeto de software. Além disso, haverá casos em que a estrutura do pacote simplesmente não se ajusta e
não vemos outra maneira senão ampliar a lacuna arquitetura/código e criar um pacote que não reflita a arquitetura.

Não existe perfeição. Mas com uma estrutura de pacotes expressiva, podemos pelo menos reduzir a lacuna entre
o código e a arquitetura.

O papel da injeção de dependência


A estrutura de pacotes descrita anteriormente percorre um longo caminho para alcançar uma arquitetura
limpa, mas um requisito essencial de tal arquitetura é que a camada de aplicação não tenha
dependências nos adaptadores de entrada e saída, como aprendemos no Capítulo 3, Invertendo Dependências.

Para adaptadores de entrada, como nosso adaptador web, isso é fácil, pois o fluxo de controle aponta na mesma
direção que a dependência entre o adaptador e o código de domínio. O adaptador simplesmente chama o serviço
na camada de aplicação. Para destacar claramente os pontos de entrada da nossa aplicação, desejaremos
ocultar os serviços reais por trás das interfaces das portas.

Para adaptadores de saída, como nosso adaptador de persistência, temos que usar o Princípio de Inversão de
Dependência para virar a dependência contra a direção do fluxo de controle.

Já vimos como isso funciona. Criamos uma interface dentro da camada de aplicação, que é implementada por
uma classe dentro do adaptador. Dentro da nossa Arquitetura Hexagonal, esta interface é uma porta. A camada
de aplicação então chama essa interface de porta para chamar a funcionalidade do adaptador, conforme mostrado
na Figura 4.1.
Machine Translated by Google

O papel da injeção de dependência 31

Figura 4.1 – O controlador web chama uma porta de entrada, que é implementada por um
serviço, e o serviço chama uma porta de saída, que é implementada por um adaptador

Mas quem fornece à aplicação os objetos reais que implementam as interfaces de porta? Não
queremos instanciar as portas manualmente na camada de aplicação porque não queremos introduzir
uma dependência em um adaptador.

É aqui que entra a injeção de dependência . Introduzimos um componente neutro que depende de todas
as camadas. Este componente é responsável por instanciar grande parte das classes que compõem
nossa arquitetura.

Na figura do exemplo anterior, o componente de injeção de dependência neutra criaria


instâncias de SendMoneyController, SendMoneyService e AccountPersistenceAdapter
Aulas. Como SendMoneyController requer SendMoneyUseCase, o mecanismo de injeção de
dependência fornecerá a ele uma instância da classe SendMoneyService durante a construção.
O controlador não sabe que realmente obteve uma instância SendMoneyService , pois só precisa
conhecer a interface.

Da mesma forma, ao construir a instância SendMoneyService , o mecanismo de injeção


de dependência injetará uma instância da classe AccountPersistenceAdapter , disfarçada
de interface UpdateAccountStatePort . O serviço nunca conhece a classe real por trás
da interface.

Falaremos mais sobre como inicializar um aplicativo usando o framework Spring como exemplo
no Capítulo 10, Montando o aplicativo.
Machine Translated by Google

32 Código de organização

Como isso me ajuda a construir software sustentável?


Vimos uma estrutura de pacote para uma arquitetura hexagonal que leva a estrutura de código real o
mais próximo possível da arquitetura de destino. Encontrar um elemento da arquitetura no código agora
é uma questão de navegar pela estrutura do pacote ao longo dos nomes de certas caixas em um
diagrama de arquitetura, ajudando na comunicação, desenvolvimento e manutenção.

Nos capítulos seguintes, veremos essa estrutura de pacote e injeção de dependência em ação à medida que
implementamos um caso de uso na camada de aplicação, um adaptador web e um adaptador de persistência.
Machine Translated by Google

5
Implementando um caso de uso

Vamos finalmente ver como podemos manifestar a arquitetura que discutimos no código real.

Como as camadas de aplicação, web e persistência são tão pouco acopladas em nossa arquitetura, somos totalmente livres
para modelar nosso código de domínio como acharmos adequado. Podemos fazer Domain-Driven Design (DDD), implementar
um modelo de domínio rico ou anêmico ou inventar nossa própria maneira de fazer as coisas.

Este capítulo descreve uma maneira opinativa de implementar casos de uso dentro do estilo de Arquitetura Hexagonal que apresentamos nos

capítulos anteriores.

Como é apropriado para uma arquitetura centrada em domínio, começaremos com uma entidade de domínio e depois construiremos um caso
de uso em torno dela.

Implementando o modelo de domínio


Queremos implementar o caso de uso de envio de dinheiro de uma conta para outra. Uma maneira de modelar isso de maneira orientada a

objetos é criar uma entidade Account que nos permita retirar dinheiro de uma conta de origem e depositá-lo em uma conta de destino:
Machine Translated by Google

34 Implementando um caso de uso


Machine Translated by Google

Um caso de uso em poucas palavras 35

A entidade Conta fornece o instantâneo atual de uma conta real. Cada retirada e depósito em uma conta é capturada em uma
entidade Atividade . Como não seria aconselhável carregar sempre todas as atividades de uma conta na memória, a entidade
Conta mantém apenas uma janela dos últimos dias ou semanas de atividades, capturada no objeto de valor ActivityWindow .

Para ainda poder calcular o saldo da conta corrente, a entidade Conta possui adicionalmente o
atributo baselineBalance , representando o saldo que a conta tinha pouco antes da primeira atividade
da janela de atividades. O saldo total, então, é o saldo da linha de base mais o saldo de todas as
atividades na janela.

Com este modelo, sacar e depositar dinheiro em uma conta é uma questão de adicionar uma nova
atividade à janela de atividades, como é feito nos métodos retirar() e depositar() . Antes de podermos
sacar, verificamos a regra de negócios que diz que não podemos sacar uma conta a descoberto.

Agora que temos uma conta que nos permite sacar e depositar dinheiro, podemos avançar para construir
um caso de uso em torno dela.

Um caso de uso em poucas palavras

Primeiro, vamos discutir o que um caso de uso realmente faz. Geralmente, segue estas etapas:

1. Receba a entrada.

2. Valide as regras de negócio.

3. Manipule o estado do modelo.

4. Retorne a saída.

Um caso de uso recebe informações de um adaptador de entrada. Você pode estar se perguntando por que não chamei a
primeira etapa de Validar entrada. A resposta é que acredito que o código do caso de uso deve se preocupar apenas com a
lógica do domínio e não devemos poluí-lo com validação de entrada. Então, faremos a validação de entrada em outro lugar,
como veremos em breve.

O caso de uso é, entretanto, responsável pela validação das regras de negócio. Partilha esta responsabilidade com
as entidades do domínio. Discutiremos a distinção entre validação de entrada e validação de regras de negócios
posteriormente neste capítulo.

Se as regras de negócios forem satisfeitas, o caso de uso manipulará o estado do modelo de uma forma ou de outra, com
base na entrada. Normalmente, ele alterará o estado de um objeto de domínio e passará esse novo estado para uma porta
implementada pelo adaptador de persistência para ser persistido. Se o caso de uso gerar outros efeitos colaterais além da
persistência, ele invocará um adaptador apropriado para cada efeito colateral.

A última etapa é traduzir o valor de retorno do adaptador de saída em um objeto de saída, que será retornado ao adaptador
de chamada.

Com essas etapas em mente, vamos ver como podemos implementar nosso caso de uso Enviar dinheiro.
Machine Translated by Google

36 Implementando um caso de uso

Para evitar o problema de serviços amplos discutido no Capítulo 2, O que há de errado com as camadas?, criaremos uma
classe de serviço separada para cada caso de uso, em vez de colocar todos os casos de uso em uma única classe de serviço.

Aqui está um teaser:

O serviço implementa a interface da porta de entrada SendMoneyUseCase e chama o Load


Interface da porta de saída AccountPort para carregar uma conta e o UpdateAccountState
Porta porta para persistir um estado de conta atualizado no banco de dados.

O serviço também define o limite para uma transação de banco de dados, conforme implícito no @Transactional
anotação. Mais sobre isso no Capítulo 7, Implementando um Adaptador de Persistência.

A Figura 5.1 fornece uma visão geral dos componentes relevantes:

Figura 5.1 – Um serviço implementa um caso de uso, modifica o modelo de domínio e

chama uma porta de saída para persistir o estado modificado


Machine Translated by Google

Validando entrada 37

Observação

UpdateAccountStatePort e LoadAccountPort, neste exemplo, são interfaces de porta implementadas


por um adaptador de persistência. Se eles forem frequentemente usados juntos, também poderemos
combiná- los em uma interface mais ampla. Poderíamos até chamar essa interface de
AccountRepository para manter a linguagem DDD. Neste exemplo, e no restante do livro, optei por
usar o nome “Repositório” apenas no adaptador de persistência, mas você pode escolher nomes diferentes!

Vamos cuidar dos comentários TODO que deixamos no código anterior.

Validando entrada
Agora, estamos falando sobre validação de entrada, embora eu tenha afirmado que isso não é responsabilidade de uma
classe de caso de uso. Ainda acho, entretanto, que isso pertence à camada de aplicação, então este é o lugar para discuti-
lo.

Por que não deixar o adaptador de chamada validar a entrada antes de enviá-la ao caso de uso? Bem, queremos confiar
que o chamador validou tudo conforme necessário para o caso de uso? Além disso, o caso de uso pode ser chamado por
mais de um adaptador, portanto a validação teria que ser implementada por cada adaptador, e alguém poderia errar ou
esquecê-lo completamente.

A camada de aplicativo deve se preocupar com a validação de entrada porque, caso contrário, poderá obter entradas
inválidas de fora do núcleo do aplicativo. Isso pode causar danos ao estado do nosso modelo.

Mas onde colocamos a validação de entrada senão na classe de caso de uso?

Deixaremos o modelo de entrada cuidar disso. Para o caso de uso Enviar dinheiro, o modelo de
entrada é a classe SendMoneyCommand que já vimos no exemplo de código anterior. Mais
precisamente, faremos a validação dentro do construtor:
Machine Translated by Google

38 Implementando um caso de uso

Para enviar dinheiro, precisamos dos IDs da conta de origem e de destino e da quantidade de
dinheiro a ser transferida. Nenhum dos parâmetros pode ser nulo e o valor deve ser maior que zero.
Se alguma dessas condições for violada, simplesmente recusamos a criação do objeto lançando uma exceção
durante a construção.

Ao usar um registro para implementar SendMoneyCommand, tornamos-no imutável. Assim, uma vez
construído com sucesso, podemos ter certeza de que o estado é válido e não pode ser alterado para algo inválido.

Como SendMoneyCommand faz parte da API dos casos de uso, ele está localizado no pacote da porta de entrada.
Assim, a validação permanece no núcleo da aplicação (na borda do hexágono da nossa arquitetura), mas não
polui o código sagrado do caso de uso.

Mas será que realmente queremos implementar cada verificação de validação manualmente quando existem
bibliotecas que podem fazer o trabalho sujo para nós? Muitas vezes ouvi declarações como “Você não deve
usar bibliotecas em suas classes de modelo”. É sensato reduzir as dependências ao mínimo, é claro, mas se
podemos nos safar com uma dependência pequena que nos poupa tempo, então por que não usá-la? Vamos
explorar como isso pode ser com a API Bean Validation do Java.1

1 Validação de feijão: https://beanvalidation.org/.


Machine Translated by Google

Validando entrada 39

A Validação de Bean nos permite expressar as regras de validação necessárias como anotações nos campos de uma classe:

A classe Validator fornece o método activate(), que simplesmente chamamos como a última instrução no
construtor. Isso avaliará as anotações de validação do Bean nos campos (@NotNull, neste caso) e
lançará uma exceção em caso de violação. Se as anotações padrão do Bean Validation não forem
expressivas o suficiente para uma determinada validação, podemos implementar nossas próprias
anotações e validadores como fizemos com a anotação @PositiveMoney.2

A implementação da classe Validator pode ser assim:

2 Você pode encontrar o código completo que implementa a anotação e o validador @PositiveMoney no
Repositório GitHub em https://github.com/thombergs/buckpal.
Machine Translated by Google

40 Implementando um caso de uso

Com a validação localizada no modelo de entrada, criamos uma camada anticorrupção em torno de nossas
implementações de casos de uso. Esta não é uma camada no sentido de uma arquitetura em camadas, chamando a
próxima camada abaixo dela, mas em vez disso, uma tela fina e protetora em torno de nossos casos de uso que devolve
informações incorretas ao chamador.

Observe que o termo “comando”, conforme usado na classe SendMoneyCommand , não corresponde à
interpretação comum do “padrão de comando”.3 No padrão de comando, um comando é executável, ou seja,
possui um método chamado execute() que realmente invoca o caso de uso. No nosso caso, o comando é
apenas um objeto de transferência de dados que transfere os parâmetros necessários para o serviço de caso
de uso que executa o comando. Poderíamos chamá-lo de SendMoneyDTO , mas gosto do termo “comando”
para deixar bem claro que estamos alterando o estado do modelo com este caso de uso.

O poder dos construtores


Nosso modelo de entrada, SendMoneyCommand, atribui muita responsabilidade ao seu construtor. Como
a classe é imutável, a lista de argumentos do construtor contém um parâmetro para cada atributo da classe.
E como o construtor também valida os parâmetros, não é possível criar um objeto com estado inválido.

No nosso caso, o construtor possui apenas três parâmetros. E se tivéssemos mais parâmetros? Não
poderíamos usar o padrão construtor para torná-lo mais conveniente de usar? Poderíamos tornar privado o
construtor com a longa lista de parâmetros e ocultar a chamada para ele no método build() do nosso
construtor. Então, em vez de chamar um construtor com 20 parâmetros, poderíamos construir um objeto como este:

Ainda podemos deixar nosso construtor fazer a validação para que o construtor não possa construir um objeto com estado
inválido.

Parece bom? Pense no que acontece se tivermos que adicionar outro campo ao SendMoneyCommandBuilder
(o que acontecerá algumas vezes durante a vida de um projeto de software). Adicionamos o novo campo ao construtor e
ao construtor. Então, um colega (ou um telefonema, um e-mail, uma borboleta…) interrompe nossa linha de pensamento.
Após o intervalo, voltamos à codificação e esquecemos de adicionar o novo campo ao código que chama o construtor
para criar um objeto.

Não recebemos uma palavra de aviso do compilador sobre a tentativa de criar um objeto imutável em um estado inválido!
Claro, em tempo de execução – esperançosamente em um teste de unidade – nossa lógica de validação ainda entrará
em ação e gerará um erro porque perdemos um parâmetro.

3 Padrão de comando: https://en.wikipedia.org/wiki/Command_pattern.


Machine Translated by Google

Diferentes modelos de entrada para diferentes casos de uso 41

Mas se usarmos o construtor diretamente em vez de ocultá-lo atrás de um construtor, cada vez que um novo campo for
adicionado ou um campo existente for removido, poderemos simplesmente seguir a trilha de erros de compilação para refletir
essa mudança no restante da base de código.

Longas listas de parâmetros podem até ser bem formatadas, e bons IDEs ajudam com dicas de nomes de parâmetros:

Figura 5.2 – O IDE mostra dicas de nomes de parâmetros em listas de parâmetros para nos ajudar a não nos perdermos

Para tornar o código anterior ainda mais legível e seguro para trabalhar, podemos introduzir objetos de valor imutável
para substituir alguns dos primitivos que usamos como parâmetros do construtor. Um objeto de valor é um objeto cujo
valor é sua identidade. Dois objetos de valor com o mesmo valor são considerados iguais.
Em vez de passar a rua, a cidade, o CEP, o país e o estado separadamente, poderíamos combiná-los em um objeto
de valor Endereço , por exemplo, porque eles pertencem um ao outro. Poderíamos até dar um passo adiante e criar
objetos de valor City e ZipCode , por exemplo. Isso reduziria a chance de confundir um parâmetro String com outro,
porque o compilador reclamaria se tentássemos passar uma Cidade para um parâmetro ZipCode e vice-versa.

Há casos em que um construtor pode ser a melhor solução. Se alguns parâmetros em ClassWithManyFields
do exemplo anterior fossem opcionais, por exemplo, teríamos que passar valores nulos para o construtor,
o que é, na melhor das hipóteses, feio. Um construtor nos permitiria definir apenas os parâmetros
necessários. Mas se estivermos usando construtores, devemos ter certeza de que o método build() falha
ruidosamente quando esquecemos de definir um parâmetro obrigatório porque o compilador não verifica isso para nós!

Diferentes modelos de entrada para diferentes casos de uso


Podemos ficar tentados a usar o mesmo modelo de entrada para diferentes casos de uso. Vamos considerar os casos
de uso Registrar conta e Atualizar detalhes da conta. Ambos precisarão inicialmente quase da mesma entrada, ou seja,
alguns detalhes da conta, como nome de usuário e endereço de e-mail.

Entretanto, o caso de uso Atualizar precisará do ID da conta que precisa ser atualizada, enquanto o caso de uso
Registrar não. Se ambos os casos de uso usarem o mesmo modelo de entrada, sempre teremos que passar um ID
de conta nulo para o caso de uso Registrar. Isso é irritante, na melhor das hipóteses, e prejudicial, na pior, porque
ambos os casos de uso estão acoplados para evoluir juntos agora.
Machine Translated by Google

42 Implementando um caso de uso

Permitir null como um estado válido de um campo em nosso objeto de comando imutável é um cheiro de código por si só.
Mas o mais importante é como estamos lidando com a validação de entrada agora? A validação deve ser diferente para os
casos de uso de Registro e Atualização, pois um precisa de um ID e o outro não. Teríamos que construir uma lógica de
validação personalizada nos próprios casos de uso, poluindo nosso sagrado código de negócios com preocupações de
validação de entrada.

Além disso, o que faremos se o campo de ID da conta acidentalmente tiver um valor não nulo no campo Registrar conta
caso de uso? Lançamos um erro? Nós simplesmente ignoramos isso? Estas são as perguntas que os engenheiros de
manutenção – incluindo nós do futuro – farão ao ver o código.

Um modelo de entrada dedicado para cada caso de uso torna o caso de uso muito mais claro e também o separa de outros
casos de uso, evitando efeitos colaterais indesejados. Porém, isso tem um custo porque temos que mapear os dados
recebidos para diferentes modelos de entrada para diferentes casos de uso. Discutiremos esta estratégia de mapeamento
juntamente com outras estratégias de mapeamento no Capítulo 9, Mapeamento entre Limites.

Validando regras de negócios


Embora a validação de entrada não faça parte da lógica do caso de uso, a validação de regras de negócios definitivamente
faz. As regras de negócios são o núcleo da aplicação e devem ser tratadas com o devido cuidado. Mas quando estamos
lidando com validação de entrada e quando estamos lidando com uma regra de negócio?

Uma distinção muito pragmática entre os dois é que a validação de uma regra de negócios requer acesso ao
estado atual do modelo de domínio, enquanto a validação de entrada não. A validação de entrada pode ser
implementada de forma declarativa, como fizemos anteriormente com as anotações @NotNull , enquanto uma
regra de negócios precisa de mais contexto.

Poderíamos também dizer que a validação de entrada é uma validação sintática, enquanto uma regra de negócio é uma
validação semântica no contexto de um caso de uso.

Vamos adotar a regra de que a conta de origem não deve estar com saldo negativo. Conforme definição
anterior, esta é uma regra de negócio, pois necessita de acesso ao estado atual do modelo para verificar o saldo do
conta de origem.

Em contraste, a regra de que o valor da transferência deve ser maior que zero pode ser validada sem acesso ao modelo e,
portanto, pode ser implementada como parte da validação de entrada.

Estou ciente de que esta distinção pode estar sujeita a debate. Você pode argumentar que o valor da transferência é tão
importante que validá-lo deveria ser considerado uma regra de negócios em qualquer caso.

A distinção nos ajuda, entretanto, a colocar certas validações dentro da base de código e encontrá- las facilmente novamente
mais tarde. É tão simples quanto responder à questão de saber se a validação precisa ou não de acesso ao estado atual do
modelo. Isto não só nos ajuda a implementar a regra em primeiro lugar, mas também ajuda o futuro engenheiro de manutenção
a encontrá-la novamente. É também um ótimo exemplo da minha afirmação do Capítulo 1, Manutenção, de que a manutenção
apoia a tomada de decisões.
Machine Translated by Google

Validando regras de negócios 43

Então, como implementamos uma regra de negócios?

A melhor maneira é colocar as regras de negócios em uma entidade de domínio, como fizemos para a regra de que a conta de origem não
deve estar com saldo negativo:

Dessa forma, a regra de negócios é fácil de localizar e raciocinar porque está próxima à lógica de negócios que exige que essa regra seja
respeitada.

Caso não seja viável validar uma regra de negócio em uma entidade de domínio, podemos fazê-lo no código do caso de uso antes de
começar a trabalhar nas entidades de domínio:

Chamamos um método que faz a validação real e lança uma exceção dedicada se a validação falhar. O adaptador que faz interface com

o usuário pode então exibir essa exceção ao usuário como uma mensagem de erro ou tratá-la de qualquer outra maneira que julgar
adequada.
Machine Translated by Google

44 Implementando um caso de uso

No caso anterior, a validação simplesmente verifica se as contas de origem e de destino realmente existem no banco de
dados. Regras de negócios mais complexas podem exigir que primeiro carreguemos o modelo de domínio do banco de
dados e depois façamos algumas verificações em seu estado. Se tivermos que carregar o modelo de domínio de qualquer
maneira, devemos implementar a regra de negócios nas próprias entidades do domínio, como fizemos com a regra de que
a conta de origem não deve estar com saldo negativo.

Modelo de domínio rico versus anêmico


Nosso estilo de arquitetura deixa em aberto como implementar nosso modelo de domínio. Isto é uma bênção porque
podemos fazer o que parece certo em nosso contexto, e uma maldição porque não temos diretrizes para nos ajudar.

Uma discussão frequente é se deve-se implementar um modelo de domínio rico seguindo a filosofia DDD ou um modelo
de domínio “anêmico” . Vamos discutir como cada um deles se encaixa em nossa arquitetura.

Em um modelo de domínio rico, o máximo possível da lógica do domínio é implementado nas entidades
no centro da aplicação. As entidades fornecem métodos para alterar o estado e só permitem alterações
válidas de acordo com as regras de negócio. Esta é a forma como buscamos a entidade Conta anteriormente.
Onde está nossa implementação de caso de uso neste cenário?

Nesse caso, nosso caso de uso serve como ponto de entrada para o modelo de domínio. Um caso de uso representa
apenas a intenção do usuário e a traduz em chamadas de método orquestradas para as entidades do domínio, que
fazem o trabalho real. Muitas das regras de negócios estão localizadas nas entidades, e não na implementação do caso de uso.

O serviço de caso de uso Enviar dinheiro carregaria as entidades da conta de origem e de destino, chamaria seus métodos
de retirada() e depósito() e os enviaria de volta ao banco de dados.4

Em um modelo de domínio “anêmico”, as próprias entidades são muito escassas. Eles geralmente fornecem apenas
campos para manter o estado e métodos getter e setter para ler e alterar o estado. Eles não contêm nenhuma lógica de
domínio.

Isso significa que a lógica do domínio é implementada nas classes de casos de uso. Eles são responsáveis por validar as
regras de negócio, alterar o estado das entidades e passá-las para as portas de saída responsáveis por armazená-las no
banco de dados. A “riqueza” está contida nos casos de uso e não nas entidades.

Qualquer um dos estilos, e vários outros estilos, podem ser implementados usando a abordagem de arquitetura discutida
neste livro. Fique à vontade para escolher aquele que se adapta às suas necessidades.

4 Na verdade, o caso de uso Enviar dinheiro também teria que garantir que nenhuma outra transferência de
dinheiro de e para a conta de origem e de destino ocorresse ao mesmo tempo para evitar saques a descoberto
uma conta.
Machine Translated by Google

Diferentes modelos de saída para diferentes casos de uso 45

Diferentes modelos de saída para diferentes casos de uso


Depois que o caso de uso tiver concluído seu trabalho, o que ele deverá retornar ao chamador?

Semelhante à entrada, há benefícios se a saída for o mais específica possível para o caso de uso. A saída deve incluir apenas os
dados realmente necessários para o funcionamento do chamador.

No código de exemplo do caso de uso Enviar dinheiro, retornamos um booleano. Este é o valor mínimo e mais específico que
poderíamos retornar neste contexto.

Poderíamos ficar tentados a retornar uma conta completa com a entidade atualizada ao chamador.
Talvez quem ligou esteja interessado no novo saldo da conta.

Mas será que realmente queremos que o caso de uso Enviar dinheiro retorne esses dados? O chamador realmente precisa disso?
Se sim, não deveríamos criar um caso de uso dedicado para acessar esses dados que podem ser usados por diferentes chamadores?

Não existe uma resposta certa para essas perguntas. Mas devemos pedir-lhes que tentem manter os nossos casos de uso tão
específicos quanto possível. Na dúvida, retorne o mínimo possível.

Compartilhar o mesmo modelo de saída entre casos de uso também tende a acoplar esses casos de uso. Se um dos casos de uso
precisar de um novo campo no modelo de saída, os outros casos de uso também terão que lidar com esse campo , mesmo que seja
irrelevante para eles. Os modelos compartilhados tendem a crescer tumorosamente por vários motivos no longo prazo. Aplicar o
Princípio da Responsabilidade Única e manter os modelos separados ajuda a dissociar os casos de uso.

Pela mesma razão, podemos querer resistir à tentação de usar nossas entidades de domínio como modelo de saída. Não
queremos que nossas entidades de domínio sejam alteradas por mais motivos do que o necessário. Entretanto, falaremos
mais sobre o uso de entidades como modelos de entrada ou saída no Capítulo 11, Utilizando atalhos conscientemente.

E quanto aos casos de uso somente leitura?


A partir de agora, discutimos como podemos implementar um caso de uso que modifique o estado do nosso modelo.
Como implementamos casos somente leitura? Vamos supor que a IU precise exibir o saldo de uma conta. Criamos uma
implementação de caso de uso específico para isso?

É estranho falar de casos de uso para operações somente leitura como esta. Claro, a IU precisa dos dados para um caso de uso que
podemos chamar de Visualizar saldo da conta, mas em alguns casos, chamar isso de “caso de uso” é um pouco artificial. Se este for
considerado um caso de uso no contexto do projeto, certamente devemos implementá-lo como os outros.

Do ponto de vista do núcleo do aplicativo, entretanto, esta é uma simples consulta de dados. Portanto, se não for considerado um
caso de uso no contexto do projeto, podemos implementá-lo como uma consulta para diferenciá-lo dos casos de uso reais.

Uma maneira de fazer isso dentro do nosso estilo de arquitetura é criar uma porta de entrada dedicada para a consulta e implementá-
la em um “serviço de consulta:”
Machine Translated by Google

46 Implementando um caso de uso

O serviço de consulta atua exatamente como nossos serviços de caso de uso de “comando”. Ele implementa uma porta de entrada chamada

GetAccountBalanceUseCase e chama a porta de saída, LoadAccountPort, para realmente carregar os dados do banco de dados. Ele está usando o

tipo GetAccountBalanceQuery como modelo de entrada.

Dessa forma, as consultas somente leitura são claramente distinguíveis da modificação de casos de uso (ou “comandos”)
em nossa base de código. Só precisamos olhar os nomes dos tipos de entrada para saber com quais estamos lidando.
Isso funciona bem com conceitos como Separação de Consulta de Comando (CQS) e Segregação de
Responsabilidade de Consulta de Comando (CQRS).

No código anterior, o serviço não faz nenhum trabalho além de passar a consulta para a porta de saída. Se usarmos o mesmo modelo entre camadas,

podemos pegar um atalho e deixar o cliente chamar diretamente a porta de saída. Falaremos sobre esse atalho no Capítulo 11, Usando atalhos

conscientemente.

Como isso me ajuda a construir software sustentável?


Nossa arquitetura nos permite implementar a lógica de domínio conforme acharmos adequado, mas se modelarmos a entrada e a saída de nossos

casos de uso de forma independente, evitaremos efeitos colaterais indesejados.

Sim, é mais trabalhoso do que apenas compartilhar modelos entre casos de uso. Temos que introduzir um modelo separado para cada caso de uso e

mapear entre este modelo e nossas entidades.

Mas os modelos específicos de caso de uso permitem uma compreensão nítida de um caso de uso, facilitando sua manutenção no longo prazo. Além

disso, eles permitem que vários desenvolvedores trabalhem em diferentes casos de uso em paralelo, sem atrapalhar uns aos outros.

Juntamente com a validação de entrada rigorosa, o uso de modelos de entrada e saída específicos para cada caso percorre um longo caminho em
direção a uma base de código sustentável.

No próximo capítulo, daremos um passo “para fora” do centro de nossa aplicação e exploraremos a construção de um adaptador web que forneça um

canal para os usuários conversarem com nosso caso de uso.


Machine Translated by Google

6
Implementando um Adaptador Web
A maioria dos aplicativos hoje tem algum tipo de interface web – seja uma UI com a qual podemos interagir por meio de
um navegador web ou uma API HTTP que outros sistemas podem chamar para interagir com nosso aplicativo.

Na nossa arquitetura alvo, toda a comunicação com o mundo exterior passa por adaptadores. Então, vamos discutir
como podemos implementar um adaptador que forneça essa interface web.

Inversão de Dependência
A Figura 6.1 oferece uma visão ampliada dos elementos da arquitetura que são relevantes para nossa discussão sobre
um adaptador web – o próprio adaptador e as portas através das quais ele interage com o núcleo do nosso aplicativo:

Figura 6.1 – Um adaptador de entrada se comunica com a camada de aplicação através de


portas de entrada dedicadas, que são interfaces implementadas pelos serviços do domínio

O adaptador web é um adaptador de “condução” ou de “entrada”. Ele recebe solicitações externas e as traduz em
chamadas para o núcleo do nosso aplicativo, informando o que fazer. O fluxo de controle vai dos controladores no
adaptador web até os serviços na camada de aplicativo.
Machine Translated by Google

48 Implementando um Adaptador Web

A camada de aplicação fornece portas específicas através das quais o web adaptor pode se comunicar.
Cada porta é o que chamei de “caso de uso” no capítulo anterior e é implementada por um serviço de
domínio na camada de aplicação.

Se olharmos mais de perto, notamos que este é o Princípio da Inversão de Dependência em ação. Como o fluxo de
controle vai da esquerda para a direita, poderíamos deixar o web adaptor chamar os casos de uso diretamente, como
mostrado na Figura 6.2.

Figura 6.2 – Podemos remover as interfaces das portas e chamar os serviços diretamente

Então, por que adicionamos outra camada de indireção entre o adaptador e os casos de uso? A razão é que as portas
são uma especificação dos locais onde o mundo exterior pode interagir com o núcleo da nossa aplicação. Ao ter portas
instaladas, sabemos exatamente qual comunicação ocorre com o mundo exterior , o que é uma informação valiosa
para qualquer engenheiro de manutenção que trabalhe em sua base de código herdada.

Conhecer as portas que controlam o aplicativo também nos permite construir um driver de teste para o aplicativo. Este
driver de teste é um adaptador que chama as portas de entrada para simular e testar determinados cenários de uso –
mais sobre testes no Capítulo 8, Testando Elementos de Arquitetura.

Tendo falado sobre a importância das portas de entrada, um dos atalhos sobre os quais falaremos no Capítulo 11,
Utilizando Atalhos Conscientemente, é simplesmente deixar as portas de entrada de fora e chamar diretamente os
serviços da aplicação.

Resta, porém, uma questão que é relevante para aplicações altamente interativas. Imagine um aplicativo de servidor
que envia dados em tempo real para o navegador do usuário via WebSocket. Como o núcleo do aplicativo envia esses
dados em tempo real para o web adaptor, que por sua vez os envia para o navegador do usuário?

Para este cenário, definitivamente precisamos de uma porta porque, sem uma porta, a aplicação teria que depender de
uma implementação de adaptador, quebrando nossos esforços para manter a aplicação livre de dependências externas.
Esta porta deve ser implementada pelo web adaptor e chamada pelo núcleo da aplicação, conforme ilustrado na Figura
6.3:
Machine Translated by Google

Responsabilidades de um adaptador web 49

Figura 6.3 – Se uma aplicação precisar notificar ativamente um web adaptor, precisamos
passar por uma porta de saída para manter as dependências na direção correta

O WebSocketController à esquerda implementa a interface da porta no pacote out , e os serviços no núcleo


do aplicativo podem chamar essa porta para enviar dados em tempo real ao navegador do usuário.

Tecnicamente falando, esta seria uma porta de saída e tornaria o adaptador web um adaptador de entrada e saída. Mas não há
razão para que o mesmo adaptador não possa ser os dois ao mesmo tempo. No restante deste capítulo, assumiremos que o
adaptador web é apenas um adaptador de entrada, já que este é o caso mais comum.

Responsabilidades de um adaptador web


O que um adaptador web realmente faz? Digamos que queremos fornecer uma API REST para nosso aplicativo BuckPal. Onde
começam e onde terminam as responsabilidades do web adaptor?

Um adaptador web geralmente faz o seguinte:

1. Mapeia a solicitação HTTP recebida para objetos.

2. Executa verificações de autorização.

3. Valida a entrada.

4. Mapeia os objetos de solicitação para o modelo de entrada do caso de uso.

5. Chama o caso de uso.

6. Mapeia a saída do caso de uso de volta para HTTP.

7. Retorna a resposta HTTP.

Primeiro de tudo, um web adaptor deve escutar solicitações HTTP que correspondam a determinados critérios, como caminho
de URL, método HTTP e tipo de conteúdo. Os parâmetros e o conteúdo de uma solicitação HTTP correspondente devem então
ser desserializados em objetos com os quais possamos trabalhar.

Normalmente, um web adaptor faz uma verificação de autenticação e autorização e retorna um erro se falhar.
Machine Translated by Google

50 Implementando um Adaptador Web

O estado dos objetos recebidos pode então ser validado. Mas já não discutimos a validação de entrada como uma
responsabilidade do modelo de entrada para os casos de uso? Sim, o modelo de entrada para os casos de uso deve permitir
apenas entradas válidas no contexto dos casos de uso. Mas aqui estamos falando sobre o modelo de entrada para o web
adaptor. Pode ter uma estrutura e semântica completamente diferentes do modelo de entrada para os casos de uso, então
talvez tenhamos que realizar validações diferentes.

Não defendo a implementação das mesmas validações no web adaptor como já fizemos no modelo de entrada dos casos de
uso. Em vez disso, devemos validar que podemos transformar o modelo de entrada do web adaptor no modelo de entrada
dos casos de uso. Qualquer coisa que nos impeça de fazer essa transformação é um erro de validação.

Isso nos leva à próxima responsabilidade de um web adaptor: chamar um determinado caso de uso com o modelo de entrada
transformado. O adaptador então pega a saída do caso de uso e a serializa em uma resposta HTTP, que é enviada de volta
ao chamador.

Se algo der errado no caminho e uma exceção for lançada, o web adaptor deverá traduzir o erro em uma mensagem que
será enviada de volta ao chamador.

São muitas responsabilidades que pesam sobre os ombros do nosso adaptador web. Mas também há muitas responsabilidades
com as quais a camada de aplicação não deveria se preocupar. Qualquer coisa que tenha a ver com HTTP não deve vazar
para a camada de aplicação. Se o núcleo do aplicativo souber que estamos lidando com HTTP externamente, perderemos a
opção de executar a mesma lógica de domínio de outros adaptadores de entrada que não usam HTTP. Em uma arquitetura
sustentável, queremos manter as opções abertas.

Observe que esse limite entre o adaptador da web e a camada de aplicativo surge naturalmente se iniciarmos o
desenvolvimento com as camadas de domínio e de aplicativo em vez de com a camada da web. Se implementarmos os
casos de uso primeiro, sem pensar em nenhum adaptador de entrada específico, não ficaremos tentados a confundir os
limites.

Controladores de fatiamento

Na maioria dos frameworks web – como Spring MVC no mundo Java – criamos classes de controladores que executam as
responsabilidades que discutimos anteriormente. Então, construímos um único controlador que responda a todas as
solicitações direcionadas à nossa aplicação? Não precisamos. Um adaptador web certamente pode consistir em mais de uma
classe.

Devemos ter o cuidado, entretanto, de colocar essas classes na mesma hierarquia de pacotes para marcá-las como
pertencentes umas às outras, conforme discutido no Capítulo 4, Código de Organização.

Então, quantos controladores construímos? Eu digo que deveríamos preferir construir muitos do que poucos. Devemos
garantir que cada controlador implemente uma fatia do adaptador web que seja o mais estreita possível e que compartilhe o
mínimo possível com outros controladores.

Vamos analisar as operações em uma entidade de conta em nosso aplicativo BuckPal. Uma abordagem
popular é criar um único AccountController que aceite solicitações para todas as operações relacionadas às contas.
Machine Translated by Google

Controladores de fatiamento 51

Um controlador Spring que fornece uma API REST pode se parecer com o seguinte trecho de código:
Machine Translated by Google

52 Implementando um Adaptador Web

Tudo relacionado ao recurso da conta está em uma única classe, o que é bom. Mas vamos discutir as
desvantagens dessa abordagem.

Primeiro, menos código por classe é uma coisa boa. Trabalhei em um projeto legado onde a maior classe tinha 30.000
linhas de código.1 Isso não é divertido. Mesmo que o controlador acumule apenas 200 linhas de código ao longo dos
anos, ainda é mais difícil compreender do que 50 linhas, mesmo quando está claramente separado em métodos.

O mesmo argumento é válido para código de teste. Se o próprio controlador tiver muito código, haverá muito código de
teste. E muitas vezes, o código de teste é ainda mais difícil de entender do que o código de produção porque tende a ser
mais abstrato. Também queremos fazer com que os testes de um determinado trecho do código de produção sejam fáceis
de encontrar, o que é mais fácil em turmas pequenas.

Igualmente importante, porém, é que colocar todas as operações em uma única classe de controlador incentiva a
reutilização de estruturas de dados. No exemplo de código anterior, muitas operações compartilham a classe de
modelo AccountResource . Serve de balde para tudo o que for necessário em qualquer uma das operações. ContaRecurso
provavelmente tem um campo de identificação . Isso não é necessário na operação de criação e provavelmente
confundirá mais aqui do que ajudará. Imagine que uma conta tenha um relacionamento um-para-muitos com o usuário
objetos. Incluímos esses objetos User ao criar ou atualizar uma conta? Os usuários serão retornados pela operação de
lista? Este é um exemplo fácil, mas em qualquer projeto acima do tamanho do jogo, faremos essas perguntas em algum
momento.

Portanto, defendo a abordagem de criação de um controlador separado, potencialmente em um pacote separado, para cada
operação. Além disso, devemos nomear os métodos e classes o mais próximo possível de nossos casos de uso:

1 30.000 linhas de código: na verdade foi uma decisão consciente de arquitetura (dos nossos antecessores, lembre-
se ) que levou essas 30.000 linhas a estarem em uma única classe: para alterar o sistema em tempo de execução,
sem reimplantação, permitiu-lhes fazer upload bytecode Java compilado em um arquivo .class . E só permitia que
eles carregassem um único arquivo, então esse arquivo deveria conter todo o código.
Machine Translated by Google

Controladores de fatiamento 53

Podemos usar primitivos como entrada, como fizemos com sourceAccountId, targetAccountId e amount no
exemplo. Mas cada controlador também pode ter seu próprio modelo de entrada. Em vez de um modelo
genérico como AccountResource, poderíamos então ter um modelo específico para o caso de uso, como
CreateAccountResource ou UpdateAccountResource. Essas classes de modelos especializados podem até
ser privadas do pacote do controlador para evitar reutilização acidental. Os controladores ainda podem
compartilhar modelos, mas usar classes compartilhadas de outro pacote nos faz pensar mais sobre isso e
talvez descubramos que não precisamos de metade dos campos e, afinal, criaremos os nossos próprios.

Além disso, devemos pensar bem nos nomes dos controladores e serviços. Em vez de CreateAccount, por
exemplo, RegisterAccount não seria um nome melhor? Em nosso aplicativo BuckPal, a única maneira de
criar uma conta é registrando-a pelo usuário. Portanto, usamos a palavra “registro” nos nomes das classes
para melhor transmitir seu significado. Certamente há casos em que os suspeitos usuais (Criar..., Atualizar...
e Excluir...) descrevem suficientemente um caso de uso, mas talvez queiramos pensar duas vezes antes de
realmente usá-los.

Outro benefício desse estilo de fatiamento é que ele facilita muito o trabalho paralelo em diferentes operações. Não
teremos conflitos de mesclagem se dois desenvolvedores trabalharem em operações diferentes.
Machine Translated by Google

54

Como isso me ajuda a construir software sustentável?


Ao construir um adaptador web para um aplicativo, devemos ter em mente que estamos construindo um adaptador
que traduz o protocolo HTTP em chamadas de método nos casos de uso de nosso aplicativo, traduz os resultados
de volta para HTTP e não faz nenhuma execução de domínio. lógica.

A camada de aplicação, por outro lado, não deve fazer HTTP, portanto devemos ter cuidado para não vazar detalhes
de HTTP. Isso torna o adaptador web substituível por outro adaptador, caso seja necessário.

Ao fatiar controladores web, não devemos ter medo de construir muitas classes pequenas que não compartilhem um
modelo. Eles são mais fáceis de compreender e testar e suportam trabalho paralelo. É mais trabalhoso inicialmente
configurar esses controladores refinados, mas valerá a pena durante a manutenção.

Tendo examinado o lado de entrada da nossa aplicação, daremos agora uma olhada no lado de saída e como
implementar um adaptador de persistência.
Machine Translated by Google

7
Implementando um
Adaptador de Persistência

No Capítulo 2, O que há de errado com as camadas? Reclamei de uma arquitetura tradicional em camadas
e afirmei que ela promove o design orientado a banco de dados porque, em última análise, tudo depende da
camada de persistência. Neste capítulo, veremos como tornar a camada de persistência um plugin para a
camada de aplicação para inverter essa dependência.

Inversão de dependência
Em vez de uma camada de persistência, falaremos sobre um adaptador de persistência que fornece funcionalidade de persistência
aos serviços de domínio. A Figura 7.1 mostra como podemos aplicar o Princípio da Inversão de Dependência para fazer exatamente
isso:

Figura 7.1 – Os serviços do núcleo utilizam portas para acessar o adaptador de persistência

Nossos serviços de domínio chamam interfaces de porta para acessar a funcionalidade de persistência. Essas portas são
implementadas por uma classe de adaptador de persistência que faz o trabalho real de persistência e é responsável por se
comunicar com o banco de dados.
Machine Translated by Google

56 Implementando um adaptador de persistência

No jargão da Arquitetura Hexagonal, o adaptador de persistência é um adaptador direcionado ou de


saída porque é chamado por nosso aplicativo e não o contrário.

As portas são efetivamente uma camada indireta entre os serviços de domínio e o código de persistência.
Vamos nos lembrar que estamos adicionando essa camada de indireção para podermos evoluir o código de domínio sem ter que
pensar em problemas de persistência, ou seja, sem dependências de código na camada de persistência. A refatoração no código de
persistência não levará a uma alteração de código no núcleo.

Naturalmente, em tempo de execução, ainda temos uma dependência do núcleo do nosso aplicativo para o adaptador de persistência.
Se modificarmos o código na camada de persistência e introduzirmos um bug, por exemplo, ainda poderemos quebrar a funcionalidade
no núcleo da aplicação. Porém, desde que os contratos das portas sejam cumpridos, estamos livres para fazer o que quisermos no
adaptador de persistência sem afetar o núcleo.

Responsabilidades de um adaptador de persistência


Vamos dar uma olhada no que um adaptador de persistência normalmente faz:

1. Recebe a entrada.

2. Mapeia a entrada no formato de banco de dados.

3. Envia a entrada para o banco de dados.

4. Mapeia a saída do banco de dados no formato do aplicativo.

5. Retorna a saída.

O adaptador de persistência recebe entrada por meio de uma interface de porta. O modelo de entrada pode ser uma entidade de
domínio ou um objeto dedicado a uma operação específica do banco de dados, conforme especificado pela interface.

Em seguida, ele mapeia o modelo de entrada para um formato com o qual pode trabalhar para modificar ou consultar o banco
de dados. Em projetos Java , normalmente usamos a Java Persistence API (JPA) para nos comunicarmos com um banco
de dados, portanto, podemos mapear a entrada em objetos de entidade JPA que refletem a estrutura das tabelas do banco de
dados. Dependendo do contexto, mapear o modelo de entrada em entidades JPA pode ser muito trabalhoso e com pouco
ganho, por isso falaremos sobre estratégias sem mapeamento no Capítulo 9, Mapeamento entre Limites.

Em vez de usar JPA ou outra estrutura de mapeamento objeto-relacional, podemos usar qualquer outra técnica para
nos comunicarmos com o banco de dados. Poderíamos mapear o modelo de entrada em instruções SQL simples e
enviá-las ao banco de dados, ou poderíamos serializar os dados recebidos em arquivos e lê-los de volta a partir daí.

A parte importante é que o modelo de entrada para o adaptador de persistência esteja dentro do núcleo do aplicativo, e não dentro do
próprio adaptador de persistência, de modo que as alterações no adaptador de persistência não afetem o núcleo.

Em seguida, o adaptador de persistência consulta o banco de dados e recebe os resultados da consulta.


Machine Translated by Google

Fatiamento de interfaces de porta 57

Finalmente, ele mapeia a resposta do banco de dados no modelo de saída esperado pela porta e a retorna.
Novamente, é importante que o modelo de saída esteja dentro do núcleo do aplicativo e não dentro do
adaptador de persistência para que as dependências apontem na direção certa.

Além do fato de que os modelos de entrada e saída estão no núcleo do aplicativo, e não no próprio adaptador de
persistência, as responsabilidades não são realmente diferentes daquelas de uma camada de persistência tradicional.

No entanto, implementar um adaptador de persistência conforme descrito aqui inevitavelmente levantará algumas questões que
provavelmente não faríamos ao implementar uma camada de persistência tradicional, já que estamos tão acostumados com a
forma tradicional que não pensamos nelas.

Fatiamento de interfaces de porta

Uma questão que vem à mente ao implementar serviços é como dividir as interfaces de porta que definem as operações de
banco de dados disponíveis para o núcleo do aplicativo.

É uma prática comum criar uma interface de repositório única que forneça todas as operações de banco de dados para uma
determinada entidade, conforme descrito na Figura 7.2.

Figura 7.2 – Centralizar todas as operações do banco de dados em uma única interface de porta de saída faz com

que todos os serviços dependam de métodos desnecessários

Cada serviço que depende de operações de banco de dados terá então uma dependência dessa única interface de porta
“ampla”, mesmo que use apenas um único método da interface. Isso significa que temos dependências desnecessárias em
nossa base de código.

As dependências de métodos que não precisamos em nosso contexto tornam o código mais difícil de entender e testar. Imagine
que estamos escrevendo um teste de unidade para RegisterAccountService da figura anterior. Para qual dos métodos da
interface AccountRepository devemos criar uma simulação ? Precisamos primeiro descobrir qual dos métodos AccountRepository
o serviço realmente chama.
Zombar de apenas parte da interface pode levar a outros problemas, pois a próxima pessoa que trabalhar nesse teste pode
esperar que a interface seja completamente ridicularizada e incorra em erros. Então, eles novamente precisam fazer algumas
pesquisas.
Machine Translated by Google

58 Implementando um adaptador de persistência

Nas palavras de Robert C. Martin: “Depender de algo que carrega uma bagagem que você não
precisa pode lhe causar problemas que você não esperava.”1

O Princípio de Segregação de Interface fornece uma resposta para esse problema. Afirma que interfaces amplas devem
ser divididas em interfaces específicas para que os clientes conheçam apenas os métodos de que precisam. Se aplicarmos
isso às nossas portas de saída, poderemos obter o resultado mostrado na Figura 7.3.

Figura 7.3 – A aplicação do Princípio de Segregação de Interface remove dependências

desnecessárias e torna as dependências existentes mais visíveis

Cada serviço agora depende apenas dos métodos de que realmente necessita. Além do mais, os nomes das portas
indicam claramente do que se trata. Em um teste, não precisamos mais pensar em quais métodos zombar , pois na maioria
das vezes existe apenas um método por porta.

Ter portas muito estreitas como essas torna a codificação uma experiência plug-and-play. Ao trabalhar em um serviço,
apenas “conectamos” as portas que precisamos. Não há bagagem para carregar.

É claro que a abordagem “um método por porto” pode não ser aplicável em todas as circunstâncias. Pode
haver grupos de operações de banco de dados que são tão coesos e frequentemente usados juntos que
podemos querer agrupá-los em uma única interface.

Fatiamento de adaptadores de persistência

Nas figuras anteriores, vimos uma única classe de adaptador de persistência que implementa todas as portas de
persistência. Não existe nenhuma regra, entretanto, que nos proíba de criar mais de um adaptador de persistência, desde
que todas as portas de persistência estejam implementadas.

1. Princípio de segregação de interface: arquitetura limpa por Robert C. Martin, página 86.
Machine Translated by Google

Fatiamento de adaptadores de persistência 59

Poderíamos optar, por exemplo, por implementar um adaptador de persistência por grupo de entidades de domínio para
as quais precisamos de operações de persistência (ou agregação no jargão do Domain-Driven Design), conforme
mostrado na Figura 7.4.

Figura 7.4 – Podemos criar múltiplos adaptadores de persistência, um para cada agregado

Dessa forma, nossos adaptadores de persistência são automaticamente divididos ao longo das costuras do domínio que
oferecemos suporte com funcionalidade de persistência.

Poderíamos dividir nossos adaptadores de persistência em ainda mais classes – por exemplo, quando quisermos implementar
algumas portas de persistência usando JPA (ou outro mapeador objeto-relacional) e algumas outras portas usando SQL simples
para melhor desempenho. Poderíamos então criar um adaptador JPA e um adaptador SQL simples, cada um implementando
um subconjunto de portas de persistência.

Lembre-se de que nosso código de domínio não se importa com qual classe cumpre os contratos definidos pelas portas de
persistência. Somos livres para fazer o que acharmos adequado na camada de persistência, desde que todas as portas estejam
implementadas.

A abordagem de um adaptador de persistência por agregado também é uma boa base para separar as necessidades de
persistência para vários contextos limitados no futuro. Digamos que, depois de um tempo, identificamos um contexto limitado
responsável pelos casos de uso relacionados ao faturamento. A Figura 7.5 adiciona esse novo domínio à aplicação.
Machine Translated by Google

60 Implementando um adaptador de persistência

Figura 7.5 – Se quisermos criar limites rígidos entre contextos limitados, cada contexto limitado

deve ter seu(s) próprio(s) adaptador(es) de persistência

Cada contexto limitado possui seu próprio adaptador de persistência (ou potencialmente mais de um,
conforme descrito anteriormente). O termo “contexto limitado” implica limites, o que significa que os serviços
do contexto da conta não podem acessar adaptadores de persistência do contexto de cobrança e vice-versa.
Se um contexto precisar de algo do outro, eles poderão chamar os serviços de domínio um do outro, ou
podemos introduzir um serviço de aplicação como coordenador entre os contextos limitados. Falaremos mais
sobre esse tópico no Capítulo 13, Gerenciando múltiplos contextos limitados.

Um exemplo com Spring Data JPA


Vamos dar uma olhada em um exemplo de código que implementa AccountPersistenceAdapter das
figuras anteriores. Este adaptador terá que salvar e carregar contas de e para o banco de dados. Já
vimos a entidade Conta no Capítulo 5, Implementando um Caso de Uso, mas aqui está seu esqueleto
novamente para referência:
Machine Translated by Google

Um exemplo com Spring Data JPA 61

Observação

A classe Account não é uma classe de dados simples com getters e setters, mas tenta ser o mais
imutável possível. Ele fornece apenas métodos de fábrica que criam uma conta em um estado
válido, e todos os métodos mutantes fazem alguma validação, como verificar o saldo da conta
antes de sacar dinheiro, para que não possamos criar um modelo de domínio inválido.
Machine Translated by Google

62 Implementando um adaptador de persistência

Usaremos Spring Data JPA para nos comunicarmos com o banco de dados, portanto, também precisaremos de classes anotadas com @Entity

para representar o estado do banco de dados de uma conta:

O estado de uma conta consiste apenas em um ID nesta fase. Posteriormente, campos adicionais,
como ID de usuário, poderão ser adicionados. Mais interessante é ActivityJpaEntity, que contém todas
as atividades de uma conta específica. Poderíamos ter conectado ActivitiyJpaEntity com AccountJpaEntity
por meio das anotações @ManyToOne ou @OneToMany do JPA para marcar a relação entre eles, mas optamos por
deixar isso de fora por enquanto, pois adiciona efeitos colaterais às consultas ao banco de dados. Na verdade, neste
Machine Translated by Google

Um exemplo com Spring Data JPA 63

Neste estágio, provavelmente seria mais fácil usar um mapeador objeto-relacional mais simples do que JPA para implementar
o adaptador de persistência, mas vamos usá-lo de qualquer maneira porque achamos que poderemos precisar dele no futuro.2

A seguir, usaremos Spring Data para criar interfaces de repositório que fornecem funcionalidades básicas de criação, leitura,
atualização e exclusão (CRUD) prontas para uso, bem como consultas personalizadas para carregar determinadas atividades
do banco de dados:

2. API Java Persistence: isso lhe parece familiar? Você escolhe JPA como mapeador OR porque é o que as
pessoas usam para esse problema. Depois de alguns meses de desenvolvimento, você amaldiçoa o
carregamento rápido e lento e os recursos de cache, desejando algo mais simples. JPA é uma ótima
ferramenta, mas para muitos problemas soluções mais simples podem ser, bem, mais simples. Dê uma olhada
no Spring Data JDBC ou jOOQ como alternativa.
Machine Translated by Google

64 Implementando um adaptador de persistência

O Spring Boot encontrará automaticamente esses repositórios, e o Spring Data fará sua mágica para fornecer uma
implementação por trás da interface do repositório que realmente se comunicará com o banco de dados.

Tendo entidades e repositórios JPA implementados, podemos implementar o adaptador de persistência que fornece
a funcionalidade de persistência para nossa aplicação:
Machine Translated by Google

Um exemplo com Spring Data JPA 65

O adaptador de persistência implementa duas portas necessárias ao aplicativo, LoadAccountPort


e UpdateAccountStatePort.
Para carregar uma conta do banco de dados, carregamos ela do AccountRepository e depois carregamos
as atividades dessa conta por um determinado período de tempo através do ActivityRepository.

Para criar uma entidade de domínio de conta válida , também precisamos do saldo que a conta tinha antes do
início desta janela de atividade, para obtermos a soma de todos os saques e depósitos desta conta do banco de dados.

Por fim, mapeamos todos esses dados para uma entidade de domínio Account e os retornamos ao chamador.

Para atualizar o estado de uma conta, iteramos todas as atividades da entidade Conta e verificamos se
elas possuem IDs. Caso contrário, são novas atividades, que persistimos por meio do ActivityRepository.

No cenário descrito anteriormente, temos um mapeamento bidirecional entre os modelos de domínio


Account e Activity e os modelos de banco de dados AccountJpaEntity e ActivityJpaEntity . Por que nos
esforçamos para mapear para frente e para trás? Não poderíamos simplesmente mover as anotações
JPA para as classes Account e Activity e armazená-las diretamente como entidades no banco de dados?
Machine Translated by Google

66 Implementando um adaptador de persistência

Essa estratégia de não mapeamento pode ser uma escolha válida, como veremos no Capítulo 9, Mapeamento
entre Fronteiras, quando falarmos sobre estratégias de mapeamento. No entanto, o JPA nos obriga a fazer
concessões no modelo de domínio. Por exemplo, o JPA exige que as entidades tenham um construtor sem
argumentos. Alternativamente, pode ser que na camada de persistência um relacionamento “muitos para um” faça
sentido do ponto de vista do desempenho, mas no modelo de domínio queremos que esse relacionamento seja o contrário.

Portanto, se quisermos criar um modelo de domínio rico sem comprometer a camada de persistência, teremos que mapear
entre o modelo de domínio e o modelo de persistência.

E quanto às transações de banco de dados?

Ainda não tocamos no tópico de transações de banco de dados. Onde colocamos nossos limites de transação?

Uma transação deve abranger todas as operações de gravação no banco de dados executadas em um determinado caso de
uso, garantindo que todas essas operações possam ser revertidas juntas se uma delas falhar.

Como o adaptador de persistência não sabe quais outras operações de banco de dados fazem parte do mesmo caso de uso,
ele não pode decidir quando abrir e fechar uma transação. Temos que delegar essa responsabilidade aos serviços que
orquestram as chamadas ao adaptador de persistência.

A maneira mais fácil de fazer isso com Java e Spring é adicionar a anotação @Transactional às classes de serviço de domínio
para que o Spring envolva todos os métodos públicos com uma transação:

Mas a anotação @Transactional não introduz uma dependência em uma estrutura que não queremos ter em
nosso precioso código de domínio? Bem, sim, temos uma dependência da anotação, mas conseguimos
tratamento de transações para essa dependência! Não gostaríamos de construir nosso próprio mecanismo de
transação apenas para que o código permanecesse “puro”.

Como isso me ajuda a construir software sustentável?


Construir um adaptador de persistência que atue como um plug-in para o código de domínio libera o código de domínio dos
detalhes de persistência para que possamos construir um modelo de domínio rico.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 67

Usando interfaces de portas estreitas, somos flexíveis para implementar uma porta de uma forma e
outra porta de outra, talvez até com uma tecnologia de persistência diferente, sem que a aplicação perceba.
Podemos até trocar toda a camada de persistência, desde que os contratos portuários sejam obedecidos.3

Agora que construímos um modelo de domínio e alguns adaptadores, vamos ver como podemos testar se eles
estão realmente fazendo o que esperamos que façam.

3. Troca da camada de persistência: embora eu tenha visto isso acontecer algumas vezes (e por boas razões), a
probabilidade de ter que trocar toda a camada de persistência é geralmente bastante baixa. Mesmo assim,
ainda vale a pena ter portas de persistência dedicadas, porque aumenta a testabilidade. Podemos implementar
facilmente um adaptador de persistência na memória para ser usado em testes, por exemplo.
Machine Translated by Google
Machine Translated by Google

8
Testando Elementos de Arquitetura
Em muitos projetos que testemunhei, especialmente projetos que já existem há algum tempo e que entraram e saíram de muitos
desenvolvedores ao longo do tempo, os testes automatizados são um mistério. Todo mundo escreve testes como bem entendem,
porque isso é exigido por alguma regra empoeirada documentada em um wiki, mas ninguém pode responder a perguntas
específicas sobre a estratégia de testes de uma equipe.

Este capítulo fornece uma estratégia de teste para uma arquitetura hexagonal. Para cada elemento de
nossa arquitetura, discutiremos o tipo de teste que o cobrirá.

A pirâmide de teste
Vamos começar a discussão sobre testes nos moldes da pirâmide de testes1 na Figura 8.1, que
é uma metáfora que nos ajuda a decidir quantos testes e de que tipo devemos almejar.

Figura 8.1 – De acordo com a pirâmide de testes, devemos criar muitos testes baratos e menos testes caros

1 A pirâmide de testes remonta ao livro de Mike Cohn, Succeeding with Agile, de 2009.
Machine Translated by Google

70 Testando Elementos de Arquitetura

A afirmação básica da pirâmide é que devemos ter uma alta cobertura de testes refinados que sejam baratos de
construir, fáceis de manter, rápidos e estáveis. São testes unitários que verificam se uma única unidade
(geralmente uma classe) funciona conforme o esperado.

Uma vez que os testes combinam múltiplas unidades e ultrapassam limites de unidade, limites de arquitetura ou mesmo limites
de sistema, eles tendem a se tornar mais caros para construir, mais lentos para executar e mais frágeis (falhando devido a
algum erro de configuração em vez de um erro funcional). A pirâmide nos diz que quanto mais caros esses testes se tornam,
menos devemos almejar uma alta cobertura desses testes porque, caso contrário, gastaremos muito tempo construindo testes
em vez de novas funcionalidades.

Dependendo do contexto, a pirâmide de teste é frequentemente mostrada com diferentes camadas. Vamos dar uma olhada
nas camadas que escolhi para discutir o teste de nossa arquitetura hexagonal.

Observação

As definições de teste de unidade, teste de integração e teste de sistema variam de acordo com o contexto. Em um
projeto, eles podem significar algo diferente de outro.

A seguir estão interpretações de diferentes tipos de teste, conforme os usaremos neste capítulo:

• Os testes unitários são a base da pirâmide. Um teste unitário geralmente instancia uma única classe e testa sua
funcionalidade por meio de sua interface. Se a classe em teste tiver dependências não triviais de outras classes,
podemos substituir essas dependências por objetos simulados que simulem o comportamento dos objetos reais,
conforme exigido pelo teste.

• Os testes de integração formam a próxima camada da pirâmide. Esses testes instanciam uma rede de múltiplas
unidades e verificam se essa rede funciona conforme o esperado, enviando alguns dados para ela através da interface
de uma classe de entrada. Em nossa interpretação, os testes de integração cruzarão a fronteira entre duas camadas,
portanto a rede de objetos não está completa ou deverá funcionar contra simulações em algum momento.

• Os testes de sistema, por fim, giram toda a rede de objetos que compõem nossa aplicação e verificam se um
determinado caso de uso funciona conforme o esperado em todas as camadas da aplicação.

Acima dos testes do sistema, pode haver uma camada de testes ponta a ponta que inclui a UI do aplicativo.
Não consideraremos testes de ponta a ponta aqui, pois neste livro estamos discutindo apenas uma arquitetura de back-end.

Observação

A pirâmide de testes, como qualquer outra orientação, não é uma solução mágica para sua estratégia de testes. É um
bom padrão, mas se, no seu contexto, você puder criar e manter testes de integração ou de sistema de forma barata,
você pode e deve criar mais desses testes, pois eles são menos vulneráveis a mudanças nos detalhes de
implementação do que os testes de unidade. Isto tornaria os lados da pirâmide mais íngremes, ou talvez até os
inverteria.

Agora que definimos alguns tipos de teste, vamos ver qual tipo de teste se adapta melhor a cada uma das camadas da nossa
Arquitetura Hexagonal.
Machine Translated by Google

Testando uma entidade de domínio com testes unitários 71

Testando uma entidade de domínio com testes unitários

Começaremos examinando uma entidade de domínio no centro de nossa arquitetura. Vamos relembrar a conta
entidade do Capítulo 5, Implementando um caso de uso. O estado da conta consiste num saldo que uma conta
tinha num determinado momento no passado (o saldo de base) e numa lista de depósitos e levantamentos
(actividades) efectuados desde então.

Queremos agora verificar se o método retirar() funciona conforme o esperado:

O teste anterior é um teste de unidade simples que instancia uma Conta em um estado específico, chama seu método retirar()
e verifica se a retirada foi bem-sucedida e teve os efeitos colaterais esperados no estado do objeto Conta em teste.

O teste é bastante fácil de configurar, fácil de entender e executado muito rápido. Os testes não são muito mais simples do
que isso. Testes unitários como esses são nossa melhor aposta para verificar as regras de negócios codificadas em nossas
entidades de domínio. Não precisamos de nenhum outro tipo de teste, pois o comportamento da entidade de domínio tem
pouca ou nenhuma dependência de outras classes.
Machine Translated by Google

72 Testando Elementos de Arquitetura

Testando um caso de uso com testes unitários


Indo uma camada para fora, o próximo elemento da arquitetura a testar são os casos de uso implementados
como serviços de domínio. Vejamos um teste para SendMoneyService, discutido no Capítulo 5,
Implementando um caso de uso. O caso de uso Enviar dinheiro retira dinheiro da conta de origem e o
deposita na conta de destino. Queremos verificar se tudo funciona conforme o esperado quando a transação for bem-sucedida:

Para tornar o teste um pouco mais legível, ele está estruturado em seções dado/quando/então , que são comumente
usadas no Desenvolvimento Orientado a Comportamento.
Machine Translated by Google

Testando um Web Adaptor com Testes de Integração 73

Na seção fornecida , criamos os objetos Conta de origem e destino e os colocamos no estado correto
com alguns métodos cujos nomes começam com dado...(). Também criamos um SendMoneyCommand
objeto para atuar como entrada para o caso de uso. Na seção quando , simplesmente chamamos o método
sendMoney() para invocar o caso de uso. A seção then afirma que a transação foi bem-sucedida e verifica se
determinados métodos foram chamados nos objetos Account de origem e de destino .

Nos bastidores, o teste faz uso da biblioteca Mockito para criar objetos simulados no dado...()
métodos.2 Mockito também fornece o método then() para verificar se um determinado método foi chamado em
um objeto simulado.

Observação

Se usado demais, a zombaria pode dar uma falsa sensação de segurança. Os simulados podem se
comportar de maneira diferente dos reais, causando problemas na produção, mesmo que nossos testes
sejam verdes. Se você puder usar objetos reais em vez de simulações sem muito esforço extra,
provavelmente deveria fazê-lo. No exemplo anterior, poderíamos optar por trabalhar com objetos Account
reais em vez de simulados, por exemplo. Isso não deve exigir muito mais esforço porque a classe
Account é uma classe de modelo de domínio que não possui dependências complicadas de outras classes.

Como o serviço de caso de uso em teste não tem estado, não podemos verificar um determinado estado na seção then .
Em vez disso, o teste verifica se o serviço interagiu com determinados métodos em suas dependências (simuladas). Isso significa

que o teste é vulnerável a alterações na estrutura do código sob teste e não apenas no seu comportamento. Isso, por sua vez,
significa que há uma chance maior de o teste precisar ser modificado se o código em teste for refatorado.

Com isso em mente, devemos pensar bem sobre quais interações realmente queremos verificar no teste. Pode ser uma boa ideia
não verificar todas as interações como fizemos no teste anterior e, em vez disso, focar nas mais importantes. Caso contrário, teremos
que alterar o teste a cada alteração na classe em teste, prejudicando o valor do teste.

Embora este teste ainda seja um teste de unidade, ele quase é um teste de integração porque testamos a interação nas dependências.
No entanto, é mais fácil de criar e manter do que um teste de integração completo porque estamos trabalhando com simulações e
não precisamos gerenciar as dependências reais.

Testando um Web Adaptor com Testes de Integração


Movendo-se para fora de outra camada, chegamos aos nossos adaptadores. Vamos discutir o teste de um adaptador web.

Lembre-se de que um adaptador da web recebe entrada, por exemplo, na forma de strings JSON, via HTTP, pode fazer alguma
validação nela, mapeia a entrada para o formato que um caso de uso espera e, em seguida, passa-a para esse caso de uso.
Em seguida, ele mapeia o resultado do caso de uso de volta para JSON e o retorna ao cliente por meio de uma resposta HTTP.

2Mockito: https://site.mockito.org/.
Machine Translated by Google

74 Testando Elementos de Arquitetura

No teste de um adaptador web, queremos ter certeza de que todas essas etapas funcionam conforme o esperado:

O teste anterior é um teste de integração padrão para um controlador web chamado SendMoneyController,
construído com a estrutura Spring Boot. No método testSendMoney() , enviamos uma solicitação HTTP
simulada ao controlador da web para acionar uma transação de uma conta para outra.

Com o método isOk() , verificamos então se o status da resposta HTTP é 200 e se a


classe de caso de uso simulada foi chamada.

A maioria das responsabilidades de um adaptador web são cobertas por este teste.

Na verdade, não estamos testando por meio do protocolo HTTP, pois estamos zombando disso com o MockMvc
objeto. Confiamos que a estrutura traduz tudo de e para HTTP de maneira adequada. Não há necessidade de testar a estrutura.
Machine Translated by Google

Testando um adaptador de persistência com testes de integração 75

No entanto, todo o caminho desde o mapeamento da entrada do JSON em um objeto SendMoneyCommand é


coberto. Se construirmos o objeto SendMoneyCommand como um comando de autovalidação, conforme explicado
no Capítulo 5, Implementando um caso de uso, nos certificaremos até mesmo de que esse mapeamento produza
uma entrada sintaticamente válida para o caso de uso. Além disso, verificamos se o caso de uso é realmente
chamado e se a resposta HTTP possui o status esperado.

Então, por que isso é um teste de integração e não um teste unitário? Embora pareça que testamos apenas uma
única classe de controlador da web neste teste, há muito mais acontecendo nos bastidores. Com o @WebMvcTest
anotação, dizemos ao Spring para instanciar toda uma rede de objetos que é responsável por responder a
determinados caminhos de solicitação, mapear entre Java e JSON, validar a entrada HTTP e assim por
diante. E neste teste verificamos se nosso controlador web funciona como parte desta rede.

Como o controlador da web está fortemente acoplado à estrutura Spring, faz sentido testá-lo quando integrado
a esta estrutura, em vez de testá-lo isoladamente. Se testássemos o controlador web com um teste de unidade
simples, perderíamos a cobertura de todo o mapeamento, validação e coisas HTTP, e nunca poderíamos ter
certeza se ele realmente funcionou na produção, onde é apenas uma engrenagem na mecânica de o quadro.

Testando um adaptador de persistência com testes de integração


Por uma razão semelhante, faz sentido cobrir os adaptadores de persistência com testes de integração em vez de testes
unitários, uma vez que não queremos apenas verificar a lógica dentro do adaptador, mas também o mapeamento no banco de dados.

Queremos testar o adaptador de persistência que construímos no Capítulo 7, Implementando um adaptador de persistência.
O adaptador possui dois métodos, um para carregar uma entidade Conta do banco de dados e outro para salvar
novas atividades de conta no banco de dados:
Machine Translated by Google

76 Testando Elementos de Arquitetura

Com @DataJpaTest, dizemos ao Spring para instanciar a rede de objetos necessários para acesso ao banco
de dados, incluindo nossos repositórios Spring Data que se conectam ao banco de dados. Usamos o @Import
anotação para importar algumas configurações adicionais para garantir que determinados objetos sejam adicionados
a essa rede. Esses objetos são necessários ao adaptador em teste para mapear objetos de domínio de entrada em
objetos de banco de dados, por exemplo.
Machine Translated by Google

Testando caminhos principais com testes de sistema 77

No teste do método loadAccount() , colocamos o banco de dados em um determinado estado usando um script SQL
com o nome AccountPersistenceAdapterTest.sql. Em seguida, simplesmente carregamos a conta por meio da API do
adaptador e verificamos se ela possui o estado que esperaríamos, dado o estado do banco de dados no script SQL.

O teste para updateActivities() faz o contrário. Criamos um objeto Account com uma nova
atividade de conta e o passamos para o adaptador persistir. Em seguida, verificamos se a
atividade foi salva no banco de dados através da API do ActivityRepository.
Um aspecto importante desses testes é que não estamos zombando do banco de dados. Os testes realmente
atingiram o banco de dados. Se tivéssemos zombado do banco de dados, os testes ainda cobririam as mesmas linhas
de código, produzindo a mesma alta cobertura de linhas de código. Entretanto, apesar dessa alta cobertura, os testes
ainda teriam uma chance bastante alta de falhar em uma configuração com um banco de dados real, devido a erros
em instruções SQL ou erros inesperados de mapeamento entre tabelas de banco de dados e objetos Java.

Observe que, por padrão, o Spring criará um banco de dados na memória para usar durante os testes. Isso é muito
prático, pois não precisamos configurar nada e os testes funcionarão imediatamente. No entanto, como esse banco
de dados na memória provavelmente não é o banco de dados que usamos na produção, ainda há uma chance
significativa de algo dar errado com o banco de dados real, mesmo quando os testes funcionam perfeitamente no
banco de dados na memória. Os fornecedores de bancos de dados adoram implementar seu próprio tipo de SQL, por exemplo.

Por esse motivo, os testes do adaptador de persistência devem ser executados no banco de dados real. Bibliotecas como
Testcontainers são de grande ajuda nesse sentido, criando um contêiner Docker com um banco de dados sob demanda.3

A execução no banco de dados real tem o benefício adicional de não precisarmos cuidar de dois sistemas de banco
de dados diferentes. Se usarmos o banco de dados na memória durante os testes, poderemos ter que configurá-lo de
uma determinada maneira ou criar versões separadas de scripts de migração de banco de dados para cada banco de
dados, o que é um grande sucesso na manutenção de nossos testes.

Testando caminhos principais com testes de sistema

No topo da pirâmide estão o que chamo de testes de sistema. Um teste de sistema inicia todo o aplicativo e
executa solicitações em sua API, verificando se todas as nossas camadas funcionam em conjunto.

A Arquitetura Hexagonal trata da criação de uma fronteira bem definida entre nossa aplicação e o mundo
exterior. Isso torna nossos limites de aplicação muito testáveis por design. Para testar nossa aplicação
localmente, precisamos apenas trocar os adaptadores por adaptadores simulados, conforme descrito na Figura 8.2.

3 Testcontainers: https://www.testcontainers.org/.
Machine Translated by Google

78 Testando Elementos de Arquitetura

Figura 8.2 – Ao substituir os adaptadores por mocks, podemos executar e testar


nossa aplicação sem dependências do mundo externo

À esquerda, podemos substituir os adaptadores de entrada por um driver de teste que chama as portas de entrada da aplicação
para interagir com ela. O driver de teste pode implementar determinados cenários de teste que simulam o comportamento do
usuário durante um teste automatizado.

À direita, podemos substituir os adaptadores de saída por adaptadores simulados que simulam o comportamento de um adaptador
real e retornam valores especificados anteriormente.4

Desta forma, podemos criar “testes de aplicação” que cobrem o “hexágono” da nossa aplicação desde as portas de entrada, passando
pelos nossos serviços e entidades de domínio, até às portas de saída.

Eu argumentaria, entretanto, que, em vez de escrever “testes de aplicação” que zombam dos adaptadores de entrada e saída,
deveríamos ter como objetivo escrever “testes de sistema” que cobrissem todo o caminho desde um adaptador de entrada real até
um adaptador de saída real. Esses testes revelam muitos bugs sutis que não detectaríamos se descartássemos os adaptadores de
entrada e saída. Esses bugs incluem erros de mapeamento entre as camadas ou simplesmente expectativas erradas entre o aplicativo
e os sistemas externos com os quais ele está se comunicando.

Um “teste de sistema” como esse exige que possamos ativar os sistemas externos reais com os quais nosso aplicativo se comunica
em uma configuração de teste.

4 Mocks: dependendo de quem você pergunta e do que você está fazendo no seu teste, em vez de chamá-lo de “simulado”, você
deve chamá-lo de “falso” ou “esboço”. Cada termo parece ter uma semântica ligeiramente diferente, mas no final, todos substituem
uma coisa “real” por uma coisa “simulada” para ser usada em testes. Normalmente sou fã de nomear as coisas corretamente,
mas, neste caso, não vejo valor em discutir as nuances entre onde termina uma simulação e começa um esboço. Ou é o contrário?
Machine Translated by Google

Testando caminhos principais com testes de sistema 79

No lado da entrada, precisamos ter certeza de que podemos fazer chamadas HTTP reais para nosso aplicativo,
por exemplo, para que as solicitações passem por nosso adaptador web real. Entretanto, isso deve ser bastante
fácil, já que precisamos apenas iniciar nosso aplicativo localmente e deixá-lo ouvir chamadas HTTP como faria
em um ambiente de produção.

No lado da saída, precisamos criar um banco de dados real, por exemplo, para que nossos testes passem pelo
adaptador de persistência real. A maioria dos bancos de dados facilita isso hoje, fornecendo uma imagem Docker que
podemos ativar localmente. Se nosso aplicativo se comunicar com um sistema de terceiros que não seja um banco de
dados, ainda devemos tentar encontrar (ou criar) uma imagem Docker que contenha esse sistema para que possamos
testar nosso aplicativo contra ele, girando um contêiner Docker local.

Se nenhuma imagem Docker estiver disponível para um determinado sistema externo, podemos escrever um adaptador
de saída simulado personalizado que simule a coisa real. A arquitetura hexagonal facilita a substituição do adaptador
de saída real por esta simulação para fins de nossos testes. E se uma imagem Docker estiver disponível, podemos
mudar para o adaptador de saída real sem muito esforço.

Existem razões válidas para testar adaptadores simulados em vez de adaptadores reais, é claro. Se nosso aplicativo
for executado em vários perfis, por exemplo, e cada perfil usar um adaptador de entrada ou saída diferente (real)
implementado nas mesmas portas de entrada e saída, poderemos querer testes que isolem erros no aplicativo de erros
nos adaptadores . Testes de aplicação que cobrem apenas nosso hexágono são exatamente a ferramenta que
queremos, então. No entanto, para uma aplicação web padrão com um banco de dados, onde os adaptadores de
entrada e saída são bastante estáticos, provavelmente desejaremos nos concentrar nos testes do sistema.

Como seria um teste de sistema? Em um teste de sistema para o caso de uso Enviar dinheiro, enviamos uma solicitação
HTTP para a aplicação e validamos a resposta, bem como o novo saldo da conta.

No mundo Java e Spring, pode ser assim:


Machine Translated by Google

80 Testando Elementos de Arquitetura

Com @SpringBootTest, dizemos ao Spring para inicializar toda a rede de objetos que compõe a
aplicação. Também configuramos o aplicativo para se expor em uma porta aleatória.

No método de teste, simplesmente criamos uma solicitação, enviamos para a aplicação e depois verificamos
o status da resposta e o novo saldo das contas.

Usamos um TestRestTemplate para enviar a solicitação, e não o MockMvc, como fizemos anteriormente no
teste do adaptador web. Isso significa que o teste faz chamadas HTTP reais, aproximando o teste um pouco
mais de um ambiente de produção.
Machine Translated by Google

Quantos testes são suficientes? 81

Assim como passamos pelo HTTP real, passamos pelos adaptadores de saída reais. No nosso caso, trata-se
apenas de um adaptador de persistência que conecta a aplicação a um banco de dados. Em uma aplicação que se
comunica com outros sistemas, teríamos adaptadores de saída adicionais instalados. Nem sempre é viável ter
todos esses sistemas de terceiros em funcionamento, mesmo para um teste de sistema, então, afinal, podemos zombar deles.
Nossa arquitetura hexagonal torna isso o mais fácil possível para nós, já que só precisamos criar
algumas interfaces de porta de saída.

Observe que me esforcei para tornar o teste o mais legível possível. Eu escondi toda a lógica feia nos métodos auxiliares.
Esses métodos agora formam uma linguagem específica de domínio que podemos usar para verificar o estado das coisas.

Embora uma linguagem específica de domínio como essa seja uma boa ideia em qualquer tipo de teste, ela é ainda mais
importante em testes de sistema. Os testes de sistema simulam os usuários reais do aplicativo muito melhor do que os
testes unitários ou de integração, portanto, podemos usá-los para verificar o aplicativo do ponto de vista do usuário.
Isso é muito mais fácil com um vocabulário adequado disponível. Esse vocabulário também permite que especialistas do
domínio, que são mais adequados para incorporar um usuário do aplicativo e provavelmente não são programadores,
raciocinem sobre os testes e forneçam feedback. Existem bibliotecas inteiras para desenvolvimento orientado a
comportamento, como JGiven5 , que fornecem uma estrutura para criar um vocabulário para seus testes.

Se criarmos testes unitários e de integração conforme descrito nas seções anteriores, os testes do sistema cobrirão grande
parte do mesmo código. Eles oferecem algum benefício adicional? Sim, eles fazem. Normalmente, eles eliminam outros
tipos de bugs além dos testes de unidade e integração. Algum mapeamento entre as camadas pode estar errado, por
exemplo, o que não notamos apenas com os testes unitários e de integração.

Os testes de sistema funcionam melhor se combinarem vários casos de uso para criar cenários. Cada cenário representa
um determinado caminho que um usuário normalmente pode seguir no aplicativo. Se os cenários mais importantes forem
cobertos pela aprovação nos testes do sistema, podemos assumir que não os quebramos com nossas modificações mais
recentes e estamos prontos para envio.

Quantos testes são suficientes?


Uma pergunta da qual muitas equipes de projeto não conseguiram responder é quantos testes devemos fazer.
É suficiente que nossos testes cubram 80% de nossas linhas de código? Deveria ser maior que isso?

A cobertura da linha é uma métrica ruim para medir o sucesso do teste. Qualquer meta diferente de 100% é
completamente sem sentido porque partes importantes da base de código podem não ser cobertas.6 E mesmo em
100%, ainda não podemos ter certeza de que todos os bugs foram eliminados.

Sugiro medir o sucesso do teste pelo quão confortáveis nos sentimos ao enviar o software. Se confiarmos nos testes o
suficiente para serem enviados após executá-los, estaremos bem. Quanto mais enviamos, mais confiança temos em
nossos testes. Se enviarmos apenas duas vezes por ano, ninguém confiará nos testes porque eles só comprovam seu
valor duas vezes por ano.

5 JGiven: https://jgiven.org/.
6 Cobertura de teste: se você quiser ler mais sobre 100% de cobertura de teste, dê uma olhada no meu artigo com
o título irônico Por que você deve impor 100% de cobertura de código em https://reflectoring.
io/100% de cobertura de teste/.
Machine Translated by Google

82

Isso exige um ato de fé nas primeiras vezes que lançamos, mas se priorizarmos a correção e aprendermos com os bugs na
produção, estaremos no caminho certo. Para cada bug de produção, devemos fazer a pergunta: “Por que nossos testes não
detectaram esse bug?”, documentar a resposta e então adicionar um teste que o cubra. Com o tempo, isso nos deixará
confortáveis com o envio, e a documentação fornecerá até mesmo uma métrica para avaliar nossa melhoria ao longo do tempo.

Ajuda, no entanto, começar com uma estratégia que defina os testes que devemos criar. Uma dessas estratégias para nossa
Arquitetura Hexagonal é esta:

• Ao implementar uma entidade de domínio, cubra-a com um teste de unidade.

• Ao implementar um serviço de caso de uso, cubra-o com um teste de unidade.

• Ao implementar um adaptador, cubra-o com um teste de integração.

• Cubra os caminhos mais importantes que um usuário pode percorrer no aplicativo com um teste de sistema.

Observe a frase durante a implementação – quando os testes são feitos durante o desenvolvimento de um recurso e não
depois, eles se tornam uma ferramenta de desenvolvimento e não parecem mais uma tarefa árdua.

Entretanto, se tivermos que gastar uma hora corrigindo testes toda vez que adicionamos um novo campo, estaremos fazendo
algo errado. Provavelmente, nossos testes são muito vulneráveis a mudanças estruturais no código, e deveríamos ver como
melhorar isso. Os testes perdem seu valor se tivermos que modificá-los a cada refatoração.

Como isso me ajuda a construir software sustentável?


O estilo de arquitetura hexagonal separa claramente a lógica de domínio e os adaptadores voltados para fora. Isso nos ajuda
a definir uma estratégia de teste clara que cobre a lógica do domínio central com testes unitários e os adaptadores com testes
de integração.

As portas de entrada e saída fornecem pontos de simulação muito visíveis nos testes. Para cada porta, podemos decidir
simular ou usar a implementação real. Se as portas forem muito pequenas e focadas, zombar delas será muito fácil, em vez de
uma tarefa árdua. Quanto menos métodos uma interface de porta fornecer, menos confusão haverá sobre quais métodos
devemos simular em um teste.

Se for muito difícil zombar das coisas, ou se não soubermos que tipo de teste devemos usar para cobrir uma determinada parte
da base de código, isso é um sinal de alerta. Nesse sentido, nossos testes têm a responsabilidade adicional de ser um canário
– alertar-nos sobre falhas na arquitetura e nos orientar de volta no caminho da criação de uma base de código sustentável.

Até agora, falamos sobre nossos casos de uso e nossos adaptadores principalmente de forma isolada. Como eles se
comunicam ? No próximo capítulo, daremos uma olhada em algumas estratégias sobre como projetar modelos de dados que
constituem a linguagem comum entre eles.
Machine Translated by Google

9
Mapeamento entre limites
Nos capítulos anteriores, discutimos as camadas web, aplicação, domínio e persistência e como cada uma
dessas camadas contribui para a implementação de um caso de uso.

No entanto, mal tocamos no temido e onipresente tema do mapeamento entre os modelos de cada camada. Aposto que você já discutiu em

algum momento sobre usar o mesmo modelo em duas camadas para evitar a implementação de um mapeador.

O argumento poderia ter sido mais ou menos assim:

Desenvolvedor pró-mapeamento:

“Se não mapearmos entre camadas, teremos que usar o mesmo modelo em ambas as camadas, o que significa que as camadas estarão

fortemente acopladas!”

Desenvolvedor de contra-mapeamento:

“Mas se mapearmos entre camadas, produziremos muito código clichê, o que é um exagero para muitos casos de uso, já que eles estão

apenas fazendo CRUD e têm o mesmo modelo em todas as camadas de qualquer maneira!”

Como costuma acontecer em discussões como esta, há verdade em ambos os lados do argumento. Vamos discutir algumas estratégias de

mapeamento com seus prós e contras e ver se podemos ajudar esses desenvolvedores a tomar uma decisão.
Machine Translated by Google

84 Mapeamento entre limites

A estratégia “Sem Mapeamento”


A primeira estratégia é, na verdade, não mapear.

Figura 9.1 – Se as interfaces portuárias utilizam o modelo de domínio como modelo


de entrada e saída, podemos optar por não mapear entre camadas

A Figura 9.1 mostra os componentes que são relevantes para o caso de uso Enviar dinheiro do nosso aplicativo de exemplo
BuckPal.

Na camada web, o controlador web chama a interface SendMoneyUseCase para executar o caso de uso.
Esta interface usa um objeto Account como argumento. Isso significa que as camadas web e de aplicação precisam de acesso
à classe Account – ambas estão usando o mesmo modelo.

Do outro lado da aplicação, temos o mesmo relacionamento entre a persistência e a camada de aplicação.

Como todas as camadas utilizam o mesmo modelo, não precisamos implementar o mapeamento entre elas.

Mas quais são as consequências deste design?

As camadas web e de persistência podem ter requisitos especiais para seus modelos. Se nossa camada web
expõe seu modelo via REST, por exemplo, as classes do modelo podem precisar de algumas anotações que
definam como serializar determinados campos em JSON. O mesmo se aplica à camada de persistência se
estivermos usando uma estrutura de mapeamento objeto-relacional (ORM) , que pode exigir algumas anotações
que definam o mapeamento do banco de dados. A estrutura também pode exigir que a classe siga um determinado contrato.

No exemplo, todos esses requisitos especiais devem ser tratados na classe de modelo de domínio
Account , mesmo que as camadas de domínio e aplicação não estejam interessadas neles. Isso viola o
Princípio da Responsabilidade Única, uma vez que a classe Account deve ser alterada por motivos
relacionados às camadas da web, do aplicativo e de persistência.

Além dos requisitos técnicos, cada camada pode exigir determinados campos personalizados na conta
aula. Isto pode levar a um modelo de domínio fragmentado com determinados campos relevantes apenas em uma camada.

Será que isto significa, porém, que nunca devemos implementar uma estratégia de “não mapeamento”? Certamente não.
Mesmo que pareça suja, uma estratégia de “sem mapeamento” pode ser perfeitamente válida.
Machine Translated by Google

A estratégia de mapeamento “bidirecional” 85

Considere um caso de uso simples de CRUD. Será que realmente precisamos mapear os mesmos campos do
modelo web para o modelo de domínio e do modelo de domínio para o modelo de persistência? Eu diria que não.

E aquelas anotações JSON ou ORM no modelo de domínio? Eles realmente nos incomodam?
Mesmo que tenhamos que alterar uma ou duas anotações no modelo de domínio se algo mudar na camada de persistência, e daí?

Desde que todas as camadas necessitem exactamente da mesma informação e exactamente na mesma estrutura, uma estratégia de “não

mapeamento” é uma opção perfeitamente válida.

Assim que estivermos lidando com problemas de web ou de persistência na camada de aplicação ou domínio (além das anotações, talvez),

devemos passar para outra estratégia de mapeamento.

Há uma lição para os dois desenvolvedores a partir da introdução: embora tenhamos decidido uma determinada estratégia de mapeamento

no passado, podemos alterá-la mais tarde.

Na minha experiência, muitos casos de uso começam como simples casos de uso CRUD. Mais tarde, eles poderão se transformar em um

caso de uso de negócios completo, com comportamento rico e validações que justifiquem uma estratégia de mapeamento mais cara. Ou

eles podem manter para sempre seu status CRUD; nesse caso, estamos felizes por não termos investido em uma estratégia de mapeamento

diferente.

A estratégia de mapeamento “bidirecional”


Uma estratégia de mapeamento em que cada camada tem seu próprio modelo é o que chamo de estratégia de mapeamento “bidirecional”,

conforme descrito na Figura 9.2.

Figura 9.2 – Com cada adaptador tendo seu próprio modelo, os adaptadores são

responsáveis por mapear seu modelo para o modelo de domínio e vice-versa

Cada camada possui seu próprio modelo, que pode ter uma estrutura completamente diferente do modelo de domínio.

A camada web mapeia o modelo web no modelo de entrada esperado pelas portas de entrada. Ele também mapeia objetos de domínio

retornados pelas portas de entrada de volta ao modelo web.

A camada de persistência é responsável por um mapeamento semelhante entre o modelo de domínio, que é utilizado pelas portas de saída,

e o modelo de persistência.

Ambas as camadas são mapeadas em duas direções, daí o nome mapeamento “bidirecional”.
Machine Translated by Google

86 Mapeamento entre limites

Com cada camada tendo seu próprio modelo, ela pode modificar seu próprio modelo sem afetar as outras camadas (desde que o
conteúdo permaneça inalterado). O modelo web pode ter uma estrutura que permite a apresentação ideal dos dados. O modelo de
domínio pode ter uma estrutura que melhor permita a implementação dos casos de uso. E o modelo de persistência pode ter a
estrutura necessária para um mapeador OR para persistir objetos em um banco de dados.

Essa estratégia de mapeamento também leva a um modelo de domínio limpo que não é contaminado por questões de web ou de
persistência. Não contém anotações de mapeamento JSON ou ORM. O Princípio da Responsabilidade Única foi satisfeito.

Outro bônus do mapeamento “bidirecional” é que, depois da estratégia “Sem mapeamento”, é conceitualmente a estratégia de
mapeamento mais simples. As responsabilidades de mapeamento são claras: as camadas/adaptadores externos são mapeados no
modelo das camadas internas e vice-versa. As camadas internas conhecem apenas seu próprio modelo e podem se concentrar na
lógica do domínio em vez de no mapeamento.

Tal como acontece com qualquer estratégia de mapeamento, o mapeamento “bidirecional” também tem as suas desvantagens.

Em primeiro lugar, geralmente resulta em muitos códigos clichê. Mesmo se usarmos uma das muitas estruturas de mapeamento
existentes para reduzir a quantidade de código, implementar o mapeamento entre modelos geralmente ocupa uma boa parte do
nosso tempo. Isso se deve em parte ao fato de que depurar a lógica de mapeamento é uma tarefa difícil – especialmente quando se
usa uma estrutura de mapeamento que esconde seu funcionamento interno atrás de uma camada de código genérico e reflexão.

Outra desvantagem potencial é que as portas de entrada e saída usam objetos de domínio como parâmetros de entrada e valores
de retorno. Os adaptadores os mapeiam em seu próprio modelo, mas isso ainda cria mais acoplamento entre as camadas do que se
introduzirmos um “modelo de transporte” dedicado, como na estratégia de mapeamento “completo” que discutiremos a seguir.

Tal como a estratégia “Sem Mapeamento”, a estratégia de mapeamento “bidirecional” não é uma solução mágica. Em muitos projetos,
entretanto, esse tipo de mapeamento é considerado uma lei sagrada que devemos cumprir em toda a base de código, mesmo nos
casos de uso mais simples do CRUD. Isso retarda desnecessariamente o desenvolvimento.

Nenhuma estratégia de mapeamento deve ser considerada uma lei férrea. Em vez disso, devemos decidir para cada caso de uso.

A estratégia de mapeamento “completo”

Outra estratégia de mapeamento é o que chamo de estratégia de mapeamento “Completo”, conforme descrito na Figura 9.3.

Figura 9.3 – Com cada operação exigindo seu próprio modelo, o adaptador web e a camada de
aplicação mapeiam cada um seu modelo no modelo esperado pela operação que desejam executar
Machine Translated by Google

A estratégia de mapeamento “unidirecional” 87

Esta estratégia de mapeamento introduz um modelo de entrada e saída separado por operação. Em vez de usar o
modelo de domínio para se comunicar através dos limites da camada, usamos um modelo específico para cada
operação, como SendMoneyCommand, que atua como um modelo de entrada para a porta SendMoneyUseCase
na figura. Podemos chamar esses modelos de “comandos”, “solicitações” ou similares.

A camada web é responsável por mapear sua entrada no objeto de comando da camada de aplicação.
Tal comando torna a interface com a camada de aplicação muito explícita, com pouco espaço para interpretação. Cada caso
de uso possui seu próprio comando com seus próprios campos e validações. Não há nenhuma suposição sobre quais campos
devem ser preenchidos e quais campos seriam melhor deixados vazios, pois, caso contrário, acionariam uma validação que
não queremos para nosso caso de uso atual.

A camada de aplicação é então responsável por mapear o objeto de comando em tudo o que for necessário para modificar o
modelo de domínio de acordo com o caso de uso.

Naturalmente, o mapeamento de uma camada para muitos comandos diferentes requer ainda mais código de mapeamento
do que o mapeamento entre um único modelo web e um modelo de domínio. Este mapeamento, no entanto, é significativamente
mais fácil de implementar e manter do que um mapeamento que tem de lidar com as necessidades de muitos casos de
utilização em vez de apenas um.

Não defendo esta estratégia de mapeamento como um padrão global. Ele apresenta melhor suas vantagens entre a camada
da web (ou qualquer outro adaptador de entrada) e a camada de aplicativo para demarcar claramente os casos de uso de
modificação de estado do aplicativo. Eu não o usaria entre as camadas de aplicação e persistência devido à sobrecarga de
mapeamento.

Normalmente, eu restringiria esse tipo de mapeamento ao modelo de entrada de operações e simplesmente usaria um objeto
de domínio como modelo de saída. SendMoneyUseCase pode então retornar um objeto Account
com o saldo atualizado, por exemplo.

Isto mostra que as estratégias de mapeamento podem e devem ser misturadas. Nenhuma estratégia de mapeamento precisa
ser uma regra global em todas as camadas.

A estratégia de mapeamento “unidirecional”


Existe ainda outra estratégia de mapeamento com outro conjunto de prós e contras: a estratégia “Unidirecional” visualizada na
Figura 9.4.

Figura 9.4 – Com o modelo de domínio e os modelos de adaptador implementando a mesma interface de “estado”, cada

camada só precisa mapear os objetos que recebe de outras camadas de uma maneira
Machine Translated by Google

88 Mapeamento entre limites

Nesta estratégia, os modelos em todas as camadas implementam a mesma interface, que encapsula o
estado do modelo de domínio, fornecendo métodos getter nos atributos relevantes.

O próprio modelo de domínio pode implementar um comportamento rico, que podemos acessar a partir de nossos
serviços na camada de aplicação. Se quisermos passar um objeto de domínio para as camadas externas, podemos
fazê-lo sem mapeamento, pois o objeto de domínio implementa a interface de estado esperada pelas portas de entrada e saída.

As camadas externas podem então decidir se podem trabalhar com a interface ou se precisam mapeá -la em seu próprio
modelo. Eles não podem modificar inadvertidamente o estado do objeto de domínio, pois o comportamento de modificação não
é exposto pela interface de estado.

Os objetos que passamos de uma camada externa para a camada de aplicação também implementam essa interface de
estado. A camada de aplicação precisa então mapeá-lo no modelo de domínio real para obter acesso ao seu comportamento.
Esse mapeamento funciona bem com o conceito de fábrica do Domain-Driven Design . Uma fábrica em termos de
DDD é responsável por reconstituir um objeto de domínio a partir de um determinado estado, que é exatamente o que
estamos fazendo.1

A responsabilidade do mapeamento é clara: se uma camada recebe um objeto de outra camada, nós o mapeamos em algo
com o qual a camada possa trabalhar. Assim, cada camada mapeia apenas uma direção, tornando esta uma estratégia de
mapeamento “unidirecional” .

Com o mapeamento distribuído entre camadas, contudo, esta estratégia é conceitualmente mais difícil do que as outras
estratégias.

Essa estratégia funciona melhor se os modelos nas camadas forem semelhantes. Para operações somente leitura , por
exemplo, a camada da web pode não precisar mapear seu próprio modelo, uma vez que a interface de estado fornece todas
as informações necessárias.

Quando usar qual estratégia de mapeamento?


Esta é a pergunta de um milhão de dólares, não é?

A resposta é a de sempre, insatisfatória depende.

Como cada estratégia de mapeamento tem vantagens e desvantagens diferentes, devemos resistir ao impulso de definir uma
estratégia única como uma regra global rígida e rápida para toda a base de código. Isso vai contra nossos instintos, pois parece
confuso misturar padrões na mesma base de código. Mas escolher conscientemente um padrão que não é o melhor para um
determinado trabalho, apenas para servir ao nosso senso de organização, é irresponsável, pura e simplesmente.

1 Fábrica: Domain Driven Design por Eric Evans, Addison-Wesley, 2004, p. 158.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 89

Além disso, à medida que o software evolui ao longo do tempo, a estratégia que era a melhor para o trabalho ontem pode
não ser ainda a melhor para o trabalho hoje. Em vez de começar com uma estratégia de mapeamento fixa e mantê-la ao
longo do tempo – não importa o que aconteça – podemos começar com uma estratégia simples que nos permita evoluir
rapidamente o código e depois passar para uma mais complexa que nos ajude a desacoplar melhor as camadas.

Para decidir qual estratégia usar e quando, precisamos chegar a um acordo sobre um conjunto de diretrizes dentro da equipe. Estas
directrizes deverão responder à questão de saber qual a estratégia de mapeamento que deverá ser a primeira escolha em cada
situação. Eles também devem responder por que são a primeira escolha, para que possamos avaliar se esses motivos ainda se
aplicam após algum tempo.

Poderíamos, por exemplo, definir diretrizes de mapeamento diferentes para modificar casos de uso e para consultas . Além disso,
podemos querer usar diferentes estratégias de mapeamento entre as camadas da web e de aplicação e entre as camadas de
aplicação e de persistência.

As diretrizes para essas situações podem ser assim:

• Se estivermos trabalhando em um caso de uso modificador, a estratégia de mapeamento “Completo” é a primeira escolha
entre a camada web e a camada de aplicação, para dissociar os casos de uso um do outro. Isso nos dá regras claras de
validação por caso de uso e não precisamos lidar com campos desnecessários em um
determinado caso de uso.

• Se estivermos trabalhando em um caso de uso modificador, a estratégia “No Mapping” é a primeira escolha entre a aplicação
e a camada de persistência para poder evoluir rapidamente o código sem sobrecarga de mapeamento. Contudo, assim que
tivermos que lidar com problemas de persistência na camada de aplicação, passaremos para uma estratégia de
mapeamento “bidirecional” para manter os problemas de persistência na camada de persistência.

• Se estivermos trabalhando em uma consulta, a estratégia “No Mapping” é a primeira escolha entre a camada web e de
aplicação e entre a camada de aplicação e persistência para poder evoluir rapidamente o código sem sobrecarga de
mapeamento. No entanto, assim que tivermos que lidar com problemas de web ou de persistência na camada de aplicação,
passaremos para uma estratégia de mapeamento “bidirecional” entre a web e a camada de aplicação ou a camada de
aplicação e a camada de persistência, respectivamente.

Para aplicar com sucesso diretrizes como essas, elas devem estar presentes na mente dos desenvolvedores.
Portanto, as diretrizes devem ser discutidas e revisadas continuamente como um esforço de equipe.

Como isso me ajuda a construir software sustentável?


As portas de entrada e saída atuam como guardiões entre as camadas da nossa aplicação. Eles definem como as camadas se
comunicam entre si e como mapeamos modelos entre camadas.

Com portas estreitas implementadas para cada caso de uso, podemos escolher diferentes estratégias de mapeamento para
diferentes casos de uso e até mesmo evoluí-las ao longo do tempo sem afetar outros casos de uso, selecionando assim a melhor
estratégia para uma determinada situação em um determinado momento.
Machine Translated by Google

90 Mapeamento entre limites

Selecionar uma estratégia de mapeamento diferente para cada caso de uso é mais difícil e requer mais comunicação do
que simplesmente usar a mesma estratégia de mapeamento para todas as situações, mas recompensará a equipe com
uma base de código que faz exatamente o que precisa e é mais fácil de manter. desde que as diretrizes de mapeamento
sejam conhecidas.

Agora que sabemos quais componentes compõem nosso aplicativo e como eles se comunicam, podemos explorar como
montar um aplicativo funcional a partir dos diferentes componentes.
Machine Translated by Google

10
Montando o Aplicativo
Agora que implementamos alguns casos de uso, web adaptors e adaptadores de persistência, precisamos
montá-los em um aplicativo funcional. Conforme discutido no Capítulo 4, Organizando o código, contamos com
um mecanismo de injeção de dependência para instanciar nossas classes e conectá-las no momento da inicialização.
Neste capítulo, discutiremos algumas abordagens para fazer isso com Java simples e as estruturas Spring e Spring Boot.

Por que se preocupar com a montagem?


Por que não estamos apenas instanciando os casos de uso e os adaptadores quando e onde precisamos deles? Porque
queremos manter as dependências do código apontadas na direção certa. Lembre-se: todas as dependências devem
apontar para dentro, em direção ao código de domínio da nossa aplicação, para que o código de domínio não precise
mudar quando algo nas camadas externas mudar.

Se um caso de uso precisar chamar um adaptador de persistência e apenas instanciá-lo, criamos uma dependência de
código na direção errada.

É por isso que criamos interfaces de porta de saída. O caso de uso conhece apenas a interface e recebe uma
implementação dessa interface em tempo de execução.

Um bom efeito colateral desse estilo de programação é que o código que estamos criando é muito mais fácil de testar.
Se pudermos passar todos os objetos que uma classe precisa para seu construtor, podemos optar por passar mocks em
vez dos objetos reais, o que facilita a criação de um teste de unidade isolado para a classe.

Então, quem é responsável por criar nossas instâncias de objetos? E como fazemos isso sem violar a Regra da
Dependência?

A resposta é que deve haver um componente de configuração que seja neutro à nossa arquitetura e que tenha
dependência de todas as classes para instanciá-las, conforme mostrado na Figura 10.1.
Machine Translated by Google

92 Montando o Aplicativo

Figura 10.1 – Um componente de configuração neutro pode acessar todas as classes para instanciá-las

Na Arquitetura Limpa apresentada no Capítulo 3, Invertendo Dependências, esse componente de configuração estaria
no círculo mais externo, que pode acessar todas as camadas internas, conforme definido pela Regra de Dependência.

O componente de configuração é responsável por montar um aplicativo funcional a partir das peças que fornecemos. Deve fazer o seguinte:

• Criar instâncias de adaptadores da web.

• Certifique-se de que as solicitações HTTP sejam realmente roteadas para os web adaptors.

• Criar instâncias de casos de uso.

• Fornecer instâncias de casos de uso aos adaptadores da Web.

• Crie instâncias de adaptadores de persistência.

• Fornecer casos de uso com instâncias de adaptadores de persistência.

• Certifique-se de que os adaptadores de persistência possam realmente acessar o banco de dados.

Além disso, o componente de configuração deve ser capaz de acessar determinadas fontes de parâmetros de configuração, como arquivos
de configuração ou parâmetros de linha de comando. Durante a montagem do aplicativo, o componente de configuração passa esses
parâmetros aos componentes do aplicativo para controlar o comportamento, como qual banco de dados acessar ou qual servidor usar para
enviar e-mails.
Machine Translated by Google

Montagem via código simples 93

São muitas responsabilidades (leia-se: motivos para mudar). Não estamos violando o Princípio da
Responsabilidade Única aqui? Sim, estamos, mas se quisermos manter o resto da aplicação limpo,
precisamos de um componente externo que cuide da fiação. E esse componente precisa conhecer todas
as partes móveis para montá-las em uma aplicação funcional.

Montagem via código simples


Existem diversas maneiras de implementar um componente de configuração responsável pela montagem da
aplicação. Se estivermos construindo um aplicativo sem o suporte de uma estrutura de injeção de dependência,
podemos criar esse componente com código simples:

Este trecho de código é um exemplo simplificado de como esse componente de configuração pode parecer. Em
Java, um aplicativo é iniciado a partir do método principal . Dentro deste método, instanciamos todas as classes
que precisamos , desde o controlador web até o adaptador de persistência, e as conectamos.

Por fim, chamamos o método místico startProcessingWebRequests(), que expõe o controlador web
via HTTP.1O aplicativo está então pronto para processar solicitações.

1 O método startProcessingWebRequests() é apenas um espaço reservado para qualquer lógica de inicialização


necessária para expor nossos adaptadores web via HTTP. Nós realmente não queremos implementar isso
nós mesmos. Em uma aplicação do mundo real, um framework faz isso por nós.
Machine Translated by Google

94 Montando o Aplicativo

Essa abordagem de código simples é a maneira mais básica de montar um aplicativo. Tem algumas
desvantagens, no entanto:

• Primeiro de tudo, o código anterior é para um aplicativo que possui apenas um único controlador web,
caso de uso e adaptador de persistência. Imagine quanto código como esse teríamos que produzir
para inicializar um aplicativo corporativo completo!

• Segundo, como estamos instanciando todas as classes fora de seus pacotes, todas essas classes
precisam ser públicas. Isso significa, por exemplo, que o compilador Java não impede que um caso
de uso acesse diretamente um adaptador de persistência, já que ele é público. Seria bom se
pudéssemos evitar dependências indesejadas como essa usando a visibilidade privada do pacote.

Felizmente, existem estruturas de injeção de dependência que podem fazer o trabalho sujo para nós e, ao
mesmo tempo, manter dependências privadas de pacotes. O framework Spring é atualmente o mais popular
no mundo Java. Spring também fornece suporte para web e banco de dados, entre muitas outras coisas,
então, afinal, não precisamos implementar o método místico startProcessingWebRequests() .

Montando via varredura de classpath do Spring


Se usarmos o framework Spring para montar nosso aplicativo, o resultado será chamado de contexto do aplicativo.
O contexto do aplicativo contém todos os objetos que juntos constituem o aplicativo (beans na linguagem Java).

Spring oferece diversas abordagens para montar um contexto de aplicação, cada uma com suas próprias vantagens
e desvantagens. Vamos começar discutindo a abordagem mais popular (e mais conveniente): varredura de classpath.

Com a varredura do caminho de classe, o Spring percorre todas as classes que estão disponíveis em uma
determinada fatia do caminho de classe e procura por classes anotadas com a anotação @Component . A
estrutura então cria um objeto de cada uma dessas classes. As classes devem ter um construtor que receba
todos os campos obrigatórios como argumento, como nosso AccountPersistenceAdapter do Capítulo 7,
Implementando um adaptador de persistência:
Machine Translated by Google

Montando via varredura de classpath do Spring 95

Nesse caso, nem mesmo escrevemos o construtor, mas deixamos a biblioteca Lombok fazer isso
por nós usando a anotação @RequiredArgsConstructor , que cria um construtor que aceita todos
os campos finais como argumentos.

O Spring encontrará esse construtor e procurará por classes anotadas em @Component dos tipos de argumentos
necessários e instanciará-os de maneira semelhante para adicioná-los ao contexto do aplicativo. Assim que todos
os objetos necessários estiverem disponíveis, ele finalmente chamará o construtor de AccountPersistenceAdapter
e adicione o objeto resultante também ao contexto do aplicativo.

A varredura de classpath é uma maneira muito conveniente de montar um aplicativo. Só precisamos


espalhar algumas anotações @Component na base de código e fornecer os construtores corretos.

Também podemos criar nossa própria anotação de estereótipo para o Spring usar. Poderíamos, por
exemplo, criar uma anotação @PersistenceAdapter :
Machine Translated by Google

96 Montando o Aplicativo

Esta anotação é meta-anotada com @Component para informar ao Spring que ela deve ser coletada durante
a varredura do caminho de classe. Agora poderíamos usar @PersistenceAdapter em vez de @Component
para marcar nossas classes de adaptadores de persistência como partes de nosso aplicativo. Com esta
anotação, tornamos nossa arquitetura mais evidente para quem lê o código.

A abordagem de varredura de caminho de classe tem suas desvantagens, entretanto. Primeiro, é invasivo porque exige que
adicionemos uma anotação específica da estrutura às nossas classes. Se você é um defensor da Arquitetura Limpa, você diria
que isso é proibido, pois vincula nosso código a uma estrutura específica.

Eu diria que no desenvolvimento normal de aplicativos, uma única anotação em uma classe não é grande coisa e pode ser
facilmente refatorada, se necessário.

Em outros contextos, no entanto, como ao construir uma biblioteca ou uma estrutura para outros desenvolvedores usarem ,
isso pode ser impossível, pois não queremos sobrecarregar nossos usuários com uma dependência da estrutura Spring.

Outra desvantagem potencial da abordagem de varredura de caminho de classe é que coisas mágicas podem acontecer.
E por magia, quero dizer o tipo ruim de magia que causa efeitos inexplicáveis que podem levar dias para serem descobertos
se você não for um especialista em Spring.

A mágica acontece porque a varredura do caminho de classe é uma arma muito contundente para usar na montagem de
aplicativos. Simplesmente apontamos o Spring para o pacote pai do nosso aplicativo e dizemos para ele procurar por
classes anotadas @Component dentro deste pacote.

Você conhece de cor todas as classes que existem em seu aplicativo? Provavelmente não. É provável que existam algumas
classes que não queremos ter no contexto da aplicação. Talvez essa classe até manipule o contexto do aplicativo de maneiras
maliciosas, causando erros difíceis de rastrear.

Vejamos uma abordagem alternativa que nos dá um pouco mais de controle.

Montando via Java Config do Spring


Embora a varredura do caminho de classe seja o bastão da montagem do aplicativo, o Java Config do Spring é o
bisturi.2 Essa abordagem é semelhante à abordagem de código simples apresentada anteriormente neste capítulo,
mas é menos confusa e nos fornece uma estrutura para que não tenhamos codificar tudo manualmente.

Nesta abordagem, criamos classes de configuração, cada uma responsável por construir um conjunto de beans que serão
adicionados ao contexto da aplicação.

Por exemplo, poderíamos criar uma classe de configuração responsável por instanciar todos os nossos adaptadores de
persistência:

2 Porrete versus bisturi: se você não passa muitas horas de sua vida matando monstros em videogames como eu e não sabe
o que é um porrete, um porrete é um bastão com uma ponta pesada que pode ser usado como uma arma. É uma arma
muito contundente que pode causar muitos danos sem precisar mirar muito bem.
Machine Translated by Google

Montando via Java Config do Spring 97

A anotação @Configuration marca esta classe como uma classe de configuração a ser obtida pela varredura do
caminho de classe do Spring. Portanto, neste caso, ainda estamos usando a varredura de caminho de classe, mas
apenas selecionamos nossas classes de configuração em vez de cada bean, o que reduz a chance de acontecer magia maligna.

Os próprios beans são criados dentro dos métodos de fábrica anotados em @Bean de nossas classes de
configuração. No caso anterior, adicionamos um adaptador de persistência ao contexto do aplicativo. Ele precisa de
dois repositórios e um mapeador como entrada para seu construtor. O Spring fornece automaticamente esses
objetos como entrada para os métodos de fábrica.

Mas de onde o Spring obtém os objetos do repositório? Se eles forem criados manualmente em um método
de fábrica de outra classe de configuração, o Spring os fornecerá automaticamente como parâmetros para
os métodos de fábrica do exemplo de código anterior. Neste caso, porém, eles são criados pelo próprio
Spring, acionados pela anotação @EnableJpaRepositories . Se o Spring Boot encontrar esta anotação, ele
fornecerá automaticamente implementações para todas as interfaces de repositório Spring Data que definimos.

Se você conhece o Spring Boot, deve saber que poderíamos ter adicionado o @EnableJpa
Anotação de repositórios para a classe principal do aplicativo em vez de nossa classe de configuração personalizada.
Sim, isso é possível, mas ativará os repositórios JPA toda vez que o aplicativo for inicializado, mesmo se
iniciarmos o aplicativo dentro de um teste que na verdade não precisa de persistência. Portanto, ao mover
essas “ anotações de recursos” para um “módulo” de configuração separado, nos tornamos muito mais
flexíveis e podemos iniciar partes de nossa aplicação em vez de sempre ter que iniciar tudo.

Com a classe PersistenceAdapterConfiguration , criamos um módulo de persistência com escopo


restrito que instancia todos os objetos necessários em nossa camada de persistência. Será automaticamente
Machine Translated by Google

98 Montando o Aplicativo

captados pela varredura do caminho de classe do Spring enquanto ainda temos controle total sobre quais beans são
realmente adicionados ao contexto do aplicativo.

Da mesma forma, poderíamos criar classes de configuração para web adaptors ou para determinados módulos em
nossa camada de aplicação. Agora podemos criar um contexto de aplicação que contém determinados módulos, mas
simula os beans de outros módulos, o que nos dá grande flexibilidade nos testes. Poderíamos até mesmo enviar o
código de cada um desses módulos para sua própria base de código, pacote ou arquivo JAR sem muita refatoração.

Além disso, essa abordagem não nos força a espalhar anotações @Component por toda a nossa base de código,
como faz a abordagem de varredura de caminho de classe. Assim, podemos manter nossa camada de aplicação limpa
sem qualquer dependência do framework Spring (ou de qualquer outro framework, nesse caso).

Há um problema com esta solução, no entanto. Se a classe de configuração não estiver no mesmo pacote que as
classes dos beans que ela cria (neste caso, as classes do adaptador de persistência), essas classes deverão ser
públicas. Para restringir a visibilidade, podemos usar pacotes como limites de módulo e criar uma classe de
configuração dedicada dentro de cada pacote. Dessa forma, não podemos usar subpacotes, como será discutido no
Capítulo 12, Impondo limites de arquitetura.

Como isso me ajuda a construir software sustentável?


Spring e Spring Boot (e estruturas semelhantes) fornecem muitos recursos que tornam nossa vida mais fácil.
Um dos principais recursos é montar o aplicativo a partir das partes (classes) que nós, como desenvolvedores de
aplicativos, fornecemos.

A varredura de classpath é um recurso muito conveniente. Só precisamos apontar o Spring para um pacote e ele
monta uma aplicação a partir das classes que encontra. Isso permite um desenvolvimento rápido, sem precisarmos
pensar na aplicação como um todo.

No entanto, quando a base de código cresce, isso leva rapidamente à falta de transparência. Não sabemos exatamente
quais beans são carregados no contexto do aplicativo. Além disso, não podemos iniciar facilmente partes isoladas do
contexto da aplicação para usar em testes.

Ao criar um componente de configuração dedicado responsável pela montagem de nossa aplicação, podemos liberar
o código da nossa aplicação dessa responsabilidade (leia: “motivo da mudança” – lembra do “S” em “SÓLIDO”?).
Somos recompensados com módulos altamente coesos que podemos iniciar isoladamente uns dos outros e que
podemos mover facilmente dentro de nossa base de código. Como sempre, isso custa algum tempo extra para manter
esse componente de configuração.

Falamos muito sobre diferentes opções de como fazer as coisas “da maneira certa” neste e nos capítulos anteriores.
No entanto, às vezes “o caminho certo” não é viável. No próximo capítulo falaremos sobre atalhos, o preço que
pagamos por eles e quando vale a pena adotá-los.
Machine Translated by Google

11

Tomando atalhos conscientemente


No prefácio deste livro, amaldiçoei o fato de nos sentirmos forçados a tomar atalhos o tempo todo, acumulando
uma grande quantidade de dívidas técnicas que nunca teremos a chance de pagar.

Para evitar atalhos, devemos ser capazes de identificá-los. Assim, o objetivo deste capítulo é aumentar a
consciencialização sobre alguns atalhos potenciais e discutir os seus efeitos.

Com essas informações, podemos identificar e corrigir atalhos acidentais. Ou, se justificado, podemos até optar
conscientemente pelos efeitos de um atalho.1

Por que os atalhos são como janelas quebradas


Em 1969, o psicólogo Philip Zimbardo conduziu um experimento para testar uma teoria que mais tarde ficou
conhecida como Teoria das Janelas Quebradas. 2

Sua equipe estacionou um carro sem placa em um bairro do Bronx e outro em um bairro supostamente “melhor” de
Palo Alto. Então, eles esperaram.

O carro no Bronx foi retirado de peças valiosas em 24 horas e então os transeuntes começaram a destruí-lo
aleatoriamente.

O carro em Palo Alto não foi tocado durante uma semana, então a equipe de Zimbardo quebrou uma janela. A partir de
então, o carro teve destino semelhante ao do Bronx e foi destruído no mesmo curto espaço de tempo por pessoas que
passavam.

As pessoas que participaram no saque e na destruição dos carros vinham de todas as classes sociais e incluíam
pessoas que, de outra forma, eram cidadãos cumpridores da lei e bem comportados.

1 Imagine falar sobre atalhos em um livro sobre engenharia de construção ou, o que é ainda mais assustador, em um
livro sobre aviônica! A maioria de nós, entretanto, não está construindo o software equivalente a um arranha-céu ou
um avião. E o software é flexível e pode ser alterado mais facilmente do que o hardware, então às vezes é mais
econômico (conscientemente!) pegar um atalho primeiro e corrigi-lo mais tarde (ou nunca).
2 A Teoria das Janelas Quebradas: https://www.theatlantic.com/magazine/
arquivo/1982/03/janelas quebradas/304465/.
Machine Translated by Google

100 Tomando atalhos conscientemente

Esse comportamento humano ficou conhecido como Teoria das Janelas Quebradas. Em minhas próprias palavras:

Assim que algo parece degradado, danificado, [insira o adjetivo negativo aqui], ou geralmente mal cuidado, o
cérebro humano sente que não há problema em torná-lo mais degradado, danificado, ou [insira o adjetivo negativo aqui].

Esta teoria se aplica a muitas áreas da vida:

• Em uma vizinhança onde o vandalismo é comum, o limite para saquear ou danificar um local abandonado
carro está baixo.

• Quando um carro tem uma janela quebrada, o limite para danificá-lo ainda mais é baixo, mesmo em uma vizinhança “boa”.

• Em um quarto desarrumado, a soleira para jogar nossas roupas no chão em vez de colocá-las
colocá-los no guarda-roupa é baixo.

• Numa sala de aula onde os alunos frequentemente interrompem a aula, o limiar para contar outra piada para
colegas de classe é baixo.

Aplicado ao trabalho com código, isso significa o seguinte:

• Ao trabalhar em uma base de código de baixa qualidade, o limite para adicionar mais código de baixa qualidade é baixo.

• Ao trabalhar em uma base de código com muitas violações de codificação, o limite para adicionar outra

a violação de codificação é baixa.

• Ao trabalhar em uma base de código com muitos atalhos, o limite para adicionar outro atalho é baixo.

Com tudo isso em mente, é realmente uma surpresa que a qualidade de muitas das chamadas bases de código “legadas” tenha diminuído tanto

ao longo do tempo?

A responsabilidade de começar limpo


Embora trabalhar com código não seja realmente como saquear um carro, todos nós estamos inconscientemente sujeitos à psicologia das

Janelas Quebradas. Isso torna importante iniciar um projeto limpo, com o mínimo de atalhos e o mínimo de dívida técnica possível. Isso ocorre

porque, assim que um atalho aparece, ele funciona como uma janela quebrada e atrai mais atalhos.

Como um projeto de software geralmente é um empreendimento muito caro e demorado, manter as janelas quebradas sob controle é uma grande

responsabilidade para nós, desenvolvedores de software. Podemos nem ser nós que terminamos o projeto e outros terão que assumir. Para eles,

é uma base de código herdada com a qual ainda não têm conexão , diminuindo ainda mais o limite para a criação de janelas quebradas.

Há momentos, no entanto, em que decidimos que um atalho é a coisa mais pragmática a fazer, seja porque a parte do código em que estamos

trabalhando não é tão importante para o projeto como um todo, porque estamos prototipando ou por razões económicas.
Machine Translated by Google

Compartilhando modelos entre casos de uso 101

Devemos ter muito cuidado ao documentar esses atalhos adicionados conscientemente, por exemplo, na forma
de Registros de Decisão de Arquitetura (ADRs), conforme proposto por Michael Nygard em seu blog.3
Devemos isso ao nosso futuro e aos nossos sucessores. Se todos os membros da equipe estiverem cientes
desta documentação, isso reduzirá até mesmo o efeito Janelas Quebradas porque a equipe saberá que os atalhos foram
tomada conscientemente e por uma boa razão.

Cada seção a seguir discute um padrão que pode ser considerado um atalho no estilo de Arquitetura Hexagonal apresentado
neste livro. Veremos os efeitos dos atalhos e os argumentos que falam a favor e contra a sua adoção.

Compartilhando modelos entre casos de uso


No Capítulo 5, Implementando um Caso de Uso, argumentei que diferentes casos de uso deveriam ter diferentes
modelos de entrada e saída, o que significa que os tipos de parâmetros de entrada e os tipos de valores de retorno
deveriam ser diferentes.

A Figura 11.1 mostra um exemplo onde dois casos de uso compartilham o mesmo modelo de entrada:

Figura 11.1 – Compartilhar o modelo de entrada ou saída entre casos de uso

leva ao acoplamento entre os casos de uso

3 Registros de decisões de arquitetura: http://thinkrelevance.com/blog/2011/11/15/


documentar decisões de arquitetura.
Machine Translated by Google

102 Tomando atalhos conscientemente

O efeito do compartilhamento neste caso é que SendMoneyUseCase e RevokeActivityUseCase são acoplados


entre si. Se mudarmos algo na classe SendMoneyCommand compartilhada , ambos os casos de uso serão
afetados. Eles compartilham uma razão para mudar em termos do Princípio da Responsabilidade Única (que
deveria ser chamado de “Princípio da Razão Única para Mudar”, conforme discutido no Capítulo 3, Invertendo
Dependências). O mesmo acontece se ambos os casos de uso compartilharem o mesmo modelo de saída.

O compartilhamento de modelos de entrada e saída entre casos de uso é válido se os casos de uso estiverem funcionalmente acoplados,
ou seja, se compartilharem um determinado requisito. Nesse caso, na verdade queremos que ambos os casos de uso sejam afetados
se alterarmos um determinado detalhe.

Entretanto, se ambos os casos de uso puderem evoluir separadamente um do outro, isso será um atalho. Nesse
caso, devemos separar os casos de uso desde o início, mesmo que isso signifique duplicar as classes de
entrada e saída se elas parecerem iguais no início.

Portanto, ao construir vários casos de uso em torno de um conceito semelhante, vale a pena perguntar regularmente ao
questão de saber se os casos de uso devem evoluir separadamente uns dos outros. Assim que a resposta for
“sim”, é hora de separar os modelos de entrada e saída.

Usando entidades de domínio como modelo de entrada ou saída


Se tivermos uma entidade de domínio Account e uma porta de entrada, SendMoneyUseCase, poderemos ficar
tentados a usar a entidade como modelo de entrada e/ou saída da porta de entrada, como mostra a Figura 11.2.

Figura 11.2 – Usar uma entidade de domínio como modelo de entrada ou saída

de um caso de uso acopla a entidade de domínio ao caso de uso

A porta de entrada depende da entidade do domínio. A consequência disso é que adicionamos outro motivo
para a alteração da entidade Conta .

Espere, a entidade Account não depende da porta de entrada SendMoneyUseCase (é o


contrário), então como a porta de entrada pode ser um motivo para mudança para a entidade?
Machine Translated by Google

Ignorando portas de entrada 103

Digamos que precisamos de algumas informações sobre uma conta no caso de uso que não está atualmente
disponível na entidade Conta . Em última análise, essas informações não devem ser armazenadas na entidade
Conta , mas em um domínio diferente ou contexto limitado. Mesmo assim , ficamos tentados a adicionar um novo
campo à entidade Conta , porque ele já está disponível na interface do caso de uso.

Para casos de uso simples de criação ou atualização, uma entidade de domínio na interface do caso de uso pode ser adequada,
pois a entidade contém exatamente as informações necessárias para persistir seu estado no banco de dados.

Assim que um caso de uso não se limitar a atualizar alguns campos no banco de dados, mas implementar uma lógica de domínio
mais complexa (potencialmente delegando parte da lógica de domínio a uma entidade de domínio rica), devemos usar um modelo
de entrada e saída dedicado. para a interface do caso de uso, porque não queremos que as alterações no caso de uso se
propaguem para a entidade do domínio.

O que torna esse atalho perigoso é o fato de que muitos casos de uso começam suas vidas como um simples caso de uso de
criação ou atualização, apenas para se tornarem feras de lógica de domínio complexa ao longo do tempo. Isto é especialmente
verdadeiro em um ambiente ágil, onde começamos com um produto mínimo viável e adicionamos complexidade à medida que
avançamos . Portanto, se usamos uma entidade de domínio como modelo de entrada no início, devemos encontrar o momento
para substituí-la por um modelo de entrada dedicado que seja independente da entidade de domínio.

Ignorando portas de entrada


Embora as portas de saída sejam necessárias para inverter a dependência entre a camada de aplicação e os adaptadores de saída
(para fazer com que as dependências apontem para dentro), não precisamos das portas de entrada para inversão de dependência.
Poderíamos decidir permitir que os adaptadores de entrada acessem nossos serviços de aplicação ou domínio diretamente, sem
portas de entrada intermediárias, como mostra a Figura 11.3.

Figura 11.3 – Sem portas de entrada, perdemos pontos de entrada claramente marcados para a lógica do domínio

Ao remover as portas de entrada, reduzimos uma camada de abstração entre os adaptadores de entrada e a camada de aplicação.
Remover camadas de abstração geralmente é muito bom.
Machine Translated by Google

104 Tomando atalhos conscientemente

As portas de entrada, entretanto, definem os pontos de entrada no núcleo do nosso aplicativo. Depois de removê
-los, devemos saber mais sobre o funcionamento interno de nossa aplicação para descobrir qual método de serviço
podemos chamar para implementar um determinado caso de uso. Ao manter portas de entrada dedicadas, podemos
identificar rapidamente os pontos de entrada da aplicação. Isso torna especialmente fácil para novos desenvolvedores
se orientarem na base de código.

Outra razão para manter as portas de entrada é que elas nos permitem aplicar facilmente a arquitetura. Com as opções de
imposição que aprenderemos no Capítulo 12, Impondo limites de arquitetura, podemos garantir que os adaptadores de entrada
chamem apenas portas de entrada e não serviços de aplicação. Isso faz com que cada ponto de entrada na camada de aplicação
seja uma decisão muito consciente. Não podemos mais chamar acidentalmente um método de serviço que não deveria ser
chamado de um adaptador de entrada.

Se um aplicativo for pequeno o suficiente ou tiver apenas um único adaptador de entrada e pudermos compreender todo o fluxo
de controle sem a ajuda das portas de entrada, talvez seja melhor ficar sem portas de entrada. No entanto, com que frequência
podemos dizer que sabemos que um aplicativo permanecerá pequeno ou terá apenas um único adaptador de entrada durante
toda a sua vida útil?

Ignorando serviços
Além das portas de entrada, para certos casos de uso, podemos querer ignorar a camada de serviço como um todo, como mostra
a Figura 11.4.

Figura 11.4 – Sem serviços, não temos mais uma representação de um caso de uso em nossa base de código

Aqui, a classe AccountPersistenceAdapter em um adaptador de saída implementa diretamente uma


porta de entrada e substitui o serviço que normalmente implementa uma porta de entrada.

É muito tentador fazer isso para casos de uso simples de CRUD, pois nesse caso um serviço geralmente encaminha apenas uma
solicitação de criação, atualização ou exclusão para o adaptador de persistência, sem adicionar nenhuma lógica de domínio. Em
vez de encaminhar, podemos deixar o adaptador de persistência implementar o caso de uso diretamente.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 105

Isso, no entanto, requer um modelo compartilhado entre o adaptador de entrada e o adaptador de saída, que é a
entidade de domínio Account neste caso, portanto geralmente significa que estamos usando o modelo de domínio
como modelo de entrada, conforme descrito anteriormente.

Além disso, não temos mais uma representação do caso de uso no núcleo da nossa aplicação. Se um caso de uso
CRUD crescer para algo mais complexo ao longo do tempo, é tentador adicionar lógica de domínio diretamente ao
adaptador de saída, uma vez que o caso de uso já foi implementado lá. Isso descentraliza a lógica do domínio,
dificultando sua localização e manutenção.

No final, para evitar serviços de passagem padronizados, podemos optar por ignorar os serviços para casos de uso
simples de CRUD, afinal. Então, entretanto, a equipe deve desenvolver diretrizes claras para introduzir um serviço
assim que se espera que o caso de uso faça mais do que apenas criar, atualizar ou excluir uma entidade.

Como isso me ajuda a construir software sustentável?


Há momentos em que os atalhos fazem sentido do ponto de vista económico. Este capítulo forneceu alguns
insights sobre as consequências que alguns atalhos podem ter para ajudar a decidir se devem ser seguidos ou não.

A discussão mostra que é tentador introduzir atalhos para casos de uso simples de CRUD, já que,
para eles, implementar toda a arquitetura parece um exagero (e os atalhos não parecem atalhos).
Entretanto, como todos os aplicativos começam pequenos, é muito importante que a equipe chegue a um acordo
sobre quando um caso de uso sai do estado CRUD. Só então a equipe poderá substituir os atalhos por uma
arquitetura que seja mais sustentável no longo prazo.

Alguns casos de uso nunca sairão do estado CRUD. Para eles, pode ser mais pragmático manter os atalhos para
sempre, já que eles não implicam realmente uma sobrecarga de manutenção.

Em qualquer caso, devemos documentar a arquitetura e as decisões pelas quais escolhemos um determinado
atalho para que nós (ou os nossos sucessores) possamos reavaliar as decisões no futuro.

Embora às vezes os atalhos possam ser aceitáveis, queremos tomar a decisão de tomar um atalho conscientemente.
Isso significa que devemos definir uma forma “correta” de fazer as coisas e aplicá-la, para que possamos desviar-
nos dessa forma se houver boas razões para o fazer. No próximo capítulo, veremos algumas maneiras de reforçar
nossa arquitetura.
Machine Translated by Google
Machine Translated by Google

12
Aplicando Arquitetura
Limites

Falamos muito sobre arquitetura nos capítulos anteriores e é bom ter uma arquitetura alvo para nos guiar em nossas decisões
sobre como criar código e onde colocá-lo.

Em todos os projetos de software acima do tamanho do jogo, entretanto, a arquitetura tende a se desgastar com o tempo. Os
limites entre as camadas enfraquecem, o código fica mais difícil de testar e geralmente precisamos de cada vez mais tempo
para implementar novos recursos.

Neste capítulo, discutiremos algumas medidas que podemos tomar para impor os limites dentro da nossa arquitetura e, assim,
combater a erosão arquitetônica.

Limites e dependências
Antes de falarmos sobre diferentes maneiras de impor limites de arquitetura, vamos discutir onde estão os limites dentro de
nossa arquitetura e o que realmente significa impor limites .
Machine Translated by Google

108 Aplicando Limites de Arquitetura

Figura 12.1 – Aplicar limites de arquitetura significa garantir que as dependências apontem na direção
certa (setas tracejadas marcam dependências que não são permitidas de acordo com nossa arquitetura)

A Figura 12.1 mostra como os elementos de nossa Arquitetura Hexagonal podem ser distribuídos em quatro camadas,
assemelhando-se à abordagem genérica de Arquitetura Limpa introduzida no Capítulo 3, Invertendo Dependências.

A camada mais interna contém entidades de domínio e serviços de domínio. A camada de aplicação ao seu redor pode
acessar essas entidades e serviços para implementar um caso de uso, geralmente por meio de um serviço de aplicação.
Os adaptadores acessam esses serviços por meio de portas de entrada ou são acessados por esses serviços por
meio de portas de saída. Finalmente, a camada de configuração contém fábricas que criam objetos adaptadores e
de serviço e os fornecem a um mecanismo de injeção de dependência.
Machine Translated by Google

Modificadores de visibilidade 109

Na figura anterior, nossos limites de arquitetura ficam bem claros. Existe um limite entre cada camada e
seu próximo vizinho interno e externo. De acordo com a Regra de Dependência, as dependências que
cruzam esse limite de camada devem sempre apontar para dentro.

Este capítulo trata de maneiras de impor a Regra de Dependência. Queremos ter certeza de que não existem dependências
ilegais que apontem na direção errada (setas tracejadas na figura).

Modificadores de visibilidade

Vamos começar com a ferramenta mais básica que as linguagens orientadas a objetos em geral, e Java em particular,
nos fornecem para impor limites: modificadores de visibilidade.

Os modificadores de visibilidade têm sido um tópico em quase todas as entrevistas de emprego iniciais que conduzi nos
últimos anos. Eu perguntaria ao entrevistado quais modificadores de visibilidade o Java fornece e quais são suas
diferenças.

A maioria dos entrevistados lista apenas os modificadores público, protegido e privado . Apenas alguns deles conhecem
o modificador package-private (ou padrão) . Esta é sempre uma oportunidade bem-vinda para fazer algumas perguntas
sobre por que tal modificador de visibilidade faria sentido, a fim de descobrir se o entrevistado pode abstrair-se de seu
conhecimento prévio.

Então, por que o modificador package-private é um modificador tão importante? Porque nos permite usar pacotes Java
para agrupar classes em “módulos” coesos. As classes dentro de tal módulo podem acessar umas às outras, mas não
podem ser acessadas de fora do pacote. Podemos então optar por tornar públicas classes específicas para atuarem como
pontos de entrada para o módulo. Isso reduz o risco de violar acidentalmente a Regra de Dependência ao introduzir uma
dependência que aponta na direção errada.

Vamos dar uma outra olhada na estrutura do pacote discutida no Capítulo 4, Organizando o código, com modificadores
de visibilidade em mente:
Machine Translated by Google

110 Aplicando Limites de Arquitetura

Podemos tornar as classes do pacote de persistência package-private (marcadas com o na árvore acima)
porque elas não precisam ser acessadas pelo mundo externo. O adaptador de persistência é acessado por
meio das portas de saída que ele implementa. Pelo mesmo motivo, podemos fazer com que o SendMoneyService
classe pacote-privado. Os mecanismos de injeção de dependência geralmente usam reflexão para instanciar classes,
portanto, eles ainda serão capazes de instanciar essas classes mesmo que sejam privadas do pacote.

Com o Spring, essa abordagem só funciona se usarmos a abordagem de varredura de caminho de classe discutida
no Capítulo 10, Montando o aplicativo, entretanto, uma vez que as outras abordagens exigem que nós mesmos
criemos instâncias desses objetos, o que requer acesso público.

O restante das classes no exemplo devem ser públicas (marcadas com +) conforme definido pela nossa arquitetura:
o pacote de domínio precisa ser acessível pelas outras camadas e a camada de aplicação precisa ser acessível pelos
adaptadores web e de persistência .
Machine Translated by Google

Função de fitness pós-compilação 111

O modificador package-private é incrível para módulos pequenos com não mais do que um punhado de classes.
Entretanto, quando um pacote atinge um certo número de classes, fica confuso ter tantas classes no
mesmo pacote. Nesse caso, gosto de criar subpacotes para facilitar a localização do código (e, admito,
para satisfazer meu senso estético). É aqui que o modificador package-private falha na entrega, já que
Java trata os subpacotes como pacotes diferentes e não podemos acessar um membro package-private
de um subpacote. Assim, os membros dos subpacotes devem ser públicos, expondo-os ao mundo exterior
e tornando assim a nossa arquitetura vulnerável a dependências ilegais.

Função de fitness pós-compilação


Assim que usarmos o modificador público em uma classe, o compilador permitirá que qualquer outra classe o utilize, mesmo que
a direção da dependência aponte na direção errada de acordo com nossa arquitetura.

Como o compilador não nos ajudará nesses casos, temos que encontrar outros meios para verificar se a Regra de Dependência
não foi violada.

Uma maneira é introduzir uma função de aptidão – uma função que toma nossa arquitetura como entrada e
determina sua aptidão em relação a um aspecto específico. No nosso caso, a aptidão é definida como a Regra da
Dependência não é violada.

Idealmente, um compilador executa uma função de fitness para nós durante a compilação, mas, na falta disso, podemos executar
tal função em tempo de execução, após o código já ter sido compilado. Essas verificações de tempo de execução são melhor
executadas durante testes automatizados em uma construção de integração contínua.

Uma ferramenta que suporta esse tipo de função de adequação arquitetônica para Java é o ArchUnit. 1Entre outras
coisas, o ArchUnit fornece uma API para verificar se as dependências apontam na direção esperada. Se encontrar uma violação,
lançará uma exceção. É melhor executá-lo dentro de um teste baseado em uma estrutura de teste de unidade como JUnit,
fazendo com que o teste falhe em caso de violação de dependência.

Com o ArchUnit, podemos agora verificar as dependências entre nossas camadas, assumindo que cada
camada possui seu próprio pacote, conforme definido na estrutura de pacotes discutida na seção anterior.
Por exemplo, podemos verificar se não há dependência do modelo de domínio em nada fora do modelo de domínio:

1 ArchUnit: https://github.com/TNG/ArchUnit.
Machine Translated by Google

112 Aplicando Limites de Arquitetura

Esta regra valida as regras de dependência visualizadas na Figura 12.2.

Figura 12.2 – Nosso modelo de domínio pode acessar a si mesmo e alguns pacotes de
bibliotecas, mas pode não acessar código em nenhum outro pacote, por exemplo, os pacotes
contendo nossos adaptadores (inspirados nos diagramas em https://www.archunit.org/use -casos)

O problema com a regra anterior é que se usarmos algum código de biblioteca no modelo de domínio, teremos
que adicionar uma exceção a esta regra para cada dependência que introduzirmos (como fiz com lombok e java
no exemplo). No Capítulo 14, Uma abordagem baseada em componentes para arquitetura de software, veremos
uma regra que não apresenta esse problema.
Machine Translated by Google

Função de fitness pós-compilação 113

Com um pouco de trabalho, podemos até criar uma espécie de linguagem específica de domínio (DSL) além do
API ArchUnit que nos permite especificar todos os pacotes relevantes dentro de nossa Arquitetura Hexagonal
e então verificar automaticamente se todas as dependências entre esses pacotes apontam na direção certa:

No exemplo de código anterior, primeiro especificamos o pacote pai do nosso aplicativo. Em seguida,
especificamos os subpacotes para as camadas de domínio, adaptador, aplicativo e configuração. A chamada
final para check() executará então um conjunto de verificações, verificando se as dependências do pacote
são válidas de acordo com a Regra de Dependência. O código para esta DSL de arquitetura hexagonal está
disponível no GitHub se você quiser brincar com ele.2

Embora verificações pós-compilação como a anterior possam ser de grande ajuda no combate a dependências
ilegais, elas não são à prova de falhas. Se escrevermos incorretamente o nome do pacote buckpal no exemplo de
código anterior, por exemplo, o teste não encontrará nenhuma classe e, portanto, nenhuma violação de dependência.
Um único erro de digitação ou, mais importante, uma única refatoração ao renomear um pacote, pode tornar todo o
teste inútil. Devemos nos esforçar para tornar esses testes seguros para refatoração, ou pelo menos fazê-los falhar
quando uma refatoração os quebrar. No exemplo anterior, podemos falhar no teste quando um dos pacotes
mencionados não existir, por exemplo (porque foi renomeado).

2 DSL de arquitetura hexagonal para ArchUnit: https://github.com/thombergs/buckpal/


blob/master/src/test/java/io/reflectoring/buckpal/archunit/
Arquitetura Hexagonal.java.
Machine Translated by Google

114 Aplicando Limites de Arquitetura

Construir artefatos
Até agora, nossa única ferramenta para demarcar limites de arquitetura dentro de nossa base de código eram os pacotes.
Todo o nosso código faz parte do mesmo artefato de construção monolítico.

Um artefato de construção é o resultado de um processo de construção (esperançosamente automatizado). As ferramentas de construção


mais populares no mundo Java são atualmente Maven e Gradle. Então, até agora, imagine que tivéssemos um único script de construção
Maven ou Gradle e pudéssemos chamar Maven ou Gradle para compilar, testar e empacotar o código de nosso aplicativo em um único
arquivo JAR.

Uma característica principal das ferramentas de construção é a resolução de dependências. Para transformar uma determinada base de
código em um artefato de construção, uma ferramenta de construção primeiro verifica se todos os artefatos dos quais a base de código

depende estão disponíveis. Caso contrário, ele tenta carregá-los de um repositório de artefatos. Se isso falhar, a compilação falhará com
um erro antes mesmo de tentar compilar o código.

Podemos aproveitar isso para impor as dependências (e, assim, impor os limites) entre os módulos e camadas da nossa arquitetura. Para
cada módulo ou camada, criamos um módulo de construção separado com sua própria base de código e, como resultado, seu próprio
artefato de construção (arquivo JAR). No script de construção de cada módulo, especificamos apenas as dependências de outros módulos

que são permitidas de acordo com nossa arquitetura. Os desenvolvedores não podem mais criar dependências ilegais inadvertidamente
porque as classes nem estão disponíveis no caminho de classe e podem ocorrer erros de compilação.

Figura 12.3 – Diferentes maneiras de dividir nossa arquitetura em múltiplos


artefatos de construção para proibir dependências ilegais
Machine Translated by Google

Construir artefatos 115

A Figura 12.3 mostra um conjunto incompleto de opções para dividir nossa arquitetura em artefatos de construção separados.

Começando à esquerda, vemos uma construção básica de três módulos com um artefato de construção separado
para as camadas de configuração, adaptador e aplicativo. O módulo de configuração pode acessar o módulo
adaptadores , que por sua vez pode acessar o módulo de aplicação . O módulo de configuração também pode
acessar o módulo de aplicação devido à dependência implícita e transitiva entre eles. O módulo adaptadores
contém o adaptador web , bem como o adaptador de persistência . Isso significa que a ferramenta de construção
não proibirá dependências entre esses adaptadores. Embora as dependências entre esses adaptadores não sejam
estritamente proibidas pela Regra de Dependência (já que ambos os adaptadores estão na mesma camada
externa), na maioria dos casos, é sensato manter os adaptadores isolados um do outro. Afinal, geralmente não
queremos que mudanças na camada de persistência vazem para a camada web e vice-versa (lembre-se do
Princípio da Responsabilidade Única!). O mesmo vale para outros tipos de adaptadores, por exemplo, adaptadores
que conectam nosso aplicativo a uma determinada API de terceiros. Não queremos que detalhes dessa API vazem
para outros adaptadores adicionando dependências acidentais entre adaptadores.

Assim, podemos dividir o módulo adaptador único em vários módulos de construção, um para cada adaptador,
conforme mostrado na segunda coluna da Figura 12.3.

Em seguida, poderíamos decidir dividir ainda mais o módulo do aplicativo. Atualmente, ele contém as portas de
entrada e saída da nossa aplicação, os serviços que implementam ou usam essas portas e as entidades de domínio
que devem conter grande parte da nossa lógica de domínio.

Se decidirmos que nossas entidades de domínio não devem ser usadas como objetos de transferência
dentro de nossas portas (ou seja, queremos proibir a estratégia No Mapping do Capítulo 9, Mapeamento
entre Limites), podemos aplicar o Princípio de Inversão de Dependência e extrair um módulo api que
contém apenas as interfaces de porta (a terceira coluna na Figura 12.3). Os módulos adaptadores e o
módulo aplicativo podem acessar o módulo API , mas não o contrário. O módulo API não tem acesso
às entidades do domínio e não pode utilizá-las nas interfaces de porta. Além disso, os adaptadores não
têm mais acesso direto às entidades e serviços, portanto devem passar pelas portas.

Podemos até dar um passo adiante e dividir o módulo API em dois, uma parte contendo apenas as
portas de entrada e a outra contendo apenas as portas de saída (a quarta coluna na Figura 12.3). Dessa
forma, podemos deixar bem claro se um determinado adaptador é um adaptador de entrada ou de saída,
declarando uma dependência apenas nas portas de entrada ou de saída.

Além disso, poderíamos dividir ainda mais o módulo da aplicação, criando um módulo contendo apenas os serviços
e outro contendo apenas o modelo de domínio. Isso garante que o modelo de domínio não acesse os serviços e
permitiria que outros aplicativos (com casos de uso diferentes e, portanto, serviços diferentes) usassem o mesmo
modelo de domínio simplesmente declarando uma dependência no artefato de construção de domínio.

A Figura 12.3 ilustra que existem muitas maneiras diferentes de dividir uma aplicação em módulos de construção
e, é claro, há mais do que apenas as quatro maneiras mostradas na figura. A essência é que quanto mais fino
cortarmos nossos módulos, mais forte poderemos controlar as dependências entre eles. Quanto mais fino for o
corte, porém, mais mapeamento teremos que fazer entre esses módulos, aplicando uma das estratégias de
mapeamento apresentadas no Capítulo 9, Mapeamento entre Limites.
Machine Translated by Google

116 Aplicando Limites de Arquitetura

Além disso, demarcar limites de arquitetura com módulos de construção tem uma série de vantagens em
relação ao uso de pacotes simples como limite:

1. Primeiro, as ferramentas de construção odeiam absolutamente dependências circulares. As dependências circulares são ruins
porque uma mudança em um módulo dentro do círculo significaria potencialmente uma mudança em todos os outros
módulos dentro do círculo, o que é uma violação do Princípio da Responsabilidade Única. As ferramentas de construção
não permitem dependências circulares porque elas entrariam em um loop infinito ao tentar resolvê- las. Assim, podemos
ter certeza de que não existem dependências circulares entre nossos módulos de construção.

O compilador Java, por outro lado, não se importa se existe uma dependência circular entre dois ou mais pacotes.

2. Em segundo lugar, os módulos de construção permitem alterações isoladas de código em determinados módulos sem a
necessidade de levar os outros módulos em consideração. Imagine que temos que fazer uma grande refatoração na
camada de aplicação que causa erros temporários de compilação em um determinado adaptador. Se os adaptadores e a
camada de aplicação estiverem dentro do mesmo módulo de construção, alguns IDEs insistirão que todos os erros de
compilação nos adaptadores devem ser corrigidos antes que possamos executar os testes na camada de aplicação, mesmo
que os testes não precisem dos adaptadores para compilar . Entretanto, se a camada de aplicação estiver em seu próprio
módulo de construção, o IDE não se importará com os adaptadores no momento e poderemos executar os testes da
camada de aplicação à vontade. O mesmo vale para a execução de um processo de compilação com Maven ou Gradle: se
ambas as camadas estiverem no mesmo módulo de compilação, a compilação falhará devido a erros de compilação em qualquer uma das camadas

Portanto, vários módulos de construção permitem alterações isoladas em cada módulo. Poderíamos até optar por colocar
cada módulo em seu próprio repositório de código, permitindo que diferentes equipes mantivessem módulos diferentes.

3. Finalmente, com cada dependência entre módulos declarada explicitamente em um script de construção, adicionar uma nova
dependência torna-se um ato consciente em vez de um acidente. Um desenvolvedor que precisa de acesso a uma
determinada classe que atualmente não pode acessar, esperançosamente pensará um pouco sobre a questão se a
dependência é realmente razoável antes de adicioná-la ao script de construção.

Essas vantagens vêm com o custo adicional de ter que manter um script de construção, portanto, a arquitetura deve ser um tanto
estável antes de dividi-la em diferentes módulos de construção.

Além disso, os módulos de construção tendem a ser menos flexíveis para mudar ao longo do tempo. Uma vez escolhidos, tendemos
a nos ater aos módulos que definimos inicialmente. Se o fatiamento dos módulos não estiver correto desde o início, é menos provável
que o corrijamos mais tarde devido ao esforço adicional de refatoração. A refatoração é mais fácil quando todo o código está dentro
de um único módulo de construção.

Como isso me ajuda a construir software sustentável?


A arquitetura de software trata basicamente do gerenciamento de dependências entre os elementos da arquitetura. Se as
dependências virarem uma grande bola de lama, a arquitetura vira uma grande bola de lama.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 117

Portanto, para preservar a arquitetura ao longo do tempo, precisamos garantir continuamente que as dependências
apontem na direção certa.

Ao produzir novo código ou refatorar código existente, devemos manter a estrutura do pacote em mente e usar a
visibilidade package-private quando possível, para evitar dependências de classes que não devem ser acessadas
de fora do pacote.

Se precisarmos impor limites de arquitetura dentro de um único módulo de construção, e o modificador package-
private não funcionar porque a estrutura do pacote não permite isso, podemos usar ferramentas pós-compilação,
como ArchUnit.

Sempre que sentirmos que a arquitetura é estável o suficiente, devemos extrair os elementos da arquitetura em seus
próprios módulos de construção, pois isso dá controle explícito sobre as dependências.

Todas as três abordagens podem ser combinadas para impor limites de arquitetura e, assim, manter a base de
código sustentável ao longo do tempo.

No próximo capítulo, continuaremos a explorar os limites da arquitetura, mas de uma perspectiva diferente:
pensaremos em como gerenciar vários domínios (ou contextos limitados) na mesma aplicação, mantendo os limites
entre eles distintos.
Machine Translated by Google
Machine Translated by Google

13
Gerenciando vários
Contextos limitados
Muitos aplicativos consistem em mais de um domínio ou, para manter a linguagem do Domain-Driven
Design, em mais de um contexto limitado. O termo “contexto limitado” nos diz que deve haver limites
entre os diferentes domínios. Se não tivermos limites entre domínios diferentes, não haverá restrições às
dependências entre classes nesses domínios. Eventualmente, as dependências crescerão entre os domínios,
acoplando-os. Este acoplamento significa que os domínios não podem mais evoluir isoladamente, mas apenas
evoluir juntos. Poderíamos muito bem não ter separado nosso código em domínios diferentes!

A razão para separar o código em domínios diferentes é para que esses domínios possam evoluir isoladamente .
Esta é uma aplicação do Princípio da Responsabilidade Única, discutido no Capítulo 3, Invertendo Dependências.
Só que desta vez não estamos falando das responsabilidades de uma única classe, mas das responsabilidades
de todo um grupo de classes que compõem um contexto limitado. Se as responsabilidades de um contexto
limitado mudarem, não queremos alterar o código de outros contextos limitados!

Gerenciar contextos limitados, ou seja, manter claros os limites entre eles, é um dos principais desafios da
engenharia de software. Muitas das dificuldades que os desenvolvedores associam ao chamado “software
legado” decorrem de limites pouco claros. E acontece que o software não precisa de muito tempo para se tornar “legado”.

Portanto, sem surpresa (pelo menos em retrospecto), muitos leitores da primeira edição deste livro me
perguntaram como gerenciar múltiplos contextos limitados com a Arquitetura Hexagonal. Infelizmente, a resposta
não é simples. Como tantas vezes acontece, existem várias maneiras de fazer isso e nenhuma delas é certa ou
errada por si só. Vamos discutir algumas maneiras de separar contextos limitados.
Machine Translated by Google

120 Gerenciando vários contextos limitados

Um hexágono por contexto limitado?


Ao trabalhar com Arquitetura Hexagonal e múltiplos contextos limitados, nosso reflexo é criar um
“hexágono” separado para cada contexto limitado. O resultado seria algo como a Figura 13.1.

Figura 13.1 – Se cada contexto limitado for implementado como seu próprio hexágono, precisaremos de uma porta
de saída, um adaptador e uma porta de entrada para cada linha de comunicação entre contextos limitados

Cada contexto limitado vive em seu próprio hexágono, fornecendo portas de entrada para interagir com ele e
usando portas de saída para interagir com o mundo exterior.

Idealmente, os contextos limitados não precisam se comunicar entre si, portanto não temos dependências entre os
dois. No mundo real, entretanto, isso raramente acontece. Vamos supor que o contexto limitado à esquerda precise
chamar alguma funcionalidade do contexto limitado à direita.

Se usarmos os elementos de arquitetura que a Arquitetura Hexagonal nos fornece, adicionamos uma porta de
saída ao primeiro contexto limitado e uma porta de entrada ao segundo contexto limitado. Em seguida, criamos um
adaptador que implementa a porta de saída, faz qualquer mapeamento necessário e chama a porta de entrada do
segundo contexto limitado.
Machine Translated by Google

Um hexágono por contexto limitado? 121

Problema resolvido, certo?

Na verdade, no papel esta parece uma solução muito limpa. Os contextos limitados são perfeitamente separados uns
dos outros. As dependências entre eles estão claramente estruturadas na forma de portas e adaptadores. Novas
dependências entre contextos limitados exigem que os adicionemos explicitamente às portas existentes ou que
adicionemos uma nova porta. É improvável que as dependências surjam “por acidente” porque há muitos rituais
envolvidos na criação de tal dependência.

Se pensarmos além de apenas dois contextos limitados, porém, torna-se evidente que esta arquitetura não se adapta
muito bem. Para dois contextos limitados com uma dependência, precisamos implementar um adaptador (a caixa
denominada Adaptador de Domínio na figura anterior). Se excluirmos as dependências circulares, talvez tenhamos
que implementar três adaptadores para três contextos limitados, seis adaptadores para quatro contextos limitados e
1
assim por diante, como mostra a Figura 13.2.

Figura 13.2 – O número de dependências potenciais entre contextos limitados cresce


desproporcionalmente ao número de contextos limitados, mesmo se excluirmos dependências circulares

Para cada dependência, teríamos que implementar um adaptador com pelo menos uma porta de entrada e saída
associada. Cada adaptador teria que mapear de um modelo de domínio para outro. Isso rapidamente se torna uma
tarefa árdua de desenvolver e manter. Se for uma tarefa árdua e exigir mais esforço do que agregar valor, a equipe
tomará atalhos para evitá-la, resultando em uma arquitetura que à primeira vista parece uma Arquitetura Hexagonal,
mas não tem os benefícios que promete.

1 A fórmula que usei para calcular as dependências potenciais entre n contextos limitados é n-1 + + 1. O primeiro
n-2 + ... contexto limitado tem n-1 dependências potenciais e não circulares, o segundo n-2 e assim por diante.
O último contexto limitado não pode ter qualquer dependência de outro contexto limitado porque cada dependência
que ele possa ter seria uma dependência circular e não queremos permitir dependências circulares.
Machine Translated by Google

122 Gerenciando vários contextos limitados

Se observarmos o artigo original que apresenta a Arquitetura Hexagonal, nunca foi intenção da Arquitetura
Hexagonal encapsular um único contexto limitado em portas e adaptadores.2 Em vez disso, a intenção é
encapsular uma aplicação. Esta aplicação pode consistir em muitos contextos limitados ou em nenhum.

Faz sentido envolver cada contexto limitado em seu próprio hexágono quando estamos nos preparando para
extraí-los em seus próprios aplicativos, ou seja, em seus próprios (micro)serviços. Isso significa que devemos
ter muita certeza de que os limites que colocamos entre eles são os limites corretos, e não esperamos que
mudem.

A conclusão aqui é que a Arquitetura Hexagonal não fornece uma solução escalonável para gerenciar vários
contextos limitados no mesmo aplicativo. E não é necessário. Em vez disso, podemos nos inspirar no Domain-
Driven Design para dissociar nossos contextos limitados, porque dentro de um hexágono podemos fazer o
que quisermos.

Contextos limitados desacoplados


Na seção anterior, aprendemos que as portas e os adaptadores devem encapsular todo o aplicativo, e não
cada contexto limitado separadamente. Como mantemos os contextos limitados separados uns dos outros ,
então?

Num caso simples, poderíamos ter contextos limitados que não se comunicam entre si. Eles fornecem
caminhos completamente separados através do código. Neste caso, poderíamos construir portas de entrada
e saída dedicadas para cada contexto limitado, como na Figura 13.3.

Figura 13.3 – Se contextos limitados (linhas tracejadas) não precisam se comunicar entre si, cada um

pode implementar suas próprias portas de entrada e chamar suas próprias portas de saída

2 O artigo original sobre Arquitetura Hexagonal:


https://alistair.cockburn.us/hexagonal-architecture/.
Machine Translated by Google

Contextos limitados adequadamente acoplados 123

Este exemplo mostra uma Arquitetura Hexagonal com dois contextos limitados. Um adaptador da web está conduzindo o
aplicativo e um adaptador de banco de dados é conduzido pelo aplicativo. Esses adaptadores são representativos de quaisquer
outros adaptadores de entrada e saída – nem todo aplicativo é um aplicativo da web com um banco de dados.

Cada contexto limitado expõe seus próprios casos de uso por meio de uma ou mais portas de entrada dedicadas. O web
adaptor conhece todas as portas de entrada e, portanto, pode chamar a funcionalidade de todos os contextos limitados.

Em vez de ter portas de entrada dedicadas para cada um dos nossos contextos limitados, também poderíamos implementar
uma porta de entrada “ampla” através da qual o adaptador web roteia solicitações para vários contextos limitados.
Neste caso, as fronteiras entre os contextos ficariam escondidas do lado de fora do nosso hexágono.
Isto pode ou não ser desejável dependendo da situação.

Além disso, cada contexto limitado define sua própria porta de saída para o banco de dados para que possa armazenar e
recuperar seus dados independentemente de qualquer outro contexto limitado.

Embora a divisão das portas de entrada por contexto limitado seja opcional, recomendo fortemente manter as portas de saída
que armazenam e recuperam os dados de domínio para um contexto limitado separadas de outros contextos limitados. Se um
contexto limitado estiver relacionado com transações financeiras e o outro com registros de usuários, deverá haver uma (ou

mais) porta de saída dedicada ao armazenamento e recuperação de dados de transação e outra dedicada ao armazenamento
e recuperação de dados de registro.

Cada contexto limitado deve ter sua própria persistência. Se contextos limitados compartilharem portas de saída para armazenar
e recuperar dados, eles rapidamente se tornarão fortemente acoplados porque ambos dependem do mesmo modelo de dados.
Imagine que precisamos extrair um contexto limitado do aplicativo Hexagonal para seu próprio microsserviço porque aprendemos
que ele possui requisitos de escalabilidade diferentes do restante do aplicativo. Se esse contexto limitado compartilhar um
modelo de banco de dados com outro contexto limitado, será muito difícil extraí-lo. Não gostaríamos que o novo microsserviço
chegasse ao banco de dados de outro aplicativo, não é? Pela mesma razão, queremos manter separado o modelo de banco
de dados de cada contexto limitado.

Contanto que vários contextos limitados sejam executados no mesmo tempo de execução, eles poderão compartilhar um banco
de dados físico e participar das mesmas transações de banco de dados. Mas dentro desse banco de dados, deve haver limites
claros entre os dados de diferentes contextos limitados, por exemplo, na forma de um esquema de banco de dados separado
ou, pelo menos, de diferentes tabelas de banco de dados.

Dividir as portas de entrada e saída desta forma tem o efeito agradável de que os contextos limitados são completamente
dissociados. Cada contexto limitado pode evoluir por si mesmo sem afetar os outros de forma alguma . Mas eles só estão
dissociados porque não estão falando um com o outro. E se tivermos casos de uso que abrangem vários contextos limitados
ou se um contexto limitado precisar se comunicar com outro?

Contextos limitados adequadamente acoplados


Se todo o acoplamento pudesse ser evitado, a arquitetura de software seria muito mais fácil. Em aplicações do mundo real, um
contexto limitado muito provavelmente precisa da ajuda de outro contexto limitado para fazer o seu trabalho.
Machine Translated by Google

124 Gerenciando vários contextos limitados

Um exemplo é novamente o nosso contexto limitado que se preocupa com transações monetárias. Por razões de
segurança, queremos registrar qual usuário emitiu uma transação. Isso significa que nosso contexto limitado precisa
de algumas informações sobre o usuário, que vive em outro contexto limitado. Mas nosso contexto limitado não
precisa estar fortemente acoplado ao contexto de gerenciamento de usuários.

Em vez de conhecer todo o objeto de usuário em nosso contexto limitado de “gerenciamento de transações”, pode
ser suficiente apenas conhecer o ID do usuário. Embora um objeto de usuário no contexto de “registro” seja um
objeto complexo com muitos atributos, uma representação de um usuário no contexto de transação pode ser apenas
um wrapper em torno do ID do usuário. No caso de uso Enviar dinheiro, agora poderíamos simplesmente aceitar o
ID do usuário que está executando a transação como entrada e registrá-la. Não precisamos associar o contexto da
transação a todos os outros detalhes de um usuário.

Mas podemos querer validar se o usuário não está bloqueado nas transações. Nesse caso, podemos usar um evento
de domínio.3 Sempre que o status de um usuário muda no contexto de gerenciamento de usuários, acionamos um
evento de domínio que pode ser recebido por outros contextos limitados. Nosso contexto de transação pode escutar
eventos quando um usuário foi registrado recentemente ou foi bloqueado, por exemplo. Ele pode então armazenar
essas informações em seu próprio banco de dados para uso posterior no caso de uso Enviar dinheiro para validar o status do usuário.

Outra solução possível é introduzir um serviço de aplicação como orquestrador entre o gerenciamento de usuários
e os contextos de transação.4 O serviço de aplicação implementa a porta de entrada Enviar dinheiro. Quando
chamado, ele primeiro solicita ao contexto limitado de gerenciamento de usuários o status do usuário e, em
seguida, passa o status para o caso de uso Enviar dinheiro fornecido pelo contexto de transação – uma
implementação diferente, mas com o mesmo efeito de quando se usa eventos de domínio.

Esses foram apenas dois exemplos de como acoplar “apropriadamente” contextos limitados. Se ainda não o fez,
recomendo a leitura da literatura sobre Design Orientado a Domínio para se inspirar.

Voltando à Arquitetura Hexagonal, o acoplamento adequado de múltiplos contextos limitados pode ser algo como
na Figura 13.4.

3 Eventos em Design Orientado a Domínio: Implementando Design Orientado a Domínio por Vaughn Vernon, Pearson,
2013, Capítulo 8.
4 Serviços de aplicativos em design orientado a domínio: implementando design orientado a domínio por Vaughn
Vernon, Pearson, 2013, Capítulo 14.
Machine Translated by Google

Como isso me ajuda a construir software sustentável? 125

Figura 13.4 – Se tivermos casos de uso abrangendo vários contextos limitados, podemos introduzir um
serviço de aplicação para orquestrar e eventos de domínio para compartilhar informações entre contextos

Introduzimos um serviço de aplicativo como orquestrador acima de nossos contextos limitados. As portas de
entrada são agora implementadas por este serviço em vez dos próprios contextos limitados. O serviço de
aplicação pode chamar portas de saída para obter as informações necessárias de outros sistemas e depois
chamar um ou mais serviços de domínio fornecidos pelos contextos limitados. Além de orquestrar as chamadas
para os contextos limitados, o serviço de aplicação também atua como um limite de transação para que
possamos chamar vários serviços de domínio na mesma transação de banco de dados, por exemplo.

Cada um dos serviços de domínio dentro dos contextos limitados ainda usa suas próprias portas de saída de banco de
dados para manter o modelo de dados entre os contextos limitados separados. Podemos decidir que esta separação não
é necessária e usar uma única porta de saída do banco de dados (mas devemos estar cientes de que compartilhar um
modelo de dados leva a um acoplamento muito forte).

Os contextos limitados têm acesso a um conjunto de eventos de domínio compartilhado que podem emitir e ouvir,
respectivamente, para trocar informações de maneira fracamente acoplada.

Como isso me ajuda a construir software sustentável?


Gerenciar limites entre domínios é uma das partes mais difíceis do desenvolvimento de software. Em uma base de código
pequena, os limites podem não ser necessários porque o modelo mental de toda a base de código ainda cabe na memória
de trabalho do nosso cérebro. Mas assim que a base de código atingir um determinado tamanho, devemos nos certificar
de introduzir limites entre os domínios, para que possamos raciocinar sobre cada domínio isoladamente. Se não fizermos
isso, as dependências surgirão, transformando nossa base de código em uma daquelas temidas “grandes bolas de lama”.
Machine Translated by Google

126 Gerenciando vários contextos limitados

A Arquitetura Hexagonal trata do gerenciamento de uma fronteira entre um aplicativo e o mundo exterior.
O limite é composto de determinadas portas de entrada fornecidas pelo aplicativo e de determinadas
portas de saída esperadas pelo aplicativo.

A Arquitetura Hexagonal não nos ajuda a gerenciar limites mais refinados em nossa aplicação.
Dentro do nosso “hexágono”, podemos fazer o que quisermos. Se a base de código ficar muito grande para nossa
memória de trabalho, devemos recorrer ao Domain-Driven Design ou outros conceitos para criar limites dentro de
nossa base de código.

No próximo capítulo, exploraremos um método leve de criação de limites que podemos usar com ou sem Arquitetura
Hexagonal.
Machine Translated by Google

14
Uma abordagem baseada em
componentes para arquitetura de softw

Quando iniciamos um projeto de software, nunca sabemos todos os requisitos que os usuários nos
apresentarão quando estiverem realmente usando o software. Um projeto de software está sempre associado
a arriscar e fazer suposições fundamentadas (gostamos de chamá-las de “suposições” para parecer mais
profissional). O ambiente de um projeto de software é muito volátil para saber antecipadamente como tudo
vai acontecer. Essa volatilidade é a razão pela qual nasceu o movimento Agile. As práticas ágeis tornam as
organizações flexíveis o suficiente para se adaptarem às mudanças.

Mas como podemos criar uma arquitetura de software que possa lidar com um ambiente tão ágil? Se tudo pode
mudar a qualquer momento, será que deveríamos nos preocupar com arquitetura?

Sim nós deveríamos. Conforme discutido no Capítulo 1, Manutenção, devemos nos certificar de que nossa
arquitetura de software permite a manutenção. Uma base de código sustentável pode evoluir com o tempo,
adaptando-se a fatores externos.

A Arquitetura Hexagonal dá um grande passo em direção à manutenção. Está criando uma fronteira entre nosso
aplicativo e o mundo exterior. No interior da nossa aplicação (dentro do hexágono), temos o nosso código de
domínio, que fornece portas dedicadas para o mundo exterior. Essas portas conectam o aplicativo a adaptadores,
que se comunicam com o mundo externo, traduzindo entre o idioma do nosso aplicativo e os idiomas dos sistemas
externos. Essa arquitetura melhora a capacidade de manutenção porque o aplicativo pode evoluir principalmente
independentemente do mundo exterior. Contanto que as portas não mudem, podemos evoluir qualquer coisa
dentro da aplicação para reagir às mudanças no ambiente ágil.

Mas, como aprendemos no Capítulo 13, Gerenciando Múltiplos Contextos Delimitados, a Arquitetura Hexagonal
não nos ajuda a criar limites dentro do núcleo da nossa aplicação. Podemos querer aplicar uma arquitetura
diferente em nosso núcleo de aplicação que nos ajude nesse aspecto.

Além disso, já ouvi algumas vezes que a Arquitetura Hexagonal parece difícil, especialmente para um
projeto de software que está apenas começando. É difícil envolver a equipe porque nem todos entendem
o valor da inversão de dependência e do mapeamento entre o modelo de domínio e o mundo exterior.
A Arquitetura Hexagonal pode ser um exagero para uma aplicação novata.
Machine Translated by Google

128 Uma abordagem baseada em componentes para arquitetura de software

Para casos como este, podemos querer começar com um estilo de arquitetura mais simples que ainda forneça a
modularidade necessária para evoluir para algo mais no futuro, mas que seja simples o suficiente para envolver
todos . Proponho que uma arquitetura baseada em componentes seja um bom ponto de partida e usaremos este
capítulo para discutir esse estilo de arquitetura.

Modularidade através de componentes


Um dos impulsionadores da manutenção é a modularidade. A modularidade nos permite vencer a complexidade de
um sistema de software dividindo-o em módulos mais simples. Não precisamos entender todo o sistema para poder
trabalhar em um módulo específico. Em vez disso, podemos nos concentrar naquele módulo e, potencialmente, nos
módulos com os quais ele faz interface. Os módulos podem evoluir independentemente uns dos outros, desde que
as interfaces entre os módulos sejam claramente definidas. Provavelmente conseguiremos encaixar um modelo
mental de um módulo em nossa memória de trabalho, mas boa sorte ao criar um modelo mental se não houver
módulos na base de código. Ficaríamos pulando no código de forma bastante impotente.

Somente a modularidade permite que nós, humanos, criemos sistemas complexos. Em seu livro Modern Software
Engineering, Dave Farley fala sobre a modularidade do programa espacial Apollo:1

“Essa modularidade tinha muitas vantagens. Isso significava que cada componente poderia ser construído para focar
em uma parte do problema e precisaria comprometer menos em seu design. Permitiu que diferentes grupos – neste
caso, empresas completamente diferentes – trabalhassem em cada módulo de forma bastante independente dos
outros. Desde que os diferentes grupos concordassem sobre como os módulos iriam interagir entre si, eles poderiam
trabalhar para resolver os problemas do seu módulo sem restrições.”

A modularidade permitiu-nos ir à Lua! A modularidade nos permite construir carros, aeronaves e edifícios.
Não deveria ser surpresa que também nos ajude a construir software complexo.

Mas o que é um módulo? Sinto que o termo está sobrecarregado no desenvolvimento de software (orientado a objetos).
Tudo e seu gato são chamados de “módulo”, mesmo que seja apenas um monte de classes que foram reunidas ao
acaso para fazer algo útil. Prefiro o termo “componente” para descrever um grupo de classes que foram
cuidadosamente projetadas para implementar certas funcionalidades que podem ser compostas em conjunto com
outros grupos de classes para construir um sistema complexo. O aspecto da composição implica que os componentes
podem ser compostos para formar um todo maior e potencialmente até mesmo recompostos para reagir às mudanças
no ambiente. A capacidade de composição requer que um componente defina uma interface clara que nos diga o
que ele fornece e o que precisa do mundo externo (portas de entrada e saída, alguém?). Pense em peças de LEGO.
Um tijolo LEGO fornece um determinado layout de pinos para outros tijolos serem fixados e requer um determinado
layout de pinos para serem fixados a outros tijolos. Dito isso, não irei julgá-lo se você usar o termo “módulo”, mas me
referirei a “componentes” no restante deste capítulo.

1 Modularidade do programa espacial Apollo: Modern Software Engineering por Dave Farley, Pearson, 2022, Capítulo
6.
Machine Translated by Google

Modularidade através de componentes 129

Para fins deste capítulo, um componente é um conjunto de classes que possui um namespace dedicado e uma API
claramente definida. Se outro componente precisar da funcionalidade deste componente, ele poderá chamá-lo por meio
de sua API, mas poderá não acessar seus componentes internos. Um componente pode ser composto de componentes menores.
Por padrão, esses subcomponentes residem dentro do componente pai, de modo que não são acessíveis
externamente. Eles podem, no entanto, contribuir para a API do componente pai se implementarem funcionalidades
que deveriam ser acessíveis externamente.

Como qualquer outro estilo de arquitetura, a arquitetura baseada em componentes trata de quais dependências
são permitidas e quais são desencorajadas. Isso é ilustrado na Figura 14.1.

Figura 14.1 – As dependências de um pacote interno são inválidas, mas as dependências de um pacote
API são válidas, desde que o pacote API não esteja aninhado em um pacote interno

Aqui, temos dois componentes de nível superior, A e B. O componente A é composto por dois subcomponentes,
A1 e A2, enquanto o componente B possui apenas um único subcomponente, B1.

Se A1 precisar de acesso à funcionalidade de B, ele poderá obtê-lo chamando a API de B. No entanto, não pode
aceder à API do B1, porque, como subcomponente, faz parte do interior do seu pai e, portanto, está oculto do exterior.
B1 ainda pode contribuir com funcionalidade para a API de seu pai, implementando uma interface na API pai.
Veremos isso em ação no estudo de caso mais tarde.
Machine Translated by Google

130 Uma abordagem baseada em componentes para arquitetura de software

As mesmas regras se aplicam entre os componentes irmãos, A1 e A2. Se A1 precisar de acesso à funcionalidade de A2, ele poderá
chamar sua API, mas não poderá chamar os componentes internos de A2.

E isso é tudo que existe na arquitetura baseada em componentes. Pode ser resumido em quatro regras simples:

1. Um componente possui um namespace dedicado para ser endereçável.

2. Um componente possui uma API dedicada e componentes internos.

3. A API de um componente pode ser chamada externamente, mas seus componentes internos não.

4. Um componente pode conter subcomponentes como parte de seus componentes internos.

Para tornar o abstrato concreto, vamos ver uma arquitetura baseada em componentes em código real.

Estudo de caso – construindo um componente “Check Engine”


Como estudo de caso para a arquitetura baseada em componentes apresentada neste capítulo, extraí um
componente de um projeto de software real em que trabalhei para um repositório GitHub independente.2 O
simples fato de que extraí o componente com relativamente pouco esforço e de que podemos raciocinar
sobre este componente sem saber nada sobre o projeto de software de onde ele vem mostra que
conquistamos com sucesso a complexidade aplicando modularidade!

O componente é escrito em Kotlin orientado a objetos, mas os conceitos se aplicam a qualquer outra linguagem orientada a objetos.

O componente é chamado de “mecanismo de verificação”. Era para ser uma espécie de web scraper que percorre páginas da web
e executa um conjunto de verificações nelas. Essas verificações podem ser qualquer coisa, desde “verificar se o HTML dessa página
da web é válido” até “retornar todos os erros ortográficos dessa página da web”.

Como muitas coisas podem dar errado ao copiar páginas da web, decidimos executar as verificações de forma assíncrona. Isso
significa que o componente precisa fornecer uma API para agendar verificações e uma API para recuperar os resultados de uma
verificação após sua execução. Isto implica uma fila para armazenar as solicitações de verificação recebidas e um banco de dados
para armazenar os resultados dessas verificações.

Do lado de fora, não importa se construímos o mecanismo de verificação “inteiro” ou o dividimos em subcomponentes. Contanto
que o componente tenha uma API dedicada, esses detalhes ficam ocultos do lado de fora. Os requisitos acima, no entanto, delineiam
certos limites naturais para subcomponentes do mecanismo de verificação. Dividir o mecanismo de verificação ao longo desses
limites nos permite gerenciar a complexidade dentro do componente do mecanismo de verificação porque cada subcomponente
será mais simples de gerenciar do que todo o problema.

2 O projeto GitHub com o “mecanismo de verificação” implementado em arquitetura baseada em componentes:


https://github.com/thombergs/components-example.
Machine Translated by Google

Estudo de caso – construindo um componente “Check Engine” 131

Criamos três subcomponentes para o mecanismo de verificação:

• Um componente de fila que encapsula o acesso a uma fila para enfileirar e retirar da fila solicitações de verificação.

• Um componente de banco de dados que envolve o acesso a um banco de dados para armazenar e recuperar resultados de verificação.

• Um componente checkrunner que sabe quais verificações executar e as executa sempre que uma verificação

a solicitação chega da fila.

Observe que esses subcomponentes introduzem principalmente limites técnicos. Muito semelhante aos adaptadores de saída na Arquitetura

Hexagonal, escondemos as especificidades do acesso a um sistema externo (a fila e o banco de dados) em subcomponentes. Mas então, o

componente do mecanismo de verificação é um componente muito técnico com pouco ou nenhum código de domínio. O único componente

que poderíamos considerar “código de domínio” é o checkrunner, que atua como uma espécie de controlador. Os componentes técnicos se

adaptam muito bem a uma arquitetura baseada em componentes porque os limites entre eles são mais claros do que os limites entre os

diferentes domínios funcionais.

Vamos dar uma olhada em um diagrama de arquitetura do componente do mecanismo de verificação para nos aprofundarmos nos detalhes

(Figura 14.2).

Figura 14.2 – O componente check engine é composto por três


subcomponentes que contribuem para a API do componente pai

O diagrama reflete a estrutura do código. Você pode pensar em cada caixa como um pacote Java (ou uma simples pasta de código-fonte em

outras linguagens de programação). Se uma caixa estiver dentro de uma caixa maior, é um subpacote dessa caixa maior. As caixas do nível

mais baixo, finalmente, são classes.

A API pública do componente do mecanismo de verificação consiste em CheckScheduler e CheckQueries


interfaces, que permitem agendar uma verificação de página da web e recuperar os resultados da verificação, respectivamente.
Machine Translated by Google

132 Uma abordagem baseada em componentes para arquitetura de software

CheckScheduler é implementado pela classe SqsCheckScheduler que reside nos componentes internos
da fila. Dessa forma, o componente da fila contribui para a API do componente pai. Somente quando
olhamos para o nome dessa classe é que ela nos diz que ela está usando o Simple Queue Service (SQS)
da Amazon. Este detalhe de implementação não vaza para fora do componente do mecanismo de
verificação. Nem mesmo os componentes irmãos sabem qual tecnologia de fila é usada. Você pode notar
que o componente queue nem possui um pacote de API, então todas as suas classes são internas!

A classe CheckRequestListener , então, escuta as solicitações recebidas da fila. Para cada


solicitação recebida, ele chama a interface CheckRunner na API do subcomponente checkrunner.
DefaultCheckRunner implementa essa interface. Ele lê o URL da página da web da solicitação recebida,
determina quais verificações serão executadas e, em seguida, executa essas verificações.

Quando uma verificação é concluída, a classe DefaultCheckRunner armazena os resultados no banco de


dados chamando a interface CheckMutations da API do subcomponente do banco de dados. Essa interface é
implementada pela classe CheckRepository , que trata dos detalhes de conexão e comunicação com um banco
de dados. Novamente, a tecnologia do banco de dados não vaza para fora do subcomponente do banco de dados.

A classe CheckRepository também implementa a interface CheckQueries , que faz parte da API pública do
mecanismo de verificação. Esta interface fornece métodos para consultar resultados de verificação.

Ao dividir o componente do mecanismo de verificação em três subcomponentes, dividimos a complexidade.


Cada subcomponente resolve uma parte mais simples do problema geral. Ele pode evoluir principalmente por si
só. Uma mudança nas tecnologias de fila ou de banco de dados devido a custos, escalabilidade ou outros motivos
não vaza para outros subcomponentes. Poderíamos até substituir os subcomponentes por implementações
simples na memória para testes, se quiséssemos.

Tudo isso conseguimos estruturando nosso código em componentes, seguindo a convenção de ter API dedicada
e pacotes internos.

Aplicando limites de componentes


É bom ter convenções, mas se isso for tudo, alguém irá quebrá-las e a arquitetura sofrerá erosão. Precisamos
fazer cumprir as convenções da arquitetura de componentes.

O bom da arquitetura de componentes é que podemos aplicar uma função de aptidão relativamente simples para
garantir que nenhuma dependência acidental tenha surgido em nossa arquitetura de componentes:

Nenhuma classe que esteja fora de um pacote “interno” deve acessar uma classe dentro desse pacote “interno”.
Machine Translated by Google

Aplicando limites de componentes 133

Se colocarmos todos os componentes internos de um componente em um pacote chamado “internal” (ou em um pacote
marcado como “interno” de alguma outra forma), só teremos que verificar se nenhuma classe nesse pacote é chamada
de fora desse pacote. Para projetos baseados em JVM, podemos codificar esta função de fitness com ArchUnit:3

Precisamos apenas de uma maneira de identificar pacotes internos durante cada compilação e alimentá-los todos na função
acima, e a compilação falhará se introduzirmos acidentalmente uma dependência em uma classe interna.

A função de fitness nem precisa saber nada sobre os componentes da nossa arquitetura.
Precisamos apenas seguir uma convenção para identificar pacotes internos e então alimentar esses pacotes na função.
Isso significa que não precisamos atualizar o teste que está executando a função de fitness sempre que adicionamos ou
removemos um componente da base de código. Muito conveniente!

Observação

Essa função de aptidão é uma forma invertida da função de aptidão que apresentamos no Capítulo 12, Aplicando
Limites de Arquitetura. No Capítulo 12, verificamos que as classes de um determinado pacote não acessam classes
fora desse pacote. Aqui, verificamos se as classes de fora do pacote não estão acessando as classes dentro do
pacote. Esta função de fitness é muito mais estável, pois não precisamos adicionar exceções para cada biblioteca
que usamos.

Ainda podemos introduzir dependências indesejadas simplesmente não seguindo nossa convenção para pacotes internos,
é claro. E a regra ainda permite uma brecha: se colocarmos classes diretamente no pacote “interno” de um componente de
nível superior, as classes de quaisquer subcomponentes poderão acessá-lo. Portanto, podemos querer introduzir outra
regra que proíba qualquer classe diretamente no pacote “interno” de um componente de nível superior.

3 Regra ArchUnit para validar que nenhum código acessa código dentro de um determinado pacote:
https://github.com/thombergs/components-example/blob/main/
servidor/src/test/kotlin/io/reflectoring/components/Internal

PacoteTest.kt.
Machine Translated by Google

134 Uma abordagem baseada em componentes para arquitetura de software

Como isso me ajuda a construir software sustentável?


A arquitetura baseada em componentes é muito simples. Contanto que cada componente tenha um namespace
dedicado, API dedicada e pacotes internos, e as classes dentro de um pacote interno não sejam chamadas de fora,
obteremos uma base de código muito sustentável que consiste em muitos componentes que podem ser compostos e
recombinados . Se adicionarmos a regra de que os componentes podem ser compostos de outros componentes,
poderemos construir uma aplicação inteira a partir de partes cada vez menores, onde cada parte resolve um problema mais simples.

Embora existam lacunas para contornar as regras da arquitetura de componentes, a arquitetura em si é tão
simples que é muito fácil de entender e comunicar. Se for fácil de entender, é fácil de manter. Se for fácil de
manter, é menos provável que as lacunas sejam exploradas.

A Arquitetura Hexagonal se preocupa com os limites no nível do aplicativo. A arquitetura baseada em


componentes se preocupa com os limites no nível do componente. Podemos usar isso para incorporar
componentes em uma arquitetura hexagonal ou podemos optar por começar com uma arquitetura simples
baseada em componentes e evoluí-la em qualquer outra arquitetura, caso seja necessário. Uma arquitetura
baseada em componentes é modular por design e os módulos são fáceis de movimentar e refatorar.

No próximo e último capítulo, encerraremos a discussão em torno da arquitetura e tentaremos responder à


questão de quando devemos escolher qual estilo de arquitetura.
Machine Translated by Google

15
Decidindo
sobre um estilo de arquitetura

Até agora, este livro forneceu uma abordagem opinativa para a construção de uma aplicação web no estilo
de arquitetura hexagonal. Da organização do código à tomada de atalhos, respondemos a muitas questões
que esse estilo de arquitetura nos confronta.

Algumas das respostas deste livro podem ser aplicadas ao estilo convencional de arquitetura em camadas.
Algumas respostas só podem ser implementadas numa abordagem centrada no domínio, como a proposta neste livro.
E algumas respostas com as quais você talvez nem concorde porque não funcionaram em sua experiência.

As questões finais para as quais queremos respostas, entretanto, são estas: quando devemos realmente usar o
estilo de Arquitetura Hexagonal? E quando devemos preferir o estilo convencional em camadas (ou qualquer
outro estilo)?

Comece simples

Um ponto importante que demorei muito para perceber é que a arquitetura de software não é apenas algo que
definimos no início de um projeto de software e que cuidará de si mesmo depois. Não podemos saber tudo o que
precisamos saber para desenhar uma grande arquitetura logo no início de um projeto! A arquitetura de um projeto
de software pode e deve evoluir ao longo do tempo para se adaptar às mudanças nos requisitos.

Isso significa que não saberemos qual estilo de arquitetura será o melhor para o projeto de software no longo
prazo e poderemos precisar mudar o estilo de arquitetura no futuro! Para tornar isso possível, precisamos ter
certeza de que nosso software é flexível a mudanças. Precisamos plantar uma semente de sustentabilidade.

Capacidade de manutenção significa que precisamos tornar nosso código modular para que possamos trabalhar
em cada módulo isoladamente e movê-lo na base de código, caso seja necessário. Nossa arquitetura precisa
deixar os limites entre esses módulos tão claros quanto possível, para que dependências indesejadas entre
esses módulos não surjam acidentalmente, reduzindo a capacidade de manutenção.
Machine Translated by Google

136 Decidindo sobre um estilo de arquitetura

O início de um projeto pode envolver apenas uma coleção de casos de uso CRUD, e uma arquitetura
centrada em domínio, como a Arquitetura Hexagonal, pode ser um exagero, então optamos por algo mais
simples, como a abordagem baseada em componentes. Ou talvez já saibamos o suficiente sobre o projeto
para começarmos a construir um modelo de domínio rico; nesse caso, o estilo de Arquitetura Hexagonal
pode ser o certo para começar.

Evolua o domínio
Com o tempo, aprendemos cada vez mais sobre os requisitos do nosso software e podemos tomar decisões cada vez
melhores sobre o melhor estilo de arquitetura. O aplicativo pode evoluir de uma coleção de casos de uso simples de
CRUD para um aplicativo rico centrado em domínio com muitas regras de negócios. Neste ponto, o estilo Arquitetura
Hexagonal se torna uma boa opção.

Deveria ter ficado claro nos capítulos anteriores que a principal característica de um estilo de Arquitetura Hexagonal é
que podemos desenvolver código de domínio livre de desvios, como preocupações de persistência e dependências de
sistemas externos. Na minha opinião, a evolução do código de domínio livre de influência externa é o argumento mais
importante para o estilo da Arquitetura Hexagonal.

É por isso que esse estilo de arquitetura combina tão bem com as práticas de DDD. Para afirmar o óbvio, no DDD, o
domínio impulsiona o desenvolvimento, e podemos raciocinar melhor sobre o domínio se não tivermos que pensar
simultaneamente em questões de persistência e outros aspectos técnicos.

Eu chegaria ao ponto de dizer que estilos de arquitetura centrados em domínio, como o estilo hexagonal, são facilitadores
do DDD. Sem uma arquitetura que coloque o domínio no centro das coisas e sem inverter as dependências em relação
ao código do domínio, não temos chance de realmente fazer DDD. O design sempre será orientado por outros fatores.

Portanto, como um primeiro indicador sobre se deve ou não usar o estilo de arquitetura apresentado neste livro: se o código
de domínio não for a coisa mais importante em sua aplicação, você provavelmente não precisará desse estilo de arquitetura.

Confie na sua experiência


Somos criaturas de hábitos. Os hábitos automatizam as decisões para nós, para que não precisemos perder tempo com eles.
Se houver um leão correndo em nossa direção, nós corremos. Se construirmos uma nova aplicação web, usaremos o
estilo de arquitetura em camadas. Já fizemos isso tantas vezes no passado que se tornou um hábito.

Não estou dizendo que as decisões habituais sejam necessariamente más decisões. Os hábitos são tão bons para ajudar
a tomar uma boa decisão quanto para ajudar a tomar uma decisão ruim. Estou dizendo que estamos fazendo aquilo em
que temos experiência. Estamos confortáveis com o que fizemos no passado, então por que deveríamos mudar alguma coisa?

Portanto, a única maneira de tomar uma decisão informada sobre um estilo de arquitetura é ter experiência em diferentes
estilos de arquitetura. Se você não tiver certeza sobre o estilo da Arquitetura Hexagonal, experimente-o em um pequeno
módulo do aplicativo que você está construindo atualmente. Acostume-se com os conceitos. Fique confortável. Aplique
as ideias deste livro, modifique-as e adicione suas próprias ideias para desenvolver um estilo com o qual você se sinta
confortável.
Machine Translated by Google

Depende 137

Essa experiência poderá então orientar sua próxima decisão de arquitetura.

Depende
Eu adoraria fornecer uma lista de questões de múltipla escolha para decidir sobre um estilo de arquitetura, assim como
todas aquelas “Que tipo de personalidade você é?” e “Se você fosse um cachorro, que tipo de cachorro você seria?”
testes que circulam regularmente nas redes sociais.1

Porém, não é tão fácil assim. A minha resposta à questão de qual estilo de arquitetura escolher continua a ser do
consultor profissional – “Depende”. Depende do tipo de software a ser construído. Depende da função do código de
domínio. Depende da experiência da equipe. E, finalmente, depende de estar confortável com a decisão.

Espero, no entanto, que este livro tenha fornecido algumas centelhas de inspiração para ajudar na questão da
arquitetura. Se você tem uma história para contar sobre decisões de arquitetura, com ou sem Arquitetura Hexagonal,
eu adoraria ouvi-la.2

1 Caso você queira saber, tenho a personalidade do tipo “Defensor” e, se eu fosse um cachorro, aparentemente seria
um pitbull.
2 Contato: você pode me enviar um e-mail para tom@reflectoring.io.
Machine Translated by Google
Machine Translated by Google

Índice

A B
adaptadores feijão 94
23 acionados Desenvolvimento Orientado a
56 acionados Comportamento 72 grande
47 entrada 47 bola de lama 4, 116 grande design
saída 56 inicial (BDUF)

persistência 55 4 limites impondo


web 49 107 contexto limitado 60, 119-121
modelo de domínio anêmico apropriadamente acoplado 123-125
versus modelo de domínio rico 44 desacoplado 122, 123
contexto de aplicação 94 Teoria do Windows Quebrado 12, 99, 100
camada de aplicação 29, 30 artefato de construção

serviço de aplicação 23 114-116


limites de arquitetura 107-109 arquitetura/ construtor 6 padrão de construtor 40
lacuna de código 29
Registros de decisão de arquitetura (ADRs) 101
ArchUnit 111
C
montando 91, 92 via varredura de caminho de classe 94

código simples 93, 94 via Arquitetura Limpa 20-23

classpath do Spring varrendo 94-96 via Java Responsabilidade de consulta de comando


Config 96-98 do Spring autenticação Segregação (CQRS) 46
49 autorização 49 Separação de consulta de comando (CQS) 46
componente 129
Machine Translated by Google

140 Índice

arquitetura baseada em componentes 129 serviços de domínio 23, 29

convenções, aplicando 132, 133 regras linguagem específica de domínio (DSL) 113
130 não se repita (DRY) 6 adaptador
construtores 40, 41 acionado 27 adaptador
controladores acionador 27

fatiando 50-53
Criar, Ler, Atualizar e Excluir 63, 104
F
função de fitness 111
D
Estratégia de mapeamento completo

design orientado a banco de dados 86, 87 requisitos funcionais 1


10, 55 transações de banco de

dados 66 tomada de
H
decisão 6 injeção de dependência 6, 30,

31, 91 inversão de Arquitetura Hexagonal 22-24, 135, 136

dependência no adaptador de

persistência 55, 56 no adaptador web 48


EU

Princípio de Inversão de Dependência


(DIP) 19, 20, 48 imutável 40

Regra de dependência 21, 91, 109 portas de entrada


validar 111 ignorando 103
capacitação do desenvolvedor 4 entidades de

experiência do desenvolvedor domínio de modelos de entrada , usando


4 alegria do desenvolvedor 4 como 102, 103 para adaptador
Adaptador de Domínio 121 de persistência 56 para

estilos de arquitetura centrada em domínio 136 casos de uso 41, 42

Design Orientado a Domínio para adaptador


(DDD) 21, 33, 44, 119 web 50 porta de entrada
entidades de domínio 27 testes de integração 70 adaptador de

29 testes, com testes unitários persistência, testando com 75-77 adaptador web, testando com 73-75

71 usando, como modelo de entrada ou saída 102, Princípio de Segregação de Interface 58

103 camada de
domínio 9 modelo de domínio
J.
anêmico 44

comprometendo 66 API de persistência Java (JPA) 56

implementando 33-35 rico


44
Machine Translated by Google

Índice 141

eu Ó
arquitetura em camadas 9, 10 mapeamento objeto-relacional (ORM) 11, 22

camadas 9, 10 Estratégia de mapeamento unidirecional

camadas, 87, 88 organizando

apresenta dificuldade, trabalha em paralelo código


14, 15 difícil de testar arquitetonicamente
12, 13 oculta casos de uso 27 por recurso

13, 14 promove design orientado a banco de dados 26 por camada 25

10, 11 propenso a atalhos 12 modela entidades de domínio de saída ,


usando como 102, 103 para casos de uso 45

porta de saída 27
M
testes de

caminhos principais , com testes de


P
sistema 77-81 manutenibilidade pacote-privado 94, 109

1, 2, 135 tomada de decisão adaptador de persistência

6, 7 alegria do 55 inversão de dependência, aplicação de 55, 56

desenvolvedor 4, 5 responsabilidades 56

funcionalidade 2-4 fatiamento 58-60

manutenção 7, 8 testes, com testes de integração 75-77 camada

de persistência 9

mapeamento de montagem

estratégia completa de código simples via 93,

86 diretrizes 89 94 fatiamento de

sem interfaces de

mapeamento 66 unidirecional 87 bidirecional maneira 85 cenáriosporta 57,88, 89


de uso

Biblioteca Mockito 73 58 portas 23 saída 49

objetos simulados Arquitetura de portas e adaptadores 23

73 lacuna de modelo/ função de aptidão pós-compilação 111-113


código projeto

29 compartilhamento de modelos, entre casos começando limpo 100

de uso 101, 102 modularidade 128

P
N requisitos de qualidade 1, 2

Nenhuma estratégia de mapeamento consulta 45

84, 85 requisitos não funcionais 1


Machine Translated by Google

142 Índice

limite de transação 125


R
Estratégia de mapeamento bidirecional 85, 86

casos de uso somente leitura 45,

46 refatoração 116
você
modelo de domínio rico

versus modelo de domínio anêmico 44 testes unitários 70

entidade de domínio, teste com 71 casos

de uso, teste com 72, 73 vulnerabilidade


S
a mudanças estruturais 73
Serviços caso de uso 35

pulando 104 regras de negócios, validação de 42 a 44


atalhos 12 construtores 40, 41

Efeitos colaterais da Teoria do Windows modelos de entrada 41, 42

Quebrado 99 18, 19 validação de entrada 37 a 40

Princípio de Responsabilidade Única modelos de saída 45


(SRP) 17, 18, 45, 102 casos de uso somente leitura 45, 46

Princípios SOLID 17 algum Enviar dinheiro, caso de uso 35, 36

design inicial (SDUF) 4


Estrutura ESPAÇO 5
V
Controlador de mola 51

Exemplo Spring Data validação de

JPA 60-65 regras de negócios 35, 42 de

Montagem de varredura de classpath entrada 35-37

do Spring via 94-96 objetos de valor 41

Configuração Java do Spring modificadores de visibilidade 109-111

montagem via testes de

sistema 96-98 70
C
caminhos principais, testando com 77-81

camada de

aplicação do adaptador
T web 48 elementos de arquitetura

dívida técnica 99, 100 47 inversão de dependência, aplicação 48


Contêineres de teste 77 implementação 47

pirâmide de teste 69, 70 responsabilidades 49, 50


testes testes, com testes de integração 73-75 camada

testes de integração 70 web 9

testes de sistema 70
testes de unidade 70
Machine Translated by Google

www.packtpub.com

Assine nossa biblioteca digital on-line para ter acesso total a mais de 7.000 livros e vídeos, bem como ferramentas
líderes do setor para ajudá-lo a planejar seu desenvolvimento pessoal e avançar em sua carreira. Para mais
informações, visite nosso site.

Por que assinar?


• Gaste menos tempo aprendendo e mais tempo codificando com e-books e vídeos práticos de mais de 4.000
profissionais do setor

• Melhore seu aprendizado com Planos de Habilidades criados especialmente para você

• Receba um e-book ou vídeo grátis todo mês

• Totalmente pesquisável para fácil acesso a informações vitais

• Copiar e colar, imprimir e marcar conteúdo

Você sabia que a Packt oferece versões em e-book de todos os livros publicados, com arquivos PDF e ePub
disponíveis? Você pode atualizar para a versão do e-book em www.packtpub.com e, como cliente do livro
impresso, tem direito a um desconto na cópia do e-book. Entre em contato conosco em atendimento ao cliente@
packtpub.com para mais detalhes.

Em www.packtpub.com, você também pode ler uma coleção de artigos técnicos gratuitos, inscrever-se para receber uma
variedade de boletins informativos gratuitos e receber descontos e ofertas exclusivas em livros e e-books da Packt.
Machine Translated by Google

Outros livros que você pode gostar

Se você gostou deste livro, pode estar interessado nestes outros livros de Packt:

Arquitetura de software prática com Java

Giuseppe Bonocore

ISBN: 9781800207301

• Compreender a importância da engenharia de requisitos, incluindo requisitos funcionais versus requisitos


não funcionais

• Explorar técnicas de design, como design orientado a domínio, desenvolvimento orientado a testes (TDD),
e desenvolvimento orientado para o comportamento

• Descubra os mantras para selecionar os padrões arquitetônicos certos para aplicações modernas

• Explore diferentes padrões de integração

• Aprimorar aplicativos existentes com padrões essenciais nativos da nuvem e práticas recomendadas

• Abordar considerações transversais em aplicativos empresariais, independentemente das escolhas


arquitetônicas e do tipo de aplicativo
Machine Translated by Google

Outros livros que você pode gostar 145

Projetando Arquitetura Hexagonal com Java

David Vieira

ISBN: 9781801816489

• Descubra como montar algoritmos de regras de negócios usando o padrão de design de especificação

• Combine técnicas de design orientadas por domínio com princípios hexagonais para criar
modelos de domínio

• Empregar adaptadores para fazer com que o sistema suporte diferentes protocolos, como REST, gRPC,
e WebSocket

• Criar uma estrutura de módulo e pacote baseada em princípios hexagonais

• Use módulos Java para impor a inversão de dependência e garantir o isolamento entre
componentes de software

• Implementar Quarkus DI para gerenciar o ciclo de vida das portas de entrada e saída
Machine Translated by Google

146

Packt está procurando autores como você

Se você estiver interessado em se tornar um autor da Packt, visiteauthors.packtpub.com e inscreva-se hoje.


Trabalhamos com milhares de desenvolvedores e profissionais de tecnologia, assim como você, para ajudá-
los a compartilhar suas ideias com a comunidade global de tecnologia. Você pode fazer uma inscrição geral,
candidatar-se a um tópico específico para o qual estamos recrutando um autor ou enviar sua própria ideia.

Compartilhe seus pensamentos

Agora que você terminou Suja as mãos na arquitetura limpa – segunda edição, adoraríamos ouvir sua
opinião! Se você comprou o livro na Amazon, clique aqui para ir direto para a resenha da Amazon
página deste livro e compartilhe seus comentários ou deixe um comentário no site onde você o comprou.

Sua avaliação é importante para nós e para a comunidade de tecnologia e nos ajudará a garantir que estamos entregando
conteúdo de excelente qualidade.
Machine Translated by Google

147

Baixe uma cópia gratuita em PDF deste livro


Obrigado por adquirir este livro!

Você gosta de ler em qualquer lugar, mas não consegue levar seus livros impressos para qualquer lugar?

A compra do seu e-book não é compatível com o dispositivo de sua escolha?

Não se preocupe, agora com cada livro Packt você obtém uma versão em PDF sem DRM desse livro, sem nenhum custo.

Leia em qualquer lugar, em qualquer lugar, em qualquer dispositivo. Pesquise, copie e cole códigos de seus livros técnicos
favoritos diretamente em seu aplicativo.

As vantagens não param por aí, você pode obter acesso exclusivo a descontos, newsletters e ótimo conteúdo gratuito em sua
caixa de entrada diariamente

Siga estas etapas simples para obter os benefícios:

1. Digitalize o código QR ou visite o link abaixo

https://packt.link/free-ebook/9781805128373

2. Envie seu comprovante de compra. 3.

Pronto! Enviaremos seu PDF grátis e outros benefícios diretamente para seu e-mail

Você também pode gostar