Você está na página 1de 117

Arquitetura Limpa na Prática

Otávio Lemos
Este livro está à venda em https://pay.hotmart.com/O59619511K
Esta versão foi publicada em 1 de Dezembro de 2021.
Copyright © 2021-2022 - Otávio Lemos | Todos os direitos reservados.
Todos os textos, imagens, gráficos e outros materiais são protegidos por
direitos autorais e outros direitos de propriedade intelectual pertencen-
tes ao autor.
Dedicado à minha esposa maravilhosa, Ana Luísa, a quem amo muito, e também
devo por todo o apoio e ideias geniais, inclusive no âmbito profissional, e aos nossos
filhos espetaculares: José, Luís, Olívia e Lucas.

i
Agradecimentos
Muita gente me ajudou em minha jornada acadêmica que culmina agora na escrita
deste livro. Gostaria de agradecer primeiramente a meus pais, que me mostraram
como ter uma vida verdadeiramente entregue ao serviço aos outros e ao trabalho de-
dicado. Também agradeço o meu orientador de mestrado e doutorado na USP, o
Prof. Paulo Cesar Masiero, que me mostrou também como ser uma pessoa ordenada
e dedicada ao trabalho. Agradeço também à Profa. Cristina Lopes, excelente pesqui-
sadora e exemplo de profissional da área tecnológica com contribuição em diversas
áreas, não somente acadêmicas, e que me recebeu tantas vezes na Universidade da
Califórnia em Irvine, lugar que acabei considerando uma segunda casa.
Agradeço também ao Rodrigo Manguinho, primeiro profissional da área de desen-
volvimento de software que observei aplicando várias das ideias que eu tinha estu-
dado somente teoricamente. Por fim, agradeço às minhas referências da área de enge-
nharia de software: Martin Fowler, Fred Brooks, Kent Beck, Dave Farley, entre outros
e, principalmente, Robert Martin, que me inspirou enormemente nessa nova fase de
minha carreira profissional e inclusive me concedeu um bate-papo em meu canal do
YouTube.
Por fim, mas não menos importante, como diria Brooks: D.O.G.!

ii
Palestra na Carnegie Mellon University (2018)

Sobre o autor
Otávio Lemos é desenvolvedor, pesquisador e professor de computação. Leciona há
mais de dez anos na Universidade Federal de São Paulo (UNIFESP), tem mestrado e
doutorado pela Universidade de São Paulo (USP), e fez pós-doutorado na Universi-
dade da Califórnia em Irvine (UCI).
Na UCI, onde figura como pesquisador associado, trabalhou em diferentes períodos
perfazendo aproximadamente dois anos. Já apresentou seu trabalho de pesquisa no
Google, na Carnegie Mellon University (CMU) e na UCI, além de em inúmeros con-
gressos internacionais.
Em 2019 iniciou seu trabalho na Internet com um canal pessoal no YouTube que
cresceu rapidamente, alcançando mais de 20.000 inscritos. Esse trabalho representa
sua aproximação com a indústria de desenvolvimento de software, tópico pelo qual é
apaixonado e procura se especializar cada vez mais. Em 2021 esse trabalho foi reconhe-
cido pela Microsoft, tendo recebido o título de Most Valuable Professional (MVP) da
empresa.
Nesse novo período, aliado com seu trabalho universitário, tem realizado workshops,
cursos, treinamentos e sessões de consultoria junto a empresas e startups de tecnolo-
gia.
Aquele que lê muito e entende muito, recebe o seu preenchimento.
Aquele que está cheio, refresca outros.

— Ambrósio
Prefácio
Arquiteturas bem projetadas são fundamentais para a manutenção e a evolução de
sistemas de software de maneira sustentável. E, da minha perspectiva, ponto final.
Mas isso não parece ser mais uma verdade absoluta e clara na cabeça de todas as de-
senvolvedoras e desenvolvedores de software. Sempre achei curioso o movimento do
“faça o mais simples que puder, e melhore depois”. Acho, na verdade, tão curioso
que gastarei boa parte deste prefácio dissecando essa recomendação.
Fazer o mais simples possível foi uma resposta às arquiteturas que eram desnecessa-
riamente complexas. Como exemplo, pense no mundo Java, ainda na época da Sun,
e o seu catálogo de padrões J2EE. Grande parte dos padrões ali serviam para ajudar
o desenvolvedor a distribuir sua aplicação em dezenas de máquinas e a escalar para
milhares de usuários. Claramente, são poucas as aplicações que realmente precisam
disso. No entanto, muitas empresas adotaram os padrões como se eles fossem boas
práticas para qualquer sistema. A consequência foi sim sistemas muito mais comple-
xos do que o necessário.
A lição aprendida foi por sempre optar por soluções simples, e só adicionar comple-
xidade caso necessário. Concordo em absoluto. Mas talvez agora estejamos errando a
mão para o outro lado, e criando sistemas de software com arquiteturas não simples,
mas simplórias. Tão simplórias que, no momento em que qualquer evolução mais
significativa tenha que acontecer, a pessoa precisa de uma energia significativa, talvez
tanta quanta ela precisaria gastar para mudar o sistema super complexo do passado.
O que me agrada da ideia da Clean Architecture (Arquitetura Limpa) e de tantas ou-
tras por aí, é que elas são bastante pé no chão. Algumas decisões de projeto e de arqui-
tetura são fáceis de serem tomadas, não consomem muito tempo de implementação
e de compreensão, e ajudarão a pessoa no futuro com certeza. Por exemplo, separar
código de infraestrutura de regras de negócio. A implementação é trivial e pode ser
sumarizada em uma frase: ponha código de infraestrutura em um lugar e regras de

vi
negócio em outro. Sim, você precisará de uma classe a mais, mas seja honesto, isso
é realmente um problema? Ou, mantenha seu controlador coeso e faça com que ele
apenas delegue comportamento para classes de negócio. Sim, ao invés de ter tudo im-
plementado em um só método, você precisará dividir as responsabilidades. Puxa, é
só um pouquinho de nada a mais de trabalho, mas que trará diversos benefícios para
sua base de código. Quais? Leia este livro!
A bem da verdade é que, por baixo dos panos, a arquitetura limpa apenas pede para
que a pessoa separe bem as responsabilidades, escreva classes coesas e encapsuladas, e
reflita bastante sobre o que pode depender do que. “Ah, mas puxa vida, ela diz que eu
preciso isolar meu banco de dados porque um dia o meu banco de dados vai mudar,
mas ele nunca muda!” Sim, concordo com você, bancos de dados raramente mudam.
No entanto, isolar o banco de dados do resto do sistema não vai só garantir que ele
possa mudar, o que pra mim também é o menor dos benefícios, mas sim em garantir
que o resto do seu código não “se suje” com detalhes que não interessam para aquela
parte. Você não quer que detalhes de cache estejam espalhados por toda sua base de
código. Encapsulamento é um conceito muito bem definido em projeto de sistemas
orientados a objetos, e é isso que está acontecendo aqui. Não é o Robert Martin que
está dizendo que isso é bom, mas sim anos de trabalhos na área.
Este livro, escrito pelo Otávio, apresenta de forma clara, pragmática e ilustrada como
se implementar uma arquitetura limpa, do começo ao fim, em um exemplo simples
e fácil de ser seguido. É uma ótima introdução para aqueles que ainda não tiveram
tempo de ler a fonte oficial do assunto. E que sim, deve ser lida após esse livro também.
Boa leitura a todos e, mais importante, comecem a projetar arquiteturas limpas!
E sim, em meus quase 20 anos como desenvolvedor, eu nunca precisei trocar de banco
de dados. Mas já perdi a conta de quantas vezes precisei trocar um componente inteiro
do sistema, porque foi implementado com tecnologia velha, ou porque foi simples-
mente bem implementado, ou por qualquer outro motivo. Trocar o componente
foi uma tarefa fácil quando a arquitetura era limpa; quando a arquitetura não era tão
limpa assim, pagamos mais do que deveríamos. A mentalidade de “isso nunca vai mu-
dar” pode ser tão perigosa quanto a “vou deixar isso totalmente flexível do começo”.
Maurício Aniche
Professor assistente, TU Delft, Holanda
Tech Academy Lead, Adyen
http://effective-software-testing.com
Conteúdo

1 Introdução 1

2 Arquitetura de Software 7

3 A Arquitetura Limpa 18

4 Tecnologia e padrões adotados 33

5 Estudo de Caso: theWisePad 42

6 Entidades 45

7 Casos de Uso 54

8 Adaptadores de Interface 69

9 Frameworks & Drivers 81

10 Principal & Configuração 95

11 Conclusão 104

viii
1 | Introdução

m 2019 eu lecionava uma disciplina de Programação Orientada a Objetos na UNI-


FESP e pensei que seria importante implementar junto com os alunos um pe-
queno projeto para colocar em prática os conceitos aprendidos em sala. Afinal
de contas, nada melhor do que por a mão na massa para aprender de fato conceitos
teóricos. Imaginei que um sistema simples de Bloco de Notas seria o ideal: algo nem
tão pequeno que fosse desinteressante e muito distante de um sistema real, nem tão
grande que fosse impossível de implementar em um semestre.
No fim das contas conseguimos implementar algo razoavelmente bom: o sistema ro-
dava e funcionava. Mas a verdade é que eu senti que mesmo com mestrado e dou-
torado na área de Engenharia de Software, eu tinha dificuldade em estruturar um
sistema de software real de maneira satisfatória. É claro que eu conhecia muitos con-
ceitos importantes de como se desenvolve software e que tinha uma boa noção de
modelagem orientada a objetos. Eu também tinha familiaridade com o MVC e ou-
tros padrões de projeto, com o TDD e outras boas práticas, mas não sabia de fato
como estruturar um sistema de maneira efetiva.
Juntamente com esse incômodo de me dar conta de que tinha pouca experiência com
a estruturação de um sistema de software real, na mesma época eu começava meu ca-
nal no YouTube1 , como uma oportunidade de me aproximar do mercado e também
de ensinar o que eu sabia sobre desenvolvimento de software. Foi aí que iniciei uma
jornada muito importante na minha carreira: comecei a me aprofundar em como
desenvolver software da melhor maneira, com boas práticas e considerando a arqui-
tetura do sistema a sério, sempre aplicando as ideias em projetos de software reais
(ainda que pessoais).
1
Otavio Lemos – http://www.youtube.com/otavioallemos

1
A verdade é que ali eu começava a por em prática tudo o que havia lido nos livros
de Engenharia de Software que eu mais gostava (incluindo os clássicos Programação
Extrema Explicada (Beck e Andres, 2004) e TDD: Desenvolvimento Guiado por Tes-
tes (Beck, 2002), ambos do Kent Beck; o primeiro com Cynthia Andres, sua esposa,
como co-autora). Essa viagem me levou logo a Robert C. Martin: um famoso desen-
volvedor de software com décadas de experiência em desenvolvimento, vários livros
escritos na área, e ideias que eu considerava essenciais para se desenvolver software de
maneira mais profissional.
Foi nessa época que eu me deparei com o conceito de Arquitetura Limpa, cunhado
pelo Robert Martin. Parecia-me algo que preenchia a lacuna que senti ao tentar de-
senvolver aquele Bloco de Notas na disciplina de Programação Orientada a Objetos.
A verdade é que o conceito não era nada novo: podemos ver o Robert Martin falando
sobre a Arquitetura Limpa há muito tempo em vídeos no YouTube. Entretanto, a
ideia começou a ser mais popularizada no começo dos anos 2020.
De fato, a Arquitetura Limpa, como reconhecido pelo próprio autor, não é nada mais
nada menos do que um apanhado de ideias desenvolvidas nas últimas décadas dentro
do assunto de Arquitetura de Software. Essas ideias incluem a Arquitetura Hexago-
nal (Cockburn, 2005) (também conhecida por Portas e Adaptadores), desenvolvida
por Alistair Cockburn e adotada por Steve Freeman e Nat Pryce no livro Growing
Object-Oriented Software Guided by Tests (Freeman e Pryce, 2009) (aliás, livro essen-
cial para se entender a escola de Londres do TDD); o DCI (Coplien e Reenskaug,
2012) (Data, Context, e Interaction; ou Dados, Contexto e Integração), desenvolvido
por James Coplien e Trygve Reenskaug (inventor também do padrão MVC); e o BCE
(Boundary, Control, e Entity; ou Fronteira, Controle e Entidade), desenvolvido por
Ivar Jacobson (um dos pioneiros também da UML) no seu livro Object-Oriented Soft-
ware Engineering: a Use-Case Driven Approach (Jacobson, 2004).
Essas arquiteturas variam em seus detalhes mas são muito semelhantes na sua essência.
Todas têm o mesmo objetivo: a separação de interesses; e atingem esse objetivo divi-
dindo o software em camadas. Desde então tenho estudado esses tipos de arquitetura
e aplicado seus conceitos na prática.
A principal referência sobre o assunto de Arquitetura Limpa é o livro de mesmo nome
do Robert Martin (Martin, 2017). Logo que entrei em contato com o tema, adquiri
o livro e estudei-o, procurando colocar as ideias em prática em projetos pessoais. A
verdade é que o livro é excelente: cobre muitos assuntos essenciais sobre Arquitetura
de Software. Por outro lado, muitos dos que o lêem sentem falta de um exemplo real
com código-fonte, pois os conceitos são explicados de maneira mais genérica, sem
uma linguagem de programação alvo. Essa é uma queixa constante de alguns que me
procuram para tirar dúvidas sobre o assunto: apesar das ideias gerais serem essenciais
e aplicarem-se a quaisquer linguagens e ambientes, às vezes é difícil entender alguns
conceitos sem um exemplo escrito em uma linguagem específica2 .
Para resolver esse problema e ajudar os desenvolvedores a se familiarizarem com o
conceito de Arquitetura Limpa e Arquitetura de Software em geral de uma maneira
mais mão-na-massa, resolvi escrever este livro. A ideia é explicar todos os conceitos
por meio de um exemplo, parecido com o livro seminal de TDD do Kent Beck (que
em inglês se chama Test-Driven Development by Example (Beck, 2002)). De fato,
ajuda muito a concretizar as ideias quando se vê um exemplo real com código-fonte.
A linguagem adotada neste livro é o TypeScript (TS). Escolhi o TS porque é a lin-
guagem que atualmente estou estudando e utilizando para por em prática todos os
conceitos que aprendo sobre desenvolvimento de software e, em particular, desen-
volvimento Web. O TS também está em plena ascensão no mercado, atraindo muitos
desenvolvedores JavaScript, buscando uma maneira mais disciplinada de se desenvol-
ver — com tipagem estática — e com caraterísticas importantes da programação ori-
entada a objetos (como, por exemplo, interfaces). Apesar dos exemplos estarem em
TS, qualquer pessoa com conhecimento básico de Programação Orientada a Obje-
tos e familiaridade com qualquer outra linguagem OO (como Java e C#) conseguirá
acompanhar o desenvolvimento das ideias.
A organização do livro é simples. No segundo e no terceiro capítulo, Arquitetura de
Software e Arquitetura Limpa (AL), são delineados os conceitos essenciais da área
juntamente com as ideias-chave da AL. A principal ideia do livro é explicar os concei-
tos por meio de um exemplo, entretanto achei por bem resumir as ideias principais
para que se pudesse seguir o exemplo com uma base inicial bem formada. Além da
Arquitetura Limpa, resumo também as outras arquiteturas que a embasaram: a Ar-
quitetura Hexagonal, o DCI e o BCE.
No quarto capítulo, delineio as tecnologias e padrões utilizados para a implementação
do exemplo, principalmente a linguagem de programação TypeScript.
No quinto capítulo, Estudo de Caso: theWisePad, é descrito o exemplo utilizado, um
backend de uma aplicação de Bloco de Notas (uma espécie de Notes da Apple ou um
Notion bem simplificado) desenvolvida como uma API REST (que não afirmo ser
2
Aproveito para agradecer a Profa. Rosana Braga do ICMC-USP que me deu a ideia de escrever este livro em uma aula
que dei para seus alunos.
RESTful (Fielding e Taylor, 2000)) em TS e Node.js. Apesar de ser uma aplicação
Web, como ficará claro, o cerne da aplicação é independente de qualquer tecnologia,
inclusive da própria Web.
A partir daí, percorro cada camada da AL com a implementação do exemplo em cada
uma delas. No sexto capítulo começo pelo centro da aplicação: a modelagem do do-
mínio com seus dados e regras de negócio críticos na camada de Entidades; no sétimo
capítulo são implementados os Casos de Uso da aplicação; no oitavo capítulo trato
dos Adaptadores de Interface que conectam as políticas do sistema com os detalhes
de implementação; e no capítulo nono trato das implementações da periferia do sis-
tema: os Frameworks e Drivers. No décimo capítulo trato da camada Principal e de
Configuração, com o ‘sujo’ main, conhecedora tanto das políticas quanto dos deta-
lhes do sistema e que faz todas as conexões necessárias para a execução do programa;
e no capítulo onze concluo o livro.
Julgo necessário um breve comentário sobre o estilo adotado. Não é um livro acadê-
mico, cheio de citações e referências bibliográficas: é um livro prático, para ajudar na
aplicação da Arquitetura Limpa. Por outro lado, sou acadêmico por formação e, as-
sim sendo, é difícil ser 100% pragmático. Assim, em alguns momentos me aprofundei
em conceitos para que ficassem mais claros (principalmente nos capítulo Arquitetura
de Software e Arquitetura Limpa). Por outro lado, preferi um estilo mais informal,
com citações mais soltas ao longo do texto, utilizando primeira pessoa algumas vezes,
para tornar a leitura mais agradável. No que diz respeito à listagem de código eu op-
tei pela beleza sobre a praticidade: os snippets são imagens e, portanto, não podem
ser copiados. Espero que esse problema seja minimizado com o acesso que pretendo
dá-lo ao repositório GitHub do código da aplicação exemplo.
Um aviso importante aos leitores: este livro representa a minha interpretação da Ar-
quitetura Limpa e não estou argumentando que seja a única, nem a melhor. O que
eu garanto é que estudei e apliquei a ideia por aproximadamente dois anos procu-
rando me aprofundar cada vez mais (e continuo me aprofundando). Outro detalhe
importante é que o código-fonte provavelmente não está perfeito — algo que julgo ser
impossível — e que provavelmente continuará passando por refatorações contínuas
(o que deve acontecer com todo sistema que se preze) e possivelmente algum debug-
ging (esse último espero que não frequentemente). De fato, o exemplo representaria
um primeiro MVP bem arquitetado de uma aplicação, para ser colocado em produ-
ção e continuar sendo progressivamente evoluído. De qualquer maneira, esforcei-me
para deixar o código o mais limpo possível segundo minha visão. De fato, desculpe-
me o termo mas, ultimamente, tento escrever código que qualquer idiota consegue
entender. Isso porque amanhã, certamente, o idiota serei eu.
Desejo que este livro seja útil para que você possa ter um conhecimento sólido sobre
Arquitetura Limpa e Arquitetura de Software em geral e possa aplicar esse conheci-
mento no seu dia-a-dia como desenvolvedor(a) de software.
Qualquer dúvida, não hesite em me contatar e boa leitura!

Aviso aos Navegantes


Antes de entrar no tema do livro, gostaria de deixar um aviso importante aqui, discu-
tindo o que a Arquitetura Limpa não é, antes de começar a dizer o que ela é.

1. A Arquitetura Limpa não é uma bala de prata. Que fique claro: as ideias conti-
das nesse livro não são a solução para todos os problemas do desenvolvimento
de software. De fato, Robert Martin chama a Arquitetura Limpa de uma idéia
prática (actionable idea). Como toda ideia, ela pode — e muitas vezes deve —
ser adaptada ao seu contexto; não deve ser utilizada cegamente, como uma so-
lução genérica para tudo. De fato, a Arquitetura Limpa coloca um conjunto
de princípios que guiam o desenho de uma arquitetura. E, para falar a verdade,
esses princípios são tão sensatos que muitas vezes se poderia chamar a Arqui-
tetura Limpa simplesmente de Arquitetura. Por outro lado é preciso avaliar a
necessidade de se estruturar o seu sistema assim. Por exemplo, se o sistema é
muito pequeno — tem menos de 5000 linhas —, talvez o uso da Arquitetura
Limpa pode atrapalhar em vez de ajudar.

2. A Arquitetura Limpa não elimina as dores de cabeça na manutenção. Sim,


como será visto neste livro, o objetivo da Arquitetura Limpa — e, aliás, de toda
boa arquitetura — é diminuir as dores de cabeça ao realizar alterações em um
sistema, ao torná-lo mais maleável. Isso não quer dizer que toda a vez que, por
exemplo, você migrar de tecnologia, isso será feito sem nenhum suor, do dia
para a noite. De fato, quando se deixa o cerne do sistema mais independente
de detalhes de implementação — como será visto mais à frente —, o esforço de
alteração é diminuído; isso não quer dizer que ele seja eliminado. Um exem-
plo seria trocar a tecnologia de banco de dados em um dado momento. É fato
que se você desenvolve utilizando repositórios mais abstratos, esse tipo de mu-
dança é facilitado. Mas isso não significa que não haverá nenhum esforço para
se adaptar a um novo tipo de tecnologia.

3. A Arquitetura Limpa não é encher o sistema de interfaces. Existe um cargocul-


tismo que se resume a exatamente isso: encher o sistema de interfaces e nunca
fazer uma chamada direta a uma operação. Isso é overengineering: pode deixar
o sistema muito mais difícil de entender! Como será verificado no exemplo
utilizado neste livro, não utilizo interfaces a torto e a direito: existe um trade-
off importante. É necessário avaliar se o ganho de flexibilidade com a adição de
uma interface compensa a complexidade adicional de se ter um nível a mais de
abstração. Isso é muito importante de se ter em mente. Repare que o princi-
pal uso de interfaces é quando se atravessa uma fronteira arquitetural, ou seja,
quando existem chamadas que vão de uma camada da arquitetura para outra.
Em particular, dentro de uma mesma camada não há necessidade de se utilizar
indireções.

Estando claros esses pontos, podemos entrar no tema do livro tranquilos sabendo o
que a Arquitetura Limpa não é para poder entender corretamente o que ela de fato
é.
2 | Arquitetura de Software
que é Arquitetura de Software? Por que ela é importante? Esses dias eu li um post
em um blog de um desenvolvedor profissional que falava exatamente sobre isso1 .
Chris Kiehl é desenvolvedor na Amazon há mais de seis anos e nesse post relatava
tópicos nos quais mudou de ideia durante esse período. Um deles é justamente a
arquitetura de software (uma evidência de que o tema muitas vezes é deixado de lado).
Ele afirma o seguinte:
“A arquitetura de software provavelmente importa mais do que qualquer outra coisa.
Uma implementação ruim de uma boa abstração não causa nenhum dano substancial
à base de código. Mas uma abstração ruim ou camada faltando causa o apodrecimento
de tudo.”
E é disso que se trata esse livro: explicar, por meio de um exemplo, uma ideia prática
de como desenvolver boas abstrações e dividir o sistema em camadas — a Arquite-
tura Limpa. De fato, se pensarmos bem, arquitetura e design ruins são piores do que
bugs. Isso porque se conseguimos nos localizar bem em uma base de código, ou seja,
se temos uma boa arquitetura, mesmo que existam bugs ali, eventualmente vamos
encontrá-los. Porém, se não conseguimos nem entender a bagunça em que estamos,
como poderemos alterar o sistema, mesmo que seja para remover um bug?
Antes de entrar na implementação das ideias da Arquitetura Limpa por meio de um
exemplo, é importante definir o que se entende por Arquitetura de Software neste
livro e o que é a Arquitetura Limpa. Neste capítulo são definidos termos importantes
relacionados com a Arquitetura de Software. Em particular, são comentadas algumas
diferentes abordagens utilizadas para sua definição.
Software development topics I’ve changed my mind on after 6 years in the industry (Chris Kiehl) – https://
1

chriskiehl.com/article/thoughts-after-6-years

7
O que é Arquitetura de Software?
Arquitetura de software é um termo difícil de se definir. Em um artigo para a IEEE
Software (Fowler, 2003b), Martin Fowler recolhe a seguinte definição de Ralph John-
son:
“Na maioria dos projetos de software de sucesso, os desenvolvedores mais experien-
tes trabalhando naquele projeto têm um entendimento compartilhado do projeto do
sistema [projeto entendido aqui no sentido de design]. Esse entendimento compar-
tilhado é chamado de ‘arquitetura’.
Esse entendimento inclui como o sistema é dividido em componentes e como os com-
ponentes interagem por meio de interfaces. Esses componentes geralmente são com-
postos por componentes menores, mas a arquitetura inclui somente os componentes
e interfaces que são entendidos por todos os desenvolvedores”.
É uma boa definição. Entretanto repare que há alguns pontos imprecisos. Por exem-
plo, o que é ‘componente’? E por que os componentes menores não estão incluídos
na arquitetura?
De fato, é difícil definir arquitetura de software de uma maneira precisa. De qualquer
maneira é fácil intuir o que seja a arquitetura de um sistema e defini-la da maneira
mais precisa possível. De maneira geral gosto de imaginar a arquitetura como uma
visão geral do sistema a partir de um zoom out que permite enxergar a sua estrutura:
os componentes maiores que o compõem e como esses componentes se comunicam.
Componente refere-se a um conjunto de módulos; e módulo refere-se a um agregado
de funções e dados (em uma linguagem orientada a objetos o módulo pode é uma
classe com seus métodos — funções — e dados — atributos; em uma linguagem não
orientada a objetos pode ser um arquivo de código-fonte com funções e dados relaci-
onados).
Parece-me também que se pode considerar que a arquitetura possui dois aspectos
principais: a sua estrutura e a sua infraestrutura. A estrutura equivale ao projeto dos
componentes e módulos — o design — do sistema; e a infraestrutura é composta por
tudo aquilo que apóia o projeto (os detalhes mais concretos de implementação, in-
cluindo as tecnologias e serviços necessários para dar vida ao sistema).
Abordagem Contemporânea e mais Geral

Em Fundamentals of Software Architecture (Richards e Ford, 2020), Mark Richards e


Neal Ford consideram quatro aspectos que se combinam para formar a arquitetura de
software: (1) a sua estrutura; (2) as suas características; (3) as suas decisões de projeto;
e (3) os seus princípios de projeto.
A estrutura consiste no estilo arquitetural utilizado no sistema (por exemplo, micros-
serviços, arquitetura em camadas ou microkernel). Esse aspecto está mais relacionado
com a topologia do sistema, ou seja, em como os componentes que o formam são dis-
tribuídos.
O segundo aspecto inclui as características do sistema, como quão escalável ele é (sua
escalabilidade), quão confiável ele é (sua confiabilidade), quão disponível ele é (sua
disponibilidade), quão testável ele é (sua testabilidade), etc.
O terceiro aspecto inclui as decisões arquitetônicas do projeto (por exemplo, que so-
mente as camadas de negócio e serviço se comunicarão com o banco de dados em uma
arquitetura em camadas).
O quarto aspecto refere-se aos princípios de projeto adotados para guiar o desenvol-
vimento da arquitetura (por exemplo, os arquitetos podem decidir que as equipes de
desenvolvimento deverão utilizar mensagens assíncronas entre serviços em um arqui-
tetura de microsserviços para melhorar a sua performance).
Outra visão importante da Arquitetura de Software é a sua relação com a organização
da empresa onde o software é desenvolvido. De acordo com a Lei de Conway desco-
berta em 1967, qualquer empresa que desenvolva um sistema produzirá um projeto
cuja estrutura é uma cópia da estrutura de comunicação da organização. Isso quer
dizer que, de maneira geral, haverá um mapeamento entre os componentes maiores
do sistema e as equipes de desenvolvimento.
É interessante observar que outro trabalho seminal sobre projeto de software de 1972
(!), On the Criteria to be Used for Decomposing Systems into Modules de Parnas
(1972), um dos primeiros trabalhos sobre ocultamento de informação — information
hiding — já colocava como objetivo da decomposição do sistema em componentes o
desenvolvimento independente de cada componente pelas diferentes equipes de de-
senvolvimento. Quando somente ficam expostas as interfaces mais abstratas de cada
componente, com os detalhes de implementação escondidos, cada equipe pode focar
no desenvolvimento de seu componente sem a necessidade de conhecer detalhes da
Figura 2.1: Os quatro aspectos da Arquitetura de Software segundo Richards e Ford
(2020) (adaptado da mesma obra).

implementação de outros componentes com os quais se comunica.


Mais recentemente, com o advento das arquiteturas estruturadas sobre microsservi-
ços — que, a propósito, têm também como um dos objetivos principais o desenvolvi-
mento independente dos microsserviços por equipes diversas —, foi cunhado o termo
Manobra Invertida de Conway2 , que recomenda evoluir a equipe e a estrutura organi-
zacional para promover a arquitetura desejada. A ideia é simples: já que a arquitetura
tenderá a refletir a divisão das equipes, antecipe-se e divida as equipes de acordo com
a arquitetura desejada.
Essa abordagem mais organizacional à Arquitetura de Software é importante pois re-
vela que o papel do arquiteto de software pode também incluir o auxílio na organi-
zação das equipes de desenvolvimento.
2
Inverse Conway Maneuver – https://www.thoughtworks.com/pt/radar/techniques/
inverse-conway-maneuver
Abordagem Acadêmica

Uma visão mais acadêmica da Arquitetura de Software pode ser encontrada em Soft-
ware Architecture Foundations, Theory, and Practice de Taylor et al. (2009)3 . Neste
livro-texto a Arquitetura de Software é definida como o conjunto de decisões de projeto
principais que governam um sistema.
Apesar de apresentarem uma visão mais acadêmica do tema, os autores fazem questão
de frisar que a Arquitetura de Software não pode ser um conceito divorciado da im-
plementação, e deve ajudar os desenvolvedores a criar sistemas reais. Uma abordagem
a esse tema que fosse desconectada da implementação, implantação e adaptações de
longo prazo arriscaria ser trabalho irrelevante, algo que poderia ser tolerado mas não
valorizado.

Abordagem Essencial

O problema das abordagens mais gerais à arquitetura de software é que elas tendem a
abarcar muitos aspectos do desenvolvimento de software. De fato, ao se ler um livro
mais generalista desse tema tem-se a impressão de que ele trata de quase tudo o que
diz respeito à engenharia de software.
Robert Martin tem uma visão mais simples e essencial da Arquitetura de Software.
Basicamente, para ele, a Arquitetura de Software é equivalente ao Projeto de Software
(ou seja, em Inglês, para ficar mais preciso: software architecture = software design). De
fato, quando consideramos plantas arquitetônicas de outros tipos de obras humanas
— tomando aqui a arquitetura de uma maneira mais ampla — percebemos que essas
plantas ou projetos também incluem detalhes de mais baixo nível de como a obra será
constituída, e não somente características de alto nível. Por exemplo, quando exami-
namos um projeto arquitetônico de uma casa, podemos notar que ali estão também
detalhes de onde vai cada um dos soquetes das lâmpadas, cada tomada e cada inter-
ruptor. Ou seja, o projeto arquitetural também inclui detalhes de baixo nível.
Por outro lado, parece-me importante frisar que apesar de existir uma equivalência
entre projeto e arquitetura de software, o último foca em olhar para o sistema de ma-
neira mais geral, como um todo, enquanto que o primeiro foca nos melindres mais
3
Uma curiosidade: o James Taylor foi orientador do Thomas Fielding na tese de doutorado que deu origem ao estilo
arquitetural REST (Fielding e Taylor, 2000) na University of California, Irvine — UCI (mesmo lugar em que fiz meu
doutorado sanduíche e pós-doutorado).
internos de como se projetar e estruturar um módulo — ou classe — do sistema (pen-
sando em seus dados e funções principais) e como esses módulos devem colaborar.
De fato, ao se investigar um título clássico de projeto de software — como Object
Design: Roles, Responsibilities, and Collaborations de Wirfs-Brock et al. (2002) ou
Design Patterns: Elements of Reusable Object-Oriented Software de Gamma et al.
(1995) — percebe-se que a preocupação principal é muito mais aprofundada nas clas-
ses e objetos do que nas camadas mais gerais do sistema. Os títulos de Arquitetura de
Software focam no zoom out comentado acima, em particular tratando das principais
camadas e estilos arquitetônicos utilizados nos sistemas.
Para seguirmos uma definição-padrão neste livro, fiquemos com a seguinte, de Ro-
bert Martin: a Arquitetura de Software é a forma dada ao sistema por aqueles que o
constroem. Essa forma é a divisão do sistema em componentes, o arranjo desses com-
ponentes e a maneira pela qual esses componentes se comunicam.
Sendo assim, a ênfase deste livro é na parte estrutural da arquitetura, em oposição à
sua infraestrutura. De fato, a Arquitetura Limpa apresenta recomendações de como
dividir um sistema em camadas, sem se preocupar com detalhes de implementação
relacionados com a sua infraestrutura.
Por outro lado deve-se ter em conta que uma arquitetura bem projetada certamente
afeta positivamente outros aspectos do desenvolvimento de software, desde a estru-
tura organizacional das equipes — como comentado anteriormente — até as pro-
priedades do sistema, como testabilidade e performance. O impacto do projeto na
testabilidade é fácil de ver mas o impacto na performance nem tanto. Mas, veja só: se
a arquitetura do sistema é ruim, por exemplo tem uma camada faltando — como o
exemplo de Chris Kiehl dado no começo do capítulo — é difícil saber até onde atuar
para melhorar a performance. Isso certamente vale também para outros requisitos
não-funcionais do sistema.

Qual é o objetivo da Arquitetura de Software?


No livro Agile Software Architecture, Coplien e Reenskaug (2013) citam que o ar-
quiteto romano Vitruvius resumia os interesses da arquitetura em três: utilitas (uti-
lidade), firmitas (solidez) e venustas (beleza). Certamente esses três interesses se apli-
cam também à arquitetura de software. A utilidade é condição sine qua non de qual-
quer sistema; a solidez está ligada a manutenibilidade — ou facilidade de alterar —
do sistema (ou seja, ele deve funcionar por um longo período de tempo, mesmo com
alterações nos requisitos); e a beleza está muito relacionada com a maneira como pro-
jetamos as interfaces do sistema. De fato, nos anos 1970 David Parnas já se dava conta
de que “a diferença entre os projetos que julgava bonitos daqueles que julgava feios
era que os projetos bonitos encapsulavam as informações maleáveis ou arbitrárias do
sistema”.
Nessa mesma linha, de acordo com Robert Martin, o propósito da arquitetura de soft-
ware é facilitar o desenvolvimento, implantação, operação e manutenção do sistema
construído a partir dela. A estratégia então é deixar o máximo possível de opções em
aberto, pelo máximo de tempo possível. Dessa maneira atinge-se o objetivo princi-
pal da arquitetura de software segundo o mesmo autor: minimizar os recursos hu-
manos necessários para construir e manter o sistema. De fato, se a forma do sistema
permite que ele seja mais facilmente alterado, os recursos humanos necessários para
desenvolvê-lo, implantá-lo, operá-lo e mantê-lo serão minimizados.
É a diferença entre hardware e software. O hardware — hard significa duro em Por-
tuguês — é algo que não se modifica facilmente; enquanto que o software — soft
significa mole em Português — é algo que deveria ser modificado mais facilmente.
Dessa maneira, quanto mais maleável — quanto mais soft — for a arquitetura, mais
fácil será alterar o sistema.
Tudo isso é compatível com a definição do propósito e do objetivo do desenvolvimento
de software colocada por Dan North — o inventor do Behaviour-Driven Develop-
ment (BDD) — em uma apresentação recente4 . Para ele, o propósito do desenvolvi-
mento de software é produzir impacto positivo de negócio. Para atingir esse propósito,
o objetivo do desenvolvimento de software é então minimizar o lead time de maneira
sustentável. Lead time é o tempo que se demora para, dado um problema, entregar-
se uma solução para esse problema. Obviamente queremos minimizar esse tempo,
entregando impacto positivo de negócio o mais rápido possível. Porém — e é aí que
entra também o cuidado com a arquitetura —, isso deve ser feito de uma maneira sus-
tentável, ou seja, durável; para que o sistema tenha sobrevida longa. O mesmo autor
também acredita que o melhor tipo de arquitetura é aquele que maximiza a possibi-
lidade de se trocar seus componentes — uma arquitetura plugável.
4
Software that Fits in Your Head — Dan North – https://www.youtube.com/watch?v=4Y0tOi7QWqM
Características de uma boa arquitetura
Se o objetivo da arquitetura de software é minimizar os recursos humanos necessários
para construir e manter o sistema, fazendo com que ele seja o mais maleável possível,
então quais seriam as características de uma boa arquitetura, que de fato a tornariam
mais facilmente alterável?
Em primeiro lugar me parece importante diferenciar os tipos de código contidos em
um sistema de software. De maneira geral existe uma gradação de abstração nos dife-
rentes tipos de funcionalidade implementadas em um sistema. Por exemplo, há uma
grande diferença entre um trecho de código que verifica se o último caracter de um
vetor é um ‘/0’ para detectar o fim de uma cadeia de caracteres, de um trecho de código
que verifica se o aluno que está tentando se matricular em uma disciplina já cumpriu
os pré-requisitos necessários para efetuar essa matrícula. Consegue perceber a diferença
no nível de abstração tratado em cada um desses trechos?
O primeiro trecho trata de um detalhe de implementação de baixíssimo nível en-
quanto que o segundo trata de uma regra de negócios de um sistema no domínio
de ensino (mais sobre regras de negócio à frente; domínio aqui refere-se a um nicho
de negócios específico, tratado pelo software; nas palavras de Eric Evans: o domínio é
uma esfera de conhecimento, influência ou atividade).
Algo essencial de se ter em conta é que, a médio e longo prazo, as porções de código de
baixo nível, que tratam de funcionalidades técnicas e específicas de uma determinada
tecnologia, tendem a mudar independentemente das políticas de negócio de alto nível
do sistema. Por exemplo, é possível ser necessário trocar um framework, biblioteca ou
banco de dados durante a sobrevida de um sistema sem ter que modificar as regras de
negócio mais críticas (como a regra de matrículas comentada acima).
De fato, se o sistema sobreviver por um longo período é provável que em algum mo-
mento será necessário atualizar pelo menos algumas das tecnologias utilizadas. Nessas
circunstâncias as regras de negócio mais críticas não deveriam ser muito impactadas
pelas mudanças de tecnologia e deveriam poder ser modificadas sem a preocupação
com essas mudanças. Efetivamente, aqui não estamos tão preocupados somente com
o fato de que, em algum momento, ter que trocar o banco de dados utilizado (pode
até ser que isso nunca ocorra); mas sim que o código relacionado com a interação com
o banco seja desacoplado do código das regras de negócio, para que ambos possam
evoluir independentemente.
Dessa maneira uma boa arquitetura deve separar esses tipos de abstração, para que
seja possível alterar os detalhes de implementação de baixo nível sem que as regras de
negócio de alto nível sejam afetadas por essas alterações, e vice-versa. Isso está dire-
tamente relacionado com os princípios de projeto S.O.L.I.D., em particular com o
princípio da Inversão de Dependência (Dependency Inversion Principle), cuja ideia
é exatamente fazer com que abstrações de alto nível não dependam de concreções de
baixo nível (é necessário que a dependência seja invertida, da implementação de baixo
nível para a de alto nível, de maneira geral utilizando uma interface da qual ambos os
trechos dependam).
Seguindo nessa mesma linha, ao observar o sistema a partir do zoom out comentado
acima, o que deveria ser mais evidente? A tecnologia utilizada para implementar o
sistema ou as suas regras de negócio? O que é mais importante: o fato do sistema
ser implementado em Python e Django ou o fato de ser um sistema de controle de
alunos e matrículas em uma instituição de ensino? O que deveria ser mais evidente
no sistema é o seu propósito e não as tecnologias utilizadas para implementá-lo, e nem
seus detalhes de mais baixo nível.
Dessa maneira, as operações de mais alto nível do sistema — também chamadas de
casos de uso — deveriam ter posição de privilégio na arquitetura, pois são elas que de-
finem o que o sistema faz. Aqui descobrimos um outro nível importante de abstração
do sistema, que não se refere nem as regras de negócio do domínio — como a regra de
pré-requisitos para uma matrícula comentada acima — e nem os detalhes de imple-
mentação de mais baixo nível (são as regras de negócio da aplicação, como veremos
mais à frente).
Como comentado por Robert Martin, a arquitetura do sistema deveria gritar o seu
propósito, e não revelar facilmente detalhes de baixo nível de como o sistema é imple-
mentado. Repare que isso acontece em arquiteturas de outros tipos de obras huma-
nas. Por exemplo, a arquitetura de uma catedral revela explicitamente o seu desígnio:
ela pode estar, por exemplo, desenhada em forma de uma cruz; a arquitetura de uma
biblioteca com todas as suas bancadas para estudo, estantes de livros e locais para com-
putadores para pesquisa também revela facilmente o seu intuito. Da mesma maneira
a arquitetura de um sistema de software deveria explicitar claramente o seu propósito,
ou seja, os seus casos de uso.
Regras de Negócio do Domínio e da Aplicação
As regras de negócio são o que há de mais importante em um sistema de software.
Dito de uma maneira nua e crua: as regras de negócio são as políticas que permitem
uma empresa ganhar ou economizar dinheiro. É o que gera valor para quem utiliza
os serviços daquele negócio.
O exemplo utilizado por Robert Martin em seu livro é muito gráfico e de fácil enten-
dimento. Imaginemos um negócio de empréstimo de dinheiro. Existem dados e polí-
ticas que são críticas ao negócio e que existiriam mesmo que não houvesse um sistema
que os implementasse. Por exemplo, para se realizar um empréstimo são necessários
os seguintes dados críticos: o principal que é o valor emprestado ao cliente somado
a quaisquer taxas necessárias; a taxa de juros que será aplicada ao empréstimo e um
período dentro do qual o empréstimo deve ser pago. Além dos dados, existem algu-
mas regras de negócio críticas atreladas a esses dados: a realização de um pagamento;
a aplicação dos juros; e a cobrança de multa caso, por exemplo, um pagamento atrase.
Repare que esses dados e regras existiram independentemente de um sistema que os
automatizasse: essas regras poderiam ser — e de fato são quando não há um sistema
— realizadas à mão.
A junção entre os dados críticos de negócio com as regras críticas de negócio formam
uma Entidade do domínio. No exemplo, poderíamos ter uma classe de domínio Em-
préstimo, que agrega os dados críticos de negócio (principal, taxa, e período) e as
regras de negócio críticas (realizarPagamento, aplicarTaxa, cobrarMultaDeAtraso).
Em uma linguagem orientada a objetos as entidades podem ser implementadas como
uma classe; em uma linguagem não orientada a objetos basta agregar os dados e as
regras críticas de domínio em um módulo para se ter a implementação da entidade.
Nem todas as regras de negócio implementadas em um sistema são tão puras e essen-
ciais quanto as regras de negócio do domínio. Existem regras de negócio atreladas a
forma como o sistema automatiza as regras de negócio do domínio. Essas são as re-
gras de negócio da aplicação e, de maneira geral, constituem os casos de uso, que são
as operações de alto nível realizadas pelos interessados no sistema.
Por exemplo, no caso de um sistema que implementasse o negócio de empréstimo co-
mentado acima em um banco, poderia existir uma regra que define que somente um
cliente com seus dados coletados e verificados e que possua um score de crédito acima
de um determinado valor poderia realizar um empréstimo. Por essa razão, o banco
pode especificar que o sistema não prosseguirá à tela de estimativa de pagamentos do
empréstimo até que a tela contendo os dados de informação do cliente tenha sido
preenchida e verificada, e que tenha sido confirmado que o score de crédito do cliente
está acima do limite inferior definido. Isso define um caso de uso do sistema.
Essa ideia de desenvolver os sistemas com ênfase e privilegiando o domínio de ne-
gócios está relacionada com o conceito de projeto guiado pelo domínio (do Inglês,
Domain-Driven Design — DDD (Evans, 2003)). No DDD um dos objetivos é co-
locar o foco principal do projeto no cerne e na lógica de domínio. Essa maneira de
desenvolver software que é tanto chave no DDD quanto na Arquitetura Limpa —
e nas outras arquiteturas que a inspiraram —, como veremos à frente, torna os siste-
mas mais resilientes, já que a parte central deles é desenvolvida de maneira testável e
independente dos outros interesses.
Isso vai de encontro com a ideia de desenvolver sistemas maleáveis, que é exatamente
o objetivo da arquitetura de software, como visto anteriormente. O objetivo é de-
senvolver sistemas com uma arquitetura de componentes plugáveis (Martin, 2017): o
cerne do sistema fica no centro e desacoplado dos diversos plug-ins que implementam
detalhes de baixo nível. Esses plug-ins podem ser plugados, desplugados e substituí-
dos por outros plug-ins conforme a necessidade.
No próximo capítulo veremos como isso é atingido utilizando a Arquitetura Limpa.
3 | A Arquitetura Limpa
egundo Dan North, como comentado anteriormente, o propósito do desenvol-
vimento de software é gerar impacto positivo de negócios. Ou seja, quando cons-
truímos software, o que queremos é gerar valor para o negócio para o qual es-
tamos desenvolvendo uma solução. Porém, poderíamos gerar impacto positivo de
negócios demorando muito (por exemplo, um ano, ou uma década). Então, outra
questão importantíssima no desenvolvimento de software é minimizar o lead time,
ou seja, minimizar o tempo que levamos para, dado um problema, gerar uma solução
que impacte positivamente o negócio.
Repare que isso não é muito difícil. Podemos gerar gambiarras em cima de gambiarras
para entregar o mais rápido possível algum software que, de alguma forma, resolve o
problema. Porém, se fizermos isso, dentro de pouco tempo teremos uma completa
bagunça: uma arquitetura caótica que impossibilitará a adição de funcionalidades
novas e, assim, nos impedirá de gerar mais impactos positivos de negócio, minando
assim nosso propósito.
Dessa maneira, existe outra questão muito importante a ser considerada: nós não
queremos simplesmente gerar impacto positivo de negócios o mais rápido possível,
nós queremos fazer isso por um longo tempo. Ou seja, queremos construir um sis-
tema no qual seja facilitada a manutenção, para que possamos continuamente e por
um longo tempo, gerar impacto positivo de negócios. Assim, o objetivo do desenvol-
vimento de software não é somente minimizar o lead time, mas sim minimizar o lead
time para o impacto positivo de negócios de maneira sustentável.
Se pensarmos bem, os termos minimizar e sustentável na definição estão em oposi-
ção. Ou seja, quando queremos minimizar o lead time, queremos entregar algo logo
(estamos dizendo algo do tipo “sai da frente que eu tenho que entregar algo rápido”).
Por outro lado, o sustentável dirá: “epa, se continuarmos assim, não conseguiremos
manter a entrega de impacto positivo; então, precisamos utilizar algumas práticas,

18
disciplinas e princípios para fazer isso de maneira sustentável”. E assim, refletindo na
engenharia de software, encontraremos várias práticas, princípios e disciplinas que
nos ajudam na parte sustentável do desenvolvimento de software.
Uma delas é arquitetar bem o sistema o que, como já comentamos, terá como prin-
cípio deixar o sistema o mais maleável possível, para que possamos continuamente
realizar manutenções, adicionando features por um longo período. Uma das manei-
ras de se fazer isso é utilizando a Arquitetura Limpa.
Segundo Robert Martin, seu idealizador, a Arquitetura Limpa é uma tentativa de
integrar várias arquiteturas desenvolvidas nas últimas décadas em uma ideia prática.
Como comentado na introdução deste livro, as arquiteturas que embasaram a Arqui-
tetura Limpa incluem a Arquitetura Hexagonal (também conhecida como Portas e
Adaptadores); a Dados, Contexto e Interação (DCI ); e a Fronteiras, Controle e Enti-
dade (BCE). Antes de explicar a Arquitetura Limpa, segue um breve resumo dessas
outras arquiteturas.

Arquitetura Hexagonal
A Arquitetura Hexagonal foi desenvolvida por Cockburn (2005) e sua intenção é pos-
sibilitar que, por um lado, uma aplicação seja executada a partir de usuários, progra-
mas, testes automatizados ou scripts e, por outro lado, seja desenvolvida e testada iso-
lada de seus dispositivos e bancos de dados necessários em tempo de execução. Repare
que existe uma ideia de fundo de desacoplar o cerne da aplicação de quem a executa
(usuários, programas, testes) e também de quem ela conversa (dispositivos e bancos
de dados).
Uma das motivações da Arquitetura Hexagonal citada por Alistair Cockburn é que
em diversas aplicações as regras de negócio tendem a vazar tanto para o lado da inter-
face gráfica quanto para o lado dos dispositivos utilizados pela aplicação (por exem-
plo, para o banco de dados). Quando isso acontece, é difícil testar a aplicação por
meio de testes automatizados ou executar a aplicação sem a interface gráfica (por
exemplo, por meio de scripts ou outros programas).
Dessa forma, segundo o autor, uma regra importante da arquitetura de software é que
o código da parte interna da aplicação — do seu cerne — não deveria vazar para partes
externas da aplicação. Para que isso seja possível, é necessário que a parte interna da
aplicação se comunique com a parte externa por meio de portas — entendidas aqui
no sentido de conectores padronizados, como USB, HDMI, etc., definidos por meio
de interfaces — que são ajustadas por meio de adaptadores conforme os dispositivos
específicos (daí o nome alternativo de Portas e Adaptadores). Na figura 3.1 é mostrado
um esquema básico da Arquitetura Hexagonal.

Figura 3.1: Esquema da Arquitetura Hexagonal (adaptado do trabalho de Stemmler


(2019)).

Dados, Contexto e Interação (DCI)


Trygve Reesnkaug, criador do MVC, e também seu co-autor James Coplien, julgam
que estamos fazendo programação orientada a objetos da maneira incorreta por mais
de 40 anos. De fato, de acordo com Reesnskaug, a essência da programação orientada
a objetos é que redes de objetos comunicantes trabalhem para atingir um objetivo
comum. Porém, em vez de comunicar isso em nosso código, optamos por escrever
programas orientados a objetos em termos de classes. As classes nos informam sobre
as propriedades dos objetos individuais que são suas instâncias; por outro lado, elas
não dizem como essas instâncias interagem para atingir o comportamento do sistema
e nem como o estado do sistema é representado por esses objetos e suas relações. O
resultado, segundo Trygve, é que o código não revela tudo sobre como o sistema fun-
cionará e que seria então necessário um novo paradigma como fundamento para um
código mais expressivo. O Dados, Contexto e Interação (DCI) é proposto como tal
paradigma (Reenskaug, 2009).
Como este livro não trata sobre problemas essenciais do paradigma orientado a ob-
jetos, ou de como ele é implementado, limito-me apenas a descrever aqui as carac-
terísticas do DCI importantes para embasar a Arquitetura Limpa. No DCI, Dados,
Contexto e Interação são diferentes perspectivas que focam em uma determinada pro-
priedade do sistema. Código na perspectiva dos Dados especificam a representação
do estado do sistema; código na perspectiva do Contexto especifica redes de objetos
comunicantes em tempo de execução; e código na perspectiva da Interação especifica
como os objetos colaboram para atingir o comportamento do sistema.
Essa separação me parece consistente com as ideias que vínhamos apresentando sobre
Arquitetura de Software em geral. Note que a perspectiva dos Dados está relacionada
com o modelo de domínio do sistema e implementa suas regras de negócio. A pers-
pectiva da Interação está relacionada com as operações do sistema, ou seja, com seus
casos de uso (interessante notar também que o próprio Robert Martin utiliza o termo
interactors — interadores — para designar os casos de uso; ou seja, a relação entre os
casos de uso com a perspectiva da interação fica clara).
A perspectiva do Contexto é mais sofisticada já que contém conceitos que não apa-
recem nativamente na maioria das linguagens de programação utilizadas atualmente
(daí a necessidade de Reenskaug e Coplien de criar um novo paradigma com sua im-
plementação). Ela implementa, por exemplo, a ideia de Roles, que são papéis que
os objetos podem representar no sistema (por exemplo, uma conta de banco pode
representar o papel de conta origem ou conta destino de uma transferência).
De fato, de acordo com seus criadores, o DCI leva à construção de arquiteturas que es-
tendem a programação orientada a objetos contemporânea de sua estrutura centrada
nos dados para focar mais no valor de negócios de operações em nível de sistema –
os casos de uso (Coplien e Reenskaug, 2012). Nesse sentido ela casa muito bem com
ideias essenciais da Arquitetura Limpa como, por exemplo, dar papel de destaque
para os casos de uso na estrutura do sistema.
Fronteira, Controle e Entidade (BCE)
O padrão arquitetural Fronteira, Controle e Entidade (do Inglês, Boundary, Control,
Entity — BCE) foi desenvolvido por Ivar Jacobson a partir de sua abordagem de en-
genharia de software orientada a objetos guiada por casos de uso. O BCE estrutura
as classes que compõem o sistema a partir de suas responsabilidades na realização dos
casos de uso.
As classes que implementam os objetos de domínio que geralmente são persistidos
compõem a responsabilidade de Entidade no padrão. As classes que representam
interações com atores externos (usuários ou sistemas externos) compõem a respon-
sabilidade de Fronteira. Essas são as interfaces com o mundo exterior (de fato, o pri-
meiro nome dessa responsabilidade era Interface mas Jacobson achou por bem mudar
o nome para não confundir com interfaces no código, apesar de haver relação com elas
pois podemos expressar uma interação com o mundo exterior por meio de uma inter-
face). A responsabilidade de Controle é composta por classes que garantem o processo
requerido para a execução de um caso de uso e sua lógica de negócio, e coordena a in-
teração de outros objetos envolvidos no caso de uso.
Aqui também podemos ver a recorrência dos temas que vínhamos analisando na ar-
quitetura de software. Temos o modelo de domínio nas Entidades, a comunicação
com o mundo externo a partir das Fronteiras e a implementação dos casos de uso e
das regras de negócio da aplicação nos Controles.

Arquitetura Limpa: Uma Ideia Prática


Como comentado anteriormente, a Arquitetura Limpa é uma ideia prática que inte-
gra conceitos de arquitetura de software desenvolvidos nas últimas décadas, incluindo
as três arquiteturas descritas anteriormente.
Essas arquiteturas produzem sistemas que têm as seguintes características:

1. Independência de frameworks. A arquitetura não depende da existência de


algum software pré-pronto que deve ser estendido para a criação do sistema.
Isso permite que os desenvolvedores utilizem frameworks como ferramentas,
em vez de ter que encaixar o sistema nas restrições definidas pelos próprios fra-
meworks. A ideia é deixar o uso de bibliotecas e frameworks na periferia da
arquitetura.
2. Testável. As regras de negócio podem ser testadas sem a Interface Gráfica, ban-
cos de dados, servidor web ou qualquer elemento externo. De fato, cada uma
das camadas da arquitetura pode ser testada em isolamento.
3. Independente da Interface Gráfica. A Interface Gráfica pode mudar facilmente,
sem a necessidade de mudar o resto do sistema. Por exemplo, uma interface
Web pode ser trocada por uma interface de console sem impacto nas regras de
negócio.
4. Independente de Banco de Dados. A arquitetura permite a troca da tecnologia
específica de banco de dados. Por exemplo, pode-se trocar o Postrgres pelo
Mongo ou o SQL Server pelo Oracle. As regras de negócio não estão embutidas
no banco de dados.

O diagrama da Figura 3.2 resume a Arquitetura Limpa.


Os círculos concêntricos representam diferentes áreas do software. Quanto mais in-
terna a camada, mais alto o nível de abstração do código contido nela (lembre-se da
comparação comentada anteriormente sobre os diferentes tipos de código contidos
em um sistema). Os círculos mais externos representam camadas que implementam
código de mais baixo nível: mecanismos ou tecnologias específicas (código que imple-
menta a infraestrutura da aplicação). Os círculos mais internos são políticas ou regras
de negócio.
Talvez o conceito mais importante da Arquitetura Limpa seja a Regra de Dependên-
cia. Essa regra diz que dependências de código-fonte só podem apontar para dentro,
em direção às políticas de alto nível (e nunca para fora). Nenhum trecho de código
contido nos círculos mais internos pode saber de nada sobre algo em um círculo mais
externo. Especificamente: o nome de algo declarado em um círculo externo não de-
veria ser mencionado por código em um círculo mais interno. Isso inclui funções,
classes, variáveis ou qualquer outra entidade de software.
Da mesma maneira, estruturas de dados declaradas em círculos mais externos — por
exemplo, em um framework ou banco de dados — não deveriam ser utilizadas em um
círculo mais interno. Não queremos que nada de um círculo externo afete algo de um
círculo interno.
Figura 3.2: A Arquitetura Limpa (adaptado do trabalho de Martin (2017)).

Camada de Entidades
Como comentado anteriormente, as Entidades encapsulam as regras de negócio crí-
ticas de um domínio. Uma entidade pode ser uma classe com métodos, ou um con-
junto de estruturas de dados e funções. Não importa o tipo de paradigma utilizado,
desde que as entidades possam ser utilizadas por diversas aplicações na empresa.
As entidades encapsulam as regras mas gerais e de mais alto nível. Elas são as partes
do sistema com menor probabilidade de serem alteradas quando algo muda exter-
namente. Por exemplo, as entidades não deveriam sofrer modificação caso haja uma
alteração no modo em que o usuário navega no sistema ou na segurança da aplicação.
Nenhuma modificação operacional a alguma aplicação em particular deveria afetar a
camada de entidades.
Camada de Casos de Uso
O código na camada de casos de uso contém as regras de negócio específicas da apli-
cação (recorde das regras de negócio da aplicação mencionadas anteriormente). Essa
camada encapsula e implementa todos os casos de uso do sistema, as operações de
mais alto nível que os usuários ou outros atores do sistema realizam. Esses casos de
uso orquestram o fluxo de dados de e para as entidades, e direcionam essas entidades
a utilizarem suas regras de negócio críticas para atingirem os objetivos do caso de uso.
Segundo Robert Martin, os casos de uso realizam a dança das entidades, pois as ins-
tanciam e chamam métodos em uma e outra conforme a necessidade das operações.
Espera-se que mudanças nessa camada não afetem as entidades. Do mesmo modo, a
camada de casos de uso não deveria ser afetada por alterações em código externo como
o banco de dados, a Interface Gráfica ou quaisquer frameworks. A camada de casos
de uso deve ser isolada de outros interesses do sistema.
Por outro lado, espera-se que mudanças na maneira em que a aplicação é operada
afetem os casos de uso e, dessa maneira, o código nessa camada. Se detalhes de um
caso de uso forem alterados então algum código dessa camada também será afetado.

Camada de Adaptadores de Interface


O software contido na camada de adaptadores de interface é composto por um con-
junto de adaptadores que convertem os dados no formato mais conveniente para os
casos de uso e entidades para o formato mais conveniente para algum agente externo
como o banco de dados ou a Web. Essa camada contém, por exemplo, a implementa-
ção de todo o padrão MVC de uma Interface Gráfica do Usuário (GUI). Os presenters,
views, e controllers todos pertencem à camada de adaptadores de interface. Os mode-
los são somente estruturas de dados convenientes que são passadas dos controladores
aos casos de uso, e depois de volta aos presenters e views.
De maneira semelhante os dados são convertidos nesta camada da maneira mais con-
veniente para as entidades e casos de uso para a forma mais conveniente para os re-
positórios utilizados (por exemplo, o banco de dados). Nenhum código em camadas
mais externas deveria conhecer de nada específico sobre o banco de dados. Por exem-
plo, se o banco de dados utilizado é baseado em SQL, todo código SQL estará restrito
a esta camada.
Se existir a comunicação do sistema com quaisquer outros agentes externos, haverá
um adaptador na camada de adaptadores para mediar a comunicação entre o sistema
e o agente. Por exemplo, se o sistema envia e-mails, haverá um adaptador de interface
para utilizar um sistema concreto de e-mails (isso ficará transparente para os casos de
uso, que apenas conhecerão uma interface de alto nível de serviço de e-mail).

Camada de Frameworks e Drivers


A camada mais externa da Arquitetura Limpa é composta de frameworks, bibliotecas
e outras ferramentas (como o próprio banco de dados e um framework Web). De
maneira geral os desenvolvedores do sistema não escreverão código nesta camada, a
não ser algum tipo de mediador que se comunica com uma camada mais interna.
Aqui é onde todos os detalhes sujos de implementação entram. Como discutido por
Robert Martin, o banco de dados é apenas um detalhe assim como a Web; dessa forma
mantemos todas essas concreções na periferia do sistema, onde elas podem causar
pouco prejuízo.

Camada Principal e de Configuração


De maneira geral gosto de incluir uma quinta camada que conterá todos os módu-
los principais e de configuração da aplicação, e que também tem conhecimento de
todos os detalhes de implementação do sistema. Essa camada é que faz todas as co-
nexões entre as interfaces contidas nas camadas mais internas da arquitetura com os
adaptadores e as implementações concretas nas camadas mais externas, configurando
o sistema para sua execução e executando-o.
Robert Martin comenta sobre o componente Principal (ou Main), que implementa
a política de mais baixo nível no sistema. É o ponto de entrada do sistema e nada, além
do sistema operacional, depende dele. O trabalho desse componente é criar todos os
Factories, Strategies e outros serviços globais, e entregá-los às porções de alto nível mais
abstratas do sistema.
Gosto de considerar o componente Principal e tudo o mais que o circunda como
uma camada, já que em geral outros módulos serão necessários para a configuração
e execução do sistema, e não somente um módulo Main. Por exemplo, os Factories
necessários para construir os objetos que serão injetados nos construtores das políticas
mais abstratas também fazem parte desta camada. Da mesma forma, as configurações
das rotas em uma aplicação ou API Web.

Quanto mais para fora, mais detalhes


A cada camada indo para fora, podemos perceber mais detalhes de implementação,
mais tecnologia e infraestrutura, como no exemplo utilizado no capítulo anterior de
interesses do domínio versus implementações de baixo nível. Isso vai ficar claro na
passagem dos capítulos de cada camada: na primeira, de Entidades, trataremos exclu-
sivamente de interesses de alto nível; na segunda, de regras de negócio da aplicação
e, portanto, de um nível um pouco mais baixo; e assim por diante, até chegar a ca-
mada Principal e de Configuração, onde lidamos com os detalhes de menor nível do
sistema.

Relevância da Arquitetura Limpa no Desenvolvimento de Software


Contemporâneo
Alguns afirmam que não é possível desenvolver arquiteturas desacopladas como pres-
crito pela Arquitetura Limpa. Isso não é o que apontam as evidências. De fato, algu-
mas empresas grandes como iFood, Uber e Netflix têm implementado com sucesso
arquiteturas desacopladas à maneira da Arquitetura Limpa.
Há algum tempo conversei longamente com um desenvolvedor do iFood, o João Ga-
briel Bracaioli (também conhecido como “Braca”). Foi uma aula de duas horas pela
qual agradeço imensamente o Braca. No tour que ele me deu no sistema do iFood,
a FoodTech referência da América Latina que processa aproximadamente 20 milhões
de pedidos por mês, ficou claro o uso de arquiteturas desacopladas como a Arquite-
tura Limpa e a Arquitetura Hexagonal. O sistema é desenvolvido com a utilização de
microsserviços e, na medida do possível, os desenvolvedores procuram utilizar con-
ceitos das arquiteturas descritas neste livro. De fato, existe um esforço para tornar
os microsserviços desacoplados dos agentes externos (por exemplo, dos repositórios
de dados). No tour pude observar alguns serviços utilizando jargão da Arquitetura
Limpa e da Arquitetura Hexagonal.
Recentemente também foi publicado um post no blog de engenharia do Netflix des-
crevendo o uso da Arquitetura Hexagonal em uma de suas aplicações. A ideia foi
exatamente desacoplar a implementação dos repositórios da lógica de negócio. Em
um dado momento do post o autor relata que conseguiram transferir requisições de
uma API JSON para uma fonte de dados GraphQL em apenas duas horas. Isso por-
que, por meio do uso da Arquitetura Hexagonal, foi possível prevenir que quaisquer
concreções na maneira com que a persistência era realizada vazasse para as porções do
código que implementam as regras de negócio. Para isso foram definidas interfaces
para os repositórios. Para trocar a fonte de dados da API JSON para o GraphQL bas-
tou implementar um adaptador do repositório em GraphQL e com uma mudança
em uma linha já foi possível passar a ler a partir de uma fonte de dados diferente. Esse
é o tipo de poder que é dado a um sistema implementado a partir de uma arquitetura
desacoplada.
O Uber tem uma história parecida. Recentemente foi relatado no blog de engenha-
ria deles a implementação do que eles chamaram de Domain-Oriented Microservices
Architecture - DOMA 1 (Arquitetura de Microsserviços Orientada ao Domínio). A
partir de muitos problemas encontrados pelo uso de microsserviços, os desenvolve-
dores resolveram pensar em uma alternativa mais bem estruturada. Ali também fica
clara a intenção de desacoplar os microsserviços - ou as coleções de microsserviços,
chamados de domínios - de outros microsserviços, por meio de, por exemplo, interfa-
ces bem definidas. De fato, os autores citam que os conceitos utilizados no DOMA
são fortemente baseados em maneiras estabelecidas de organizar código como o DDD
e a Arquitetura Limpa.
Ou seja, esses exemplos são evidência de que mesmo em um mundo de aplicações al-
tamente escaláveis e distribuídas com milhões de usuários e requisições, é importante
cuidar da arquitetura. De fato, além dessas três companhias (iFood, Netflix e Uber), já
ouvi menção do uso de ideias relacionadas com a Arquitetura Limpa de profissionais
de mais três empresas grandes: Amazon2 , Mercado Livre3 e Nubank4 .
É óbvio que a infraestrutura utilizada em aplicações contemporâneas é importantís-
sima também: deve-se pensar em como escalar a aplicação por meio do uso de estraté-
gias de cache, espelhamento de bases de dados, escalonamento de servidores, utiliza-
ção de serviços serverless e assim por diante. Ou seja, deve-se cuidar tanto da estrutura
1
Introducing Domain-Oriented Microservices Architecture – https://eng.uber.com/
microservice-architecture/
2
Teaching the important parts of Ports and Adapters – https://chriskiehl.com/article/
how-to-teach-ports-and-adapters
3
Mensageria no Mercado Livre: Apache Pulsar e BigQ – https://youtu.be/rcwXdDIYEQ0?t=2573
4
Why We Killed Our End-to-End Test Suite – https://building.nubank.com.br/
why-we-killed-our-end-to-end-test-suite/
da sua aplicação — do seu projeto, da sua arquitetura — quanto da infraestrutura:
ambos são importantes. De maneira geral, como comentado anteriormente, parece-
me que uma arquitetura desacoplada pode inclusive facilitar a implementação de di-
ferentes estratégias de infraestrutura para melhorar características importantes do sis-
tema (como performance e disponibilidade).
Está fora do escopo deste livro tratar dessas questões de arquitetura do ponto de vista
de infraestrutura. Recomendo como material essencial desse tema o livro Systems
Design Interview de Xu (2020) que, apesar de focar em questões de entrevistas relaci-
onadas a como lidar com a infraestrutura de um sistema, delineia muito bem os tipos
de problemas que se encontram no desenvolvimento de aplicações contemporâneas
altamente distribuídas.

Microsserviços: modularizando a aplicação por componen-


tes
Um detalhe importante relacionado com microsserviços e a Arquitetura Limpa é o
seguinte. De maneira geral neste livro focamos no entendimento da aplicação por ca-
madas. Se pensarmos em uma aplicação Web isso seria equivalente a tratar em uma
camada de nossas Entidades, depois tratar das operações de alto nível em nossos Casos
de Uso, mais externamente, tratar de nossos controladores Web na camada de Adap-
tadores de Interface, e assim por diante. Isso faz bastante sentido em uma aplicação
menor e monolítica como a que utilizaremos como exemplo neste livro.
Uma outra maneira de modularizar a aplicação seria pensar em cada fatia de funcio-
nalidade, focando em cada contexto delimitado, por assim dizer (Bounded Context,
do Domain-Driven Design). Por exemplo, em uma aplicação maior onde ocorre al-
gum tipo de venda, podemos pensar no contexto dos Produtos — o que incluiria
um serviço de catálogo de produtos —, no contexto de Pedidos — o que envolveria
a realização de um pedido de compra pelo usuário –, e no contexto de Pagamentos
— o que envolveria a comunicação, por exemplo, com serviços externos que realizam
o pagamento. Ao modularizar a aplicação dessa forma, teríamos como que fatias da
Arquitetura Limpa em cada módulo. (Repare também que poderíamos inclusive de-
senvolver uma aplicação monolítica dessa forma, separando inclusive os repositórios
de dados de cada módulo.)
Essa forma de pensar na aplicação quebrando-a em módulos baseados em cada con-
texto delimitado é mais compatível com um estilo arquitetural de microsserviços. De
fato, nesse caso, poderíamos como que implementar uma mini Arquitetura Limpa
em cada microsserviço. Achei importante deixar claro esse ponto porque, repare,
tudo o que falamos aqui neste livro continua valendo para um contexto mais mo-
derno de aplicações que utilizam mensageria. Esse seria apenas mais um detalhe da
solução, não algo que afetaria significativamente a arquitetura do sistema (de fato,
isso é afirmado pelo próprio Robert Martin no seu livro seminal sobre o tema (Mar-
tin, 2017)).

E o frontend?
Muitas pessoas me pedem para falar sobre o uso da Arquitetura Limpa no frontend.
Na minha visão, o frontend faz parte das camadas mais externas da arquitetura: im-
plementa a interface gráfica. Dessa maneira, se pensarmos bem, já estamos no nível
de tecnologias específicas, ou seja, de código de baixo nível (pois podemos dizer que
quanto mais perto das entradas e saídas do sistema menor o nível do código, em com-
paração com as abstrações de mais alto nível que encontramos no cerne das aplica-
ções).
Dessa forma, não vejo tanto sentido em aplicar rigorosamente a regra de dependên-
cia aqui: de maneira geral já escolhemos uma tecnologia — seja React, Angular ou
Next.js —; o maior ganho tera sido separar o backend com uma boa arquitetura do
frontend, para que possamos acessar o sistema via diferentes interfaces gráficas (pode
ser um cliente Web, pode ser uma aplicação mobile, ou ainda, pode ser uma aplicação
console).
Isso não quer dizer que não nos preocupemos com o design de software no cliente.
Todos os princípios de projeto também valem aqui: queremos módulos desacopla-
dos e bem coesos — aplicando o princípio da responsabilidade única — Single Res-
ponsibility Principle —, queremos componentes extensíveis aplicando o Open/Closed
Principle; enfim, todo o S.O.L.I.D. e as boas práticas de projeto continuam valendo.
Por outro lado, não me parece fazer sentido replicar as camadas todas da Arquitetura
Limpa no frontend pois, aqui, de maneira geral, não será bom que tenhamos regras
de negócio, o cerne do sistema que nos interessa preservar na arquitetura do sistema.
Um bom padrão de projeto para ser utilizado no frontend é o Humble Object. Como,
de maneira geral, é difícil testar elementos da interface gráfica — devemos utilizar me-
canismos para observar se tais e tais elementos aparecem com tais e tais propriedades
na tela —, podemos mover a lógica principal — quando há alguma — para um mó-
dulo separado, e testar essa lógica ali. Os módulos que implementam a apresentação
dos elementos da interface ficam, assim, humildes, simplesmente delegando as partes
mais complexas a módulos separados.
Ao utilizar o padrão Humble Object, podemos separar os comportamentos mais com-
plexos em um módulo Presenter, e deixar os elementos de interface gráfica em uma
View (Martin, 2017). A View é o objeto que é difícil de testar: o código neste objeto
é mantido o mais simples possível. Ela move dados para a interface gráfica mas não
processa esse dado. O Presenter é que aceita dados da aplicação e os formata para apre-
sentação, de maneira que a View pode simplesmente colocá-los na tela. Por exemplo,
se a aplicação deseja que uma data seja apresentada em um campo, ela passará um
objeto do tipo Date ao Presenter que, por sua vez, vai formatar essa data em uma
string simples. Essa string é que é passada à View para ser simplesmente apresentada
na tela. Repare: as Views só tem o papel de apresentar os dados já processadores pe-
los Presenters, ficando, assim, humildes. Os Presenters podem então ser testados mais
facilmente.
Concedo que o frontend das aplicações Web está cada vez mais complexo e, dessa
forma, exige a utilização de boas práticas para que seja manutenível. Há pouco tempo,
um padrão dividindo Model, Views e Presenters como apresentado no diagrama da Fi-
gura 3.3 poderia ser suficiente. Com a complexidade das aplicações contemporâneas
é bem possível que precisemos gastar mais tempo para arquitetar bem o frontend.

Figura 3.3: Model-View-Presenter tradicional (adaptado do trabalho de Stemmler


(2020)).

Recomendo o trabalho de Khalil Stemmler, que tem tratado desse tema com profun-
didade. Ele possui um guia de arquitetura para o lado do cliente em seu blog (Stemm-
ler, 2020). Nesse guia ele propõe o zoom in no padrão Model, View, Presenter, co-
mentado anteriormente, como apresentado no diagrama da Figura 3.4. Repare que o
Model torna-se quase que uma camada em si, para tratar da lógica de interação com
dados globais da aplicação cliente e da recuperação dos dados. Conforme algumas
trocas de ideia que tive com o Khalil, ele pretende continuar tratando desse assunto5 .

Figura 3.4: Model-View-Presenter com zoom in (adaptado do trabalho de Stemmler


(2020)).

No seguinte post ele discute exatamente o que estou falando aqui: https://khalilstemmler.com/articles/
5

typescript-domain-driven-design/ddd-frontend/
4 | Tecnologia e padrões adota-
dos
as camadas mais internas da Arquitetura Limpa, a ideia é tratar dos conceitos mais
puros do domínio de nossa aplicação. Por outro lado, em princípio é impossível
fazê-lo sem utilizar uma linguagem de programação: é preciso se comprometer
um pouco. No caso deste livro, escolhi a linguagem TypeScript. Não vou perder
muito tempo explicando a linguagem: procurei programar de um jeito que qualquer
pessoa com conhecimentos rudimentares de Programação Orientada a Objetos con-
siga entender o exemplo. De qualquer maneira resumo brevemente algumas caracte-
rísticas do TypeScript aqui1 .
TypeScript (TS) é uma linguagem fortemente tipada construída por cima do JavaS-
cript, e que provê um ferramental para o desenvolvimento de aplicações robustas.
A maneira de se trabalhar com TS é um pouco incomum quando comparada com
outras linguagens mais convencionais como o Java. No Java, de maneira geral, pro-
gramas são constituídos de arquivos-texto com sequências de comandos. Sobre esses
arquivos é realizada uma análise sintática — em inglês parsing —, que transforma
o código-fonte em uma Árvore de Sintaxe Abstrata (AST), uma estrutura de dados
que ignora coisas como espaços em branco e comentários. O compilador então trans-
forma a AST em bytecode Java, um código intermediário que pode rodar em uma má-
quina virtual Java (a famosa JVM). A máquina virtual Java faz parte de um ambiente
de runtime maior que contém, além da máquina virtual, bibliotecas, configurações e
outros arquivos de recursos necessários para executar um programa Java.
No caso de um programa escrito em TS, primeiro é feita uma análise sintática e trans-
forma-se o código em uma AST TypeScript. A partir da AST, realiza-se a checagem de
1
A maior parte deste capítulo tem como referência o livro de Cherny (2019) e a documentação oficial do TypeScript
(https://www.typescriptlang.org/).

33
tipos, para verificar a correta utilização de tipos no programa (por exemplo, se alguma
variável de um tipo específico receber um valor de tipo incompatível, o typechecker
emite um erro). Depois que se verificou que o programa está type-safe a partir da
AST TypeScript, transpila-se a AST em código-fonte JavaScript.
A partir daí o programa é executado como normalmente é em JS, envolvendo os pas-
sos que vimos também no Java: o código é sintaticamente analisado, construindo
uma AST JavaScript e a partir daí gera-se bytecode JavaScript que pode ser interpre-
tado pelo runtime (em um browser ou, por exemplo, no Node.js). Na verdade o có-
digo pode ou não ser compilado para bytecode: em alguns casos, dependendo da im-
plementação, ele pode ser simplesmente interpretado (uma espécide de compilação
e execução linha-a-linha). No caso do Node.js, utilizado para nosso exemplo, como
ele é baseado no V82 , até onde pude entender, atualmente o código é primeiro trans-
formado em bytecode para depois ser interpretado (antigamente não havia esse passo
intermediário de compilação para bytecode). Na Figura 4.1 é mostrado um passo-a-
passo de como um código TypeScript é compilado. Sendo assim, o TS é, de fato, um
superconjunto do JS, de maneira que código escrito em JavaScript é também TypeS-
cript. Isso é interessante pois permite que desenvolvedores JS adotem o TS de maneira
incremental.

Figura 4.1: Esquema de compilação do TypeScript (adaptado do trabalho de Cherny


(2019)).

Uma diferença do TS para linguagens tradicionais OO é que no TS — como no JS


— podemos ter funções soltas, sem a necessidade de pertencerem a uma classe. Na
aplicação exemplo deste livro utilizo algumas funções fora das classes. Isso permite
2
v8 – https://v8.dev/
que elas sejam utilizadas sem a necessidade de serem prefixadas pelo nome da classe
— se fossem métodos estáticos — ou por um this — se fossem métodos de instância3 .
O sistema de tipos do TypeScript é diferente do sistema utilizado em linguagens OO
tradicionais como C# ou Java. No caso dessas linguagens, utiliza-se um sistema de
tipos nominal reificado. Isso quer dizer que qualquer valor ou objeto tem exatamente
um tipo: nulo, primitivo, ou um tipo conhecido de classe. Pode-se chamar métodos
como value.GetType() ou value.getClass() para consultar o tipo exato em tempo
de execução. A definição desse tipo reside em uma classe em algum lugar com algum
nome. Não se pode utilizar classes com estruturas parecidas intercambiavelmente,
a não ser que exista uma relação explícita de herança ou uma interface comum. Os
tipos que escrevemos em código estão presentes em tempo de execução, e os tipos são
relacionados por meio de suas declarações, e não por suas estruturas.
Em C# ou Java, faz sentido pensar em uma correspondência um-para-um entre os
tipos em tempo de execução e suas declarações em tempo de compilação. Já no Ty-
peScript faz mais sentido pensar em um tipo como um conjunto de valores que com-
partilham algo em comum. Como os tipos são somente conjuntos, um valor particular
pode pertencer a muitos conjuntos ao mesmo tempo.
Quando se pensa em tipos como conjuntos, fica mais fácil entender algumas ope-
rações do TypeScript. Por exemplo, no C# é estranho passar um valor que é uma
string ou um int, porque não há um tipo que representa esse tipo de valor. No
TypeScript isso é natural, uma vez que se entende que cada tipo é somente um con-
junto. Como podemos descrever que um valor que pertence ao conjunto string ou
ao conjunto number? Ele simplesmente pertence à união desses dois conjuntos: string
| number.

Além disso, a tipagem do TypeScript é estrutural, e não nominal. Se dois valores têm
a mesma estrutura, eles podem ser considerados do mesmo tipo. Sendo assim, não
seria necessário, por exemplo, criar uma interface que define operações particulares
para que um objeto que implemente essas operações fosse passado no lugar de um
valor do tipo da interface (como é feito no Java ou no C#). Entretanto, neste livro
preferi utilizar interfaces nesses casos e, inclusive, declarar que uma determinada classe
implementa uma interface — mesmo que não fosse necessário — para deixar mais
explícitas as intenções.
3
Alguns autores desencorajam essa prática (como, por exemplo, Michael Feathers em Working Effectively with Legacy
Code (Feathers, 2004)). Entretanto, parece-me que em alguns casos o que se precisa é apenas uma função, e acho conve-
niente poder escrever funções soltas nessas circunstâncias (em particular com funções puras cujos valores de retorno são
determinados apenas pelos seus valores de entrada, sem efeitos colaterais observáveis).
Uso da monad Either para tratar erros de maneira elegante
Além do TypeScript em si, utilizei a monad Either por toda a aplicação para o trata-
mento de erros. Como essa solução foi padronizada, achei por bem já explicá-la aqui
antes de entrar nas camadas da Arquitetura Limpa para a aplicação exemplo.
Tenho uma confissão a fazer: não gosto de código de tratamento de exceções e pre-
firo fazê-lo somente nas camadas mais externas da aplicação para, de fato, capturar
somente cenários verdadeiramente inesperados. O try-catch deixa o código um
pouco sujo, com comportamento que soa a GOTO. Explico: dentro de um bloco try
é possível haver um desvio de fluxo de cada um dos comandos para os tratadores de
exceção. Ou seja, é possível pular de cada um dos comandos do bloco para os tratado-
res. A meu ver, isso deixa o código complicado de entender. Entenda, não sou con-
tra o uso de tratamento de exceção e considero um grande avanço das linguagens de
programação o fato de haver construções específicas para lidar com isso. Mas prefiro
deixá-lo para situações verdadeiramente excepcionais, como estouro de memória ou
perda da conexão (de fato, no exemplo descrito neste livro há apenas UM try-catch
na classe WebController e outro na implementação de um middleware, como será
visto mais à frente). Para erros mais comuns, que podem ser de fato previstos, tenho
dado preferência a retornar o erro do que lançá-lo.
Que fique claro: isso não tem exatamente a ver com Arquitetura Limpa e é somente
uma decisão de projeto que tomei na implementação do exemplo. Porém, acho im-
portante considerar essa opção caso a linguagem permita. Não vejo grandes pro-
blemas em utilizar o try-catch em vez dessa solução — de fato, no livro Clean
Code (Martin, 2008), recomenda-se o uso de tratamento de exceção em vez do re-
torno de erros —, mas confesso que gostei bastante do resultado final com o Either.
O Either é então utilizado para tratar elegantemente situações onde pode haver algum
erro mais comum. Por exemplo, um email pode ser inválido e assim quem tentou
criá-lo deve saber se foi o caso. Nesse tipo de situação eu utilizei o Either para retor-
nar o erro. De fato, no livro Object Design de Wirfs-Brock et al. (2002), os autores
recomendam esse tipo de tratamento, onde se retorna o erro em vez de lançá-lo (fi-
quei genuinamente feliz de encontrar essa recomendação depois de tomar a decisão
de fazer assim após refletir bastante sobre o assunto).
O funcionamento do Either — que literalmente significa OU uma coisa OU outra
— é simples: você pode definir que um método pode retornar alguns tipos de erro ou
alguns tipos de sucesso. Os tipos de erro vêm à esquerda e os tipos de sucesso à direita.
Por exemplo, no caso da criação de um email que será visto à frente, o método que
cria o email pode retornar InvalidEmailError OU o objeto do tipo Email criado com
sucesso. É possível saber se o tipo retornado foi de erro ou de sucesso perguntando se
ele é right ou left.
Um exemplo mais interessante aparece no factory method de criação do usuário que
será descrito mais à frente: ele pode retornar um InvalidEmailError, um InvalidPass-
wordError ou a instância de User criada com sucesso. Assim, a interface do método
fica da seguinte forma (cada tipo de erro é separado por uma ‘|’ — união de tipos
do TypeScript comentada anteriormente — e para separar tipos de erro de tipos de
sucesso utiliza-se uma ‘,’ — separação de left e right):

Ou seja, ele recebe duas strings, uma representando o email e a outra a senha, e pode
retornar erros de email ou senha inválidos (à esquerda) ou o usuário criado (à direita).
O Either empacota o retorno e é possível então perguntar se ele é um tipo errôneo ou
de sucesso; é possível também acessar o valor em si a partir da propriedade value. Em
caso de erro o valor será o objeto representando o erro; em caso de sucesso será o
objeto retornado.
Na figura 4.2 encontra-se a implementação do tipo Either utilizada no exemplo deste
livro. Primeiramente são definidas as classes Left e Right com a propriedade value
e os métodos isLeft e isRight para sabermos se o retorno foi de sucesso ou de erro.
Depois vem a definição do próprio tipo Either — que é a simples união de Left e
Right — e mais abaixo vêm as definições das funções de conveniência left e right, que
são utilizadas para instanciar objetos do tipo Left e Right respectivamente. Repare
no uso de programação genérica (generics) nos tipos variáveis L e A. Eles são utilizados
para que se possa instanciar os tipos concretos de erro e de sucesso quando se utiliza o
Either (não se preocupe se você não entendeu esse código: é provavelmente o trecho
mais difícil deste livro; por outro lado, como ficará claro, o seu uso é bem simples).
Além do livro Object Design citado anteriormente, esse tipo de estratégia para tra-
tar erros aparece como opção também no livro Programming Typescript de Cherny
(2019).
Como utilizo esse padrão de tratamento de erros por toda a aplicação, e o Either em
Figura 4.2: Implementação do Either.

si não é um recurso da linguagem, adicionei o seu código em um local compartilhado


(uma pasta shared). Esse local não representa uma camada da arquitetura, mas sim um
recurso de tecnologia que pode ser utilizado por qualquer camada. Ele é comparável
aos recursos da própria plataforma adotada (por exemplo, funções do JavaScript).
A ideia é que esses recursos são estáveis, ou seja, não queremos mudá-los durante a
evolução da aplicação (se fosse alguma tecnologia volátil, não seria desejável que os
módulos da aplicação dependessem diretamente dela).
Assincronismo no JavaScript / TypeScript
De maneira geral a maioria do código que escrevemos nas camadas mais internas da
Arquitetura Limpa é bem agnóstica a tecnologias. Por outro lado, é impossível fu-
gir completamente dos detalhes relacionados com a linguagem e plataforma onde a
aplicação irá rodar. No caso do TypeScript que é um superconjunto de JavaScript —
como comentado anteriormente —, e do Node.js, tecnologia e ambiente alvos neste
livro, uma particularidade importante a ser levada em conta é a questão do assincro-
nismo.
Por design, o JavaScript é uma linguagem que, de maneira geral, executa em uma
única thread. Digo “de maneira geral” porque atualmente não há construções ine-
rentes à própria definição da linguagem que permitam, por exemplo, a criação e ge-
renciamento de threads (em oposição a linguagens como o Java, que têm construções
nativas para lidar com multi-threading). Para ser mais exato, quem oferece a possibi-
lidade de se trabalhar com multi-threading é o hardware subjacente utilizado; mas os
sistemas operacionais e as linguagens nos ajudam — ou podem impedir quando não
há facilidades para isso — de fazer uso dessa capacidade do próprio hardware. Em
particular, o Node.js, que é o runtime que estamos utilizando como alvo neste livro
para executar código JavaScript (a partir da transpilação do TypeScript), tem como
característica rodar em uma única thread. Para lidar com operações potencialmente
bloqueantes (como a busca de recursos em um servidor remoto, o acesso a recursos
do sistema de arquivos, ou o acesso a dados em um banco de dados), o JavaScript pos-
sui construções para a execução de funções de maneira assíncrona. O TypeScript, por
conseguinte, também possui essa facilidade. Essa característica permite que as aplica-
ções executem de maneira mais eficiente por sobre a tecnologia subjacente do Node.js,
o event loop, já que é possível executar outras operações até que a operação assíncrona
finalize.
Um exemplo do mundo real utilizado por Eric Lippert ajuda muito a entender a uti-
lidade do assincronismo no JavaScript (Vaggalis, 2014). Imagine que uma pessoa vai
fazer o café da manhã; ela vai fazer umas torradas e cozinhar alguns ovos. A única
thread é análoga à única pessoa que realizará as tarefas. O equivalente à abordagem
síncrona seria a seguinte sequência: coloque os pães na torradeira; espere que eles ter-
minem de torrar; entregue as torradas; coloque os ovos na frigideira; espere os ovos
cozinharem; entregue os ovos. Repare nas duas esperas. Uma abordagem assíncrona
— mas não paralela com outras threads, que é o que ocorre quando temos apenas
uma thread — seria: enquanto os pães estão na torradeira, coloque os ovos na frigi-
deira; alterne entre checar os ovos, checar a torradeira e checar se existe algum outro
pedido chegando que poderia ser iniciado. Quando a torrada ou os ovos estiverem
prontos, entregue-os. Fica claro que a segunda abordagem é potencialmente mais rá-
pida já que não é necessário esperar uma operação terminar — por exemplo, o pão
ser torrado — para se iniciar uma outra operação.
O mecanismo mais moderno para a execução de operações assíncronas no JavaScript
é uma API baseada em Promises com uma sintaxe especial que permite tratar código
assíncrono como se fosse síncrono. Uma Promise é exatamente o que o nome diz: uma
promessa de eventual conclusão ou falha de uma operação assíncrona. De maneira
geral, no TypeScript, basta utilizar o modificador async para informar que a função
(ou método) é assíncrono. Quando esse é o caso, a operação deve retornar um objeto
do tipo Promise, que empacota o retorno real da operação. Quando se chama uma
operação assíncrona e se deseja esperar o final de sua execução, é necessário utilizar o
comando await. Esse comando suspende a execução até que a promessa de retorno da
função assíncrona seja cumprida.
Sendo assim, todas as operações potencialmente bloqueantes em nosso exemplo serão
encapsuladas em métodos assíncronos. Como estamos utilizando a ideia do Either
para o tratamento elegante de erros (explicado anteriormente), quando houver uma
operação assíncrona que pode retornar um erro, o tipo de retorno será uma Promise
que, por sua vez, empacotará o Either. Por exemplo, o tipo de retorno do caso de uso
de Sign up que cadastra o usuário e será descrito à frente, é o seguinte:

Isso significa que ele é um método assíncrono que pode retornar (1) um erro de usuá-
rio existente, (2) um erro de email inválido, (3) um erro de senha inválida; ou (4) um
resultado de autenticação de sucesso. A princípio parece um código complicado de
entender, mas se lemos o significado dos nomes, é fácil ver o que está acontecendo.
Em Inglês, seria algo como o método dizendo I PROMISE to return EITHER some
error types (existing user, invalid email or invalid password) OR an authentication
result; em Português: eu PROMETO retornar OU alguns tipos de erros (usuário
existente, email inválido ou senha inválida) OU um resultado de autenticação. Não é
exatamente isso o que está acontecendo (na verdade, a função retorna uma promessa,
não promete um retorno), mas é uma aproximação que ajuda a entender o código.
Com os comandos async e await o código é simplificado pois programamos como se
tudo fosse síncrono. Repare que na implementação das Promises, como no Either,
também foi utilizada programação genérica (generics) para que se possa instanciar os
tipos concretos de retorno. No capítulo que descreve os casos de uso começaremos a
utilizar métodos assíncronos.
No próximo capítulo, o estudo de caso é delineado juntamente com o escopo dos
casos de uso que implementaremos em nosso exemplo.
5 | Estudo de Caso: theWisePad
omo comentado na introdução deste livro, o estudo de caso que será utilizado para
demonstrar a Arquitetura Limpa é um Bloco de Notas na Web chamado theWi-
sePad (dei esse nome à aplicação devido ao meu projeto pessoal theWiseDev). A
ideia é simples: um sistema no qual usuários possam guardar diversas notas com texto
comum (uma espécie de Notes da Apple ou um Notion bem simplificado).
Cada usuário deve poder logar no sistema e gerenciar suas notas; cada nota deve ter
um título e conteúdo (o texto em si para ser guardado). Dos usuários pretende-se
inicialmente guardar apenas seu e-mail e uma senha para o login. O sistema é imple-
mentado como uma API REST (Fielding e Taylor, 2000) em Node.js e TypeScript
mas boa parte dele nem saberá que se trata de uma aplicação Web (em particular as
entidades e casos de uso não terão nenhuma referência à Web).
De maneira geral gosto de desenvolver sistemas começando por pensar nos casos de
uso principais. Depois disso passo por uma modelagem inicial das entidades para es-
boçar as classes do domínio com suas relações. Não se trata de big upfront design: a
ideia é perder pouco tempo aqui para já iniciarmos a implementação. Não faz parte
do escopo deste livro, mas desenvolvo todo o sistema utilizando Test-Driven Deve-
lopment (TDD), o que ajuda também a obter um projeto testável. Na versão atual do
sistema, existem 109 testes automatizados, a maioria deles sendo testes de unidade.
Gosto também da ideia de desenvolver o sistema em fatias. Ou seja: seleciona-se um
caso de uso e desenvolve-se tudo o que é necessário para ele em todas as camadas. Essa
foi a estratégia utilizada no desenvolvimento do theWisePad. Entretanto, para ficar
mais didático, apresentarei a implementação a partir de cada camada, de dentro para
fora.
Outra ideia que gosto bastante ao iniciar um projeto novo (um green field project) é
construir um esqueleto ambulante (do Inglês: walking skeleton). O termo aparece no

42
livro Growing Object-Oriented Software Guided by Tests de Freeman e Pryce (2009).
O esqueleto ambulante é uma implementação inicial de alguma funcionalidade mí-
nima do sistema com todas as tecnologias que se deseja utilizar (e, em geral, escreve-se
um teste tipo BDD de aceitação no início, antes de implementar a funcionalidade,
para se ter uma confirmação de que de fato o esqueleto está andando quando os testes
passarem). Não se trata de fechar todas as decisões tecnológicas de início, mas apenas
de ter uma maneira de executar o sistema com uma infraestrutura mínima adequada.
Dessa maneira mitigam-se riscos que poderiam ser altos se fosse deixado para muito
mais tarde a execução do sistema todo. Isso é particularmente importante num sis-
tema Web onde existem várias questões a serem decididas antes de se colocar o sistema
para rodar.
Por exemplo, se temos o interesse de desenvolver uma API REST em Node.js e Ty-
peScript, um esqueleto ambulante já teria tudo o que fosse necessário para rodar um
sistema com essa tecnologia no lugar. Apesar de apreciar bastante essa ideia, não a uti-
lizarei neste livro, mais uma vez para tornar a apresentação da Arquitetura Limpa com
o exemplo mais didática. Tratarei dos detalhes tecnológicos somente no final, quando
estivermos nas camadas mais externas. É outra estratégia de desenvolvimento possível
e que contém suas vantagens; uma delas sendo a de poder decidir sobre os detalhes
somente mais tarde.
Seguindo então a estratégia adotada neste livro, vamos delinear alguns casos de uso
essenciais para implementar o sistema (coloco aqui apenas os seus nomes, sem ne-
nhuma formalização de UML, por exemplo, para tornar tudo mais simples, e sua
tradução em Inglês que será utilizada no código):

1. Cadastrar usuário (Sign up)


2. Fazer login do usuário (Sign in)
3. Criar nota (Create note)
4. Carregar notas (Load notes)
5. Atualizar nota (Update note)
6. Remover nota (Remove note)

Poderiam ser vislumbrados outros casos de uso, como trocar a senha do usuário, atu-
alizar usuário ou remover usuário. Porém, para simplificar o exemplo, vamos fechar
o escopo nesses cinco casos de uso.
Uma questão importante de se ter em mente é que o exemplo é simples e, portanto,
não abarca todos os tipos de problemas que podem ser encontrados em aplicações
reais mais complexas. Por outro lado, não seria possível tratar de uma aplicação mais
real em um livro focado como esse: é preciso algo que seja complexo o suficiente para
que as ideias sejam transmitidas mas simples o suficiente para que seja didático e caiba
em um livro. De qualquer maneira, procurei em diversas ocasiões mostrar os tipos de
problemas reais que poderiam aparecer caso se tratasse de uma aplicação mais com-
plexa.
Com os casos de uso descritos, qual seria um modelo mínimo do domínio necessário
para a implementação? Isso é o que será visto no próximo capítulo, que lida especi-
ficamente com a camada de entidades, a camada que contém o modelo de domínio
com os dados e regras críticas de negócio.
6 | Entidades

Um modelo base
ão sou fã da utilização pesada de modelagem UML no desenvolvimento de soft-
ware, porém acredito sim no uso da UML para rascunho de ideias (Fowler, 2003a).
Em particular gosto da UML para a modelagem de classes.
Eu acredito também ser possível derivar um modelo de classes totalmente a partir do
TDD, utilizando os testes para guiar a modelagem. Por outro lado, gosto de iniciar
um projeto criando um modelo mínimo antes de ir para o código. Isso não quer dizer
que esse modelo não mudará: é apenas um rascunho inicial. E mesmo que eu goste
de iniciar pensando e desenhando um modelo básico, ao começar a tocar no código,

45
de maneira geral desenvolvo tudo utilizando o TDD (com o modelo mínimo como
guia). Quando o modelo de domínio é mais complexo, é possível fazer tudo de uma
maneira mais orgânica a partir do TDD (como no exemplo de valores monetários
em moedas variadas utilizado no livro Test-Driven Development By Example de Beck
(2002)).
Um modelo de domínio para a aplicação de Bloco de Notas é muito simples: não há
o que complicar. Podemos começar com o seguinte: uma classe User para represen-
tar um usuário, e uma classe Note para representar uma nota. Para que esse modelo
fique um pouco mais interessante do ponto de vista das regras de negócio e para que
não tenhamos modelos anêmicos, optei por utilizar Value Objects para representar os
valores das propriedades das entidades principais. Dessa maneira, User deve conter
email e password e Note deve conter title e content. Como content pode ser qualquer
conjunto de caracteres, optei por deixar essa propriedade como string mesmo. Para
as outras três propriedades utilizei Value Objects.
É possível argumentar que a senha é uma propriedade que está mais relacionada com
a aplicação do que estritamente com o nosso domínio, pois está associada com um
interesse da aplicação: a autenticação. Dessa maneira, uma possível implementação
deixaria essa propriedade para ser tratada somente na camada de casos de uso, e não
aqui nas Entidades. Optei por deixar a senha aqui por simplicidade e para fazer sua
validação da mesma forma que é feita a validação do email. E, de fato, poderíamos
inclusive argumentar que alguma espécie de senha para acessar o bloco de notas de
uma pessoa específica — do usuário — poderia existir no mundo real (no domínio
de bloco de notas). Seria como aqueles diários que contém cadeados com senhas nu-
méricas, que só podem ser abertos por quem possui a senha (conforme a Figura 6.1).
Por outro lado, note que não há nenhum tipo de identificador artificial do usuário ou
da nota — nenhum id — aqui nas Entidades. Fiz isso de propósito: trato das identi-
ficações artificiais somente na camada de casos de uso, quando de fato pensamos em
repositórios (um interesse claramente relacionado com a maneira como automatiza-
mos o sistema, ou seja, um interesse dos casos de uso).
Esses seriam os nossos dados críticos de negócio. Como regras críticas de negócio, po-
deríamos pensar em como será a relação entre usuários e notas e como essas instâncias
serão criadas. Para deixar as coisas mais simples, podemos definir que uma nota terá
somente um dono (owner). O sistema é tão simples que acredito que essa seja a única
regra crítica de negócios que teríamos inicialmente, além de como serão realizadas as
criações das instâncias. Para deixar as coisas um pouco mais interessantes, decidi por
Figura 6.1: Um bloco de notas com senha numérica.

fazer com que os nossos Value Objects sejam auto-validados, ou seja, email, password e
title deverão ser validados no momento de sua criação. As classes que representam os
usuários e notas também terão factory methods para a criação de suas instâncias. Na
Figura 6.2 é apresentado o modelo de classes UML base para nossa aplicação.

Validação
A validação dos dados email, senha e título poderiam ser feitas em camadas mais ex-
ternas, como na própria interface gráfica (por exemplo, no JavaScript do formulário).
De fato, tratei sobre esse assunto com o próprio Robert Martin em nosso bate-papo
no meu canal do YouTube1 . Para ele esse tipo de validação mais sintática pode estar
em camadas mais externas e, assim, podemos esperar que os dados já venham ‘limpos’
nas camadas mais internas como a de entidades.
Mas, como já comentei, preferi fazer essas validações aqui nas entidades para deixar o
modelo mais interessante. Além disso, se pensarmos bem, ainda que essa dependên-
cia possa ser ‘tolerada’, se esperamos que os dados venham validados de fora, existe
de fato uma dependência implícita desta camada mais interna com as camadas mais
externas (ou seja, meu modelo não é completamente auto-contido). A verdade é que
poderíamos ficar discutindo sobre esse assunto por muito tempo; então decidi dei-
xar as validações mesmo que mais sintáticas aqui na camada de entidades para deixar
o modelo mais interessante e auto-contido (ele também fica mais ‘reusável’, já que
1
Clean Architecture with Robert Martin – https://youtu.be/ekBWizEpyvo
Password Title

Note
User owner
- content: string

Email

Figura 6.2: Modelo base da aplicação de Bloco de Notas theWisePad com as classes
User e Note e os Value Objects Password, Email e Title.

pode ser reutilizado em outras aplicações mantendo as auto-validações). Parece-me


que não há nenhum problema em fazer diferente, desde que haja alguma garantia de
que os dados cheguem de fato ‘limpos’ aqui (de qualquer maneira parece-me impor-
tante validar os dados no backend novamente depois de uma validação mais externa
no frontend; até porque em algumas situações é possível inclusive desabilitar o JavaS-
cript no front).
É importante notar também que alguns praticantes do Domain-Driven Design (Evans,
2003), em particular aqueles que utilizam programação funcional, defendem que esse
tipo de validação deve ser feita de fato na própria camada de entidades. Dessa maneira,
fica impossível criar uma instância de uma entidade que seja inválida. O trabalho de
Scott Wlaschin com F# vai nessa linha2 . Uma citação utilizada por Wlaschin repre-
senta bem essa ideia: “Make illegal states irrepresentable! ” (Yaron Minsky). Eu sim-
patizo com essa abordagem e, dessa forma, temos mais uma razão por fazer as coisas
dessa maneira.
Domain Modeling Made Functional - Scott Wlaschin - KanDDDinsky 2019 – https://www.youtube.com/
2

watch?v=2JB1_e5wZmU&t=514s
Value Objects Email, Password e Title
Um Value Object é um padrão de projeto que define pequenos objetos que represen-
tam entidades simples cujas igualdades não são baseadas em identidades — identifi-
cadores únicos — mas sim em seus próprios valores. Dois Value Objects são iguais
quando têm o mesmo valor, não necessariamente quando são o mesmo objeto.
Email, senha e título da nota podem ser implementados como Value Objects já que não
precisam de identificadores únicos e suas igualdades dependem somente dos seus va-
lores. Para essas entidades simples também optei por deixá-las auto-validáveis, como
comentado anteriormente. Utilizei um factory method na criação dos objetos. Esse
método, chamado create, recebe os valores brutos, valida-os e, só então, chama o cons-
trutor da classe (o construtor é privado para que todas as instanciações sejam feitas
somente via factory method).
No construtor eu também chamo o método freeze do JavaScript para que as instân-
cias sejam imutáveis. De maneira geral, quando possível, sigo um estilo mais funcional
de programação; isso ajuda a evitar problemas de concorrência, por exemplo. Além
de procurar deixar as instâncias dos objetos imutáveis, também utilizo funções de or-
dem superior (higher-order functions) como map, find e some, quando possível, pois
deixam o código mais declarativo e elegante. De fato, parece-me que há uma onda
forte entre programadores experientes de construir software a partir de uma Arqui-
tetura Limpa (possivelmente utilizando o nome de Hexagonal), com um modelo de
domínio puramente funcional3 .
Na figura 6.3 encontra-se a implementação do Value Object Email. A validação do
email foi feita utilizando os documentos RFC 5322 (seções 3.2.3 e 3.4.1) e RFC 5321, re-
sumidos na Wikipedia (entrada “Email address”4 ). Repare na utilização do Either no
retorno do create: o método pode retornar um InvalidEmailError ou uma instância
de Email quando a string representa um endereço de email válido.
A expressão regular que define um endereço de email válido foi omitida do código por
razões de espaço (na função nonConformant). As funções emptyOrTooLarge (verifica se
uma string está vazia ou excede o tamanho máximo — passado como parâmetro),
nonConformant (verifica se o endereço de email não está em conformidade com as nor-
mas) e somePartIsTooLargeIn (verifica se alguma parte do domínio — entre os ‘.’s —
3
Veja, por exemplo, esse tweet do Nat Pryce, um dos autores do Growing Object-Oriented Software Guided by Tests (Fre-
eman e Pryce, 2009): https://twitter.com/natpryce/status/1444253955417067522?s=20.
4
Email address – https://en.wikipedia.org/wiki/Email_address
Figura 6.3: O Value Object Email.
excede o tamanho máximo permitido) foram criadas para deixar o código mais limpo.
A validação do email aparece aqui na classe Email mas no repositório preferi mover as
funções (de valid para baixo) em um módulo email-validator separado.
Os Value Objects Password e Title são bem bem parecidos. No caso da senha, para
simplificar, deixei somente a regra de que ela tem que conter pelo menos um número
e tamanho mínimo de seis caracteres. No caso do título, verifica-se se ele tem tamanho
maior ou igual a três e menor do que 256. Caso contrário ele é considerado inválido.

As classes User e Note


As classes mais importantes do modelo do nosso domínio são User e Note. Elas re-
presentam um usuário e uma nota. A classe User é apresentada na Figura 6.4.
Optei por deixar privados os atributos que representam o email e senha do usuário e
adicionei gets para poder acessá-los. Repare que os tipos desses atributos são os Value
Objects explicados acima, e não simples tipos primitivos (no caso, seriam strings). De
fato, é um conhecido code smell a obsessão por tipos primitivos. Procurei fugir dessa
obsessão com o uso dos Value Objects.
O funcionamento da classe é simples: ao tentar criar um usuário, primeiro tenta-
se criar o email e a senha passados como parâmetros para o factory method. Se al-
gum desses dados é errôneo, retorna-se um Left com a instância do erro empacotada:
InvalidEmailError no caso do email ser inválido e InvalidPasswordError no caso da
senha ser inválida. Caso o email e a senha sejam criados com sucesso, retorna-se um
Right com a instância do usuário empacotada.

A classe Note funciona de maneira semelhante: tenta-se criar uma instância de Title e,
caso possível, retorna-se um Right com a instância da nota empacotada; caso contrário
retorna-se um Left com uma instância de InvalidTitleError empacotada.

Independência
Repare que nessa camada não há nada relacionado a tecnologia ou detalhes de im-
plementação. Aqui estamos pensando somente nos mais puros elementos do domí-
nio e como eles devem se relacionar. Esse tipo de modelagem é o que aparece nos
livros clássicos de design orientado a objetos (como por exemplo, Analysis Patterns
Figura 6.4: A classe User.

de Fowler (1996) ou Object-Oriented Analysis and Design with Applications de Bo-


och (2004)). Considero que esse tipo de modelagem continua sendo extremamente
importante (fato que é confirmado pelo grande interesse recente em Domain-Driven
Design, que tem como um dos objetivos principais exatamente colocar como foco
primário a lógica do domínio).

Próxima camada: casos de uso


Na próxima camada analisaremos a implementação das regras de negócio da aplica-
ção, ou seja, dos casos de uso. Os casos de uso implementam as operações de alto nível
do sistema.
7 | Casos de Uso

ma lista dos casos de uso que serão tratados neste livro já apareceu no Capítulo 5.
Não é uma lista completa: apresenta somente algumas operações que deveriam
existir no sistema. Os casos de uso que identificamos para o escopo deste livro
são os seguintes:

1. Cadastrar usuário (Sign up)


2. Fazer login do usuário (Sign in)
3. Criar nota (Create note)
4. Carregar notas (Load notes)
5. Atualizar nota (Update note)
6. Remover nota (Remove note)

54
Sign Up
O caso de uso Sign Up refere-se ao cadastro do usuário no sistema. Repare na dife-
rença entre a camada de entidades e a camada de Casos de Uso: aqui estamos tratando
de interesses relacionados a como automatizaremos o domínio que, nesse caso, trata-
se da possibilidade de uma pessoa — que chamamos de usuário — poder manipular
um conjunto de notas. Como em um sistema de software de maneira geral é neces-
sário que um usuário se cadastre para poder utilizar o sistema, essa operação será im-
plementada aqui nesta camada. Fica claro então que essa é uma regra de negócios da
aplicação.
O cadastro de um usuário no sistema consiste em receber suas informações — em
nosso caso seu email e senha —, verificar se são válidos (o que, em nosso caso, ficou
a cargo dos próprios factory methods criados na camada de domínio), e gravá-los em
um repositório de usuários, verificando de antemão se não se trata de um usuário exis-
tente já cadastrado no sistema. Quando se trata de senhas, para se ter um mínimo de
segurança, é importante não guardá-las no repositório de maneira direta: é essencial
criptografar a senha antes de gravá-la no repositório.
Outra coisa que podemos fazer ao cadastrar o usuário é de alguma forma já logá-lo no
sistema, quando o cadastro é realizado com sucesso. Isso permite que ele já permaneça
conectado depois do cadastro. Note que isso é uma decisão de projeto e não necessari-
amente precisamos implementar assim: poderíamos optar, por exemplo, por antes de
logar o usuário verificar se ele é realmente o dono daquele email. Em um sistema real
que fosse utilizado em produção seria muito importante fazer isso; por outro lado,
como trato aqui apenas de um exemplo simples para explicar a Arquitetura Limpa,
deixarei essa funcionalidade como exercício (o bom de ser professor é que você sem-
pre pode deixar algumas coisas como exercício para os alunos fazerem).
Implementando o cadastro da maneira descrita, podemos ver que necessitaremos de
três serviços específicos: (1) o repositório, onde guardaremos os usuários; (2) o serviço
de criptografia, para não guardarmos a própria senha do usuário no repositório; e (3)
o serviço de autenticação, já que queremos logar o usuário assim que ele realizar o
cadastro com sucesso. Aqui entra uma ideia essencial da Arquitetura Limpa: como
não queremos acoplar nosso caso de uso com um repositório específico (seja banco de
dados, arquivos-texto ou quaisquer outros tipos de armazenamento); nem a uma im-
plementação específica de criptografia; e nem a um serviço concreto de autenticação,
conversaremos com esses serviços por meio de portas (ou interfaces). Dessa maneira
deixamos o nosso caso de uso mais genérico e podemos trocar as implementações es-
pecíficas com menos esforço.
O repositório de usuários pode ser implementado como uma tabela em um banco de
dados, um arquivo-texto ou qualquer outra forma de armazenamento. Como não
queremos nos importar aqui em como esse serviço será implementado concretamente,
limitamo-nos a criar uma interface para ele. De maneira geral aqui precisaremos de
uma operação para recuperar um usuário com base em seu email e para adicionar o
usuário no repositório. Como a operação de recuperar todos os usuários do repo-
sitório pode ser útil mais para frente — eu sei, eu sei, de maneira geral não é bom
ficar prevendo o futuro, mas convenhamos: é só uma simples operação! — optei por
adicioná-la à interface também.
Confesso: em alguns momentos infringirei o Interface Segregation Principle (‘I’ do
SOLID) em nome da simplicidade; em vez de criar diversas interfaces, uma para cada
conjunto de operações realmente necessárias pelo caso de uso, criei um conjunto en-
xuto de operações e optei por utilizar menos interfaces. Se fosse o caso do sistema
crescer muito e essas interfaces realmente incharem, aí eu optaria por quebrá-las. Isso
aconteceu principalmente no caso das interfaces para os repositórios nas quais op-
tei por fechar um conjunto simples de operações que serão utilizadas pelos casos de
uso (algumas por uns, outras por outros)1 . Além disso, como será discutido mais à
frente, também não crio uma interface por caso de uso, mas uma única interface que
representa qualquer caso de uso.
Na Figura 7.1 é listado o código da interface UserRepository. UserData é uma interface
criada por conveniência com os dados do usuário (email e senha) e possivelmente seu
identificador. Uma vantagem importante de se ter esse tipo de DTO é que você não
precisa manipular diretamente os objetos de domínio em outras camadas — aliás,
algo imprescindível — e também, caso se adicione ou remova algum dado, não se-
rão quebrados vários pontos da aplicação que os manipulam (pois eles ser referem a
UserData e não a cada propriedade específica). Como as operações do repositório po-
dem ser bloqueantes, repare que o retorno de todas elas é uma Promise que empacota
o tipo de retorno alvo (explicada na seção sobre assincronismo no Capítulo 4).
Para a criptografia da senha é necessária uma operação que cifre uma string e tam-
bém que depois compare uma string descriptografada com a sua correspondente crip-
1
Para minha defesa, observei que o própio Robert Martin, um dos criadores do SOLID, faz o mesmo em um curso
disponível em sua plataforma www.cleancoders.com. Ele argumenta na mesma linha: vamos começar assim e, se ne-
cessário, podemos refatorar mais tarde, quebrando as interfaces caso elas inchem.
Figura 7.1: A interface UserRepository.

tografada. Para isso, necessitamos de uma interface — que chamei de Encoder —


que possui a operação de criptografia — encode — e a operação de comparação —
compare.

No caso do serviço de autenticação, ele precisa de uma operação — que chamaremos


aqui de auth — que recebe as credenciais e verifica se estão corretas. Existem basica-
mente três respostas possíveis para essa operação: ou (1) o usuário não existe; ou (2)
a senha está incorreta para o usuário; ou (3) as credenciais batem. Em sistemas mo-
dernos de autenticação por token, quando as credenciais batem, o sistema gera um to-
ken de acesso que pode ser utilizado pelas aplicações-cliente para fazerem requisições
subsequentes às operações da API. Dessa maneira optei por utilizar essa abstração no
serviço de autenticação: o resultado de uma autenticação de sucesso conterá um token
e uma identificação do usuário (repare que não nos importamos aqui nesta camada
em como esse token será gerado).
Sendo assim, para o serviço de autenticação que será utilizado pelo caso de uso de Sign
Up (e, mais tarde, pelo Sign in), criei a interface AuthenticationService apresentada
na Figura 7.2, juntamente com os tipos AuthenticationParams e AuthenticationRe-
sult. Essa interface serve principalmente para podermos testar os casos de uso que
utilizam o serviço de autenticação em isolamento, já que podemos fazer um stub do
serviço, sem que seja necessário autenticar um usuário de fato para poder testar a ló-
gica de nossas operações.

Autenticação
A autenticação de um usuário no sistema não é um caso de uso em si; no caso de nossa
aplicação, os casos de uso de Sign in e Sign up é que farão uso da lógica de autenti-
cação. Por outro lado, parece-me claro que autenticar um usuário no sistema é um
interesse da aplicação e, dessa forma, sua lógica cabe adequadamente na camada de
Casos de Uso (pois, de fato, será utilizada pelos casos de uso). Isso porque esse inte-
Figura 7.2: Os tipos AuthenticationParams e AuthenticationResult e a interface
AuthenticationService.

resse está diretamente relacionado com a forma como automatizamos o nosso domínio
de negócios.
Outras formas de autenticação também podem ser implementadas — como auten-
ticação via serviços do Google, Facebook e Twitter, por exemplo —, mas essas im-
plementações estariam em camadas mais externas, como será visto. De fato, a cada
requisição da API, é necessário verificar se o usuário está autenticado e autorizado a
realizar aquela operação. Nesse momento é que podem ser utilizadas outras manei-
ras de autenticar e autorizar, além da maneira customizada da própria aplicação, que
veremos a seguir.
Na Figura 7.3 é apresentada a classe CustomAuthentication que implementa um ser-
viço de autenticação customizado para nossa aplicação, utilizando o repositório, o
serviço de criptografia e o serviço de gerenciamento de tokens abstraídos em suas res-
pectivas interfaces. O que ficará por concretizar nas camadas mais externas serão a
maneira concreta de se recuperar os dados do usuário, de se criptografar e descripto-
grafar as senhas, e de se gerar e verificar os tokens.
Aqui fica clara a inversão de dependência: em vez de fazermos as nossas operações de
alto nível dependerem diretamente de implementações concretas desses serviços, as
operações dependem somente de abstrações — ou seja, das interfaces ou portas. Em
uma camada mais externa essas interfaces serão implementadas por adaptadores que
se comunicarão com as bibliotecas concretas. A dependência é invertida porque em
vez de nossas operações de alto nível dependerem diretamente das implementações
de baixo nível, as implementações é que dependerão das abstrações de alto nível —
Figura 7.3: A classe CustomAuthentication, uma possível implementação, ainda que
mais genérica, do serviço de autenticação.

ou seja, das interfaces.


A classe CustomAuthentication (Figura 7.3) recebe as implementações concretas dos
serviços necessários (repositório, criptografia e gerenciamento de tokens) por injeção
de dependência (uma maneira de se implementar a inversão de dependência). Depois
ela só faz uso das operações implementadas nos respectivos adaptadores. Por exem-
plo, depois de encontrar o usuário no repositório verifica-se se a senha obtida do re-
positório casa com a senha descriptografada utilizando a operação compare do serviço
de criptografia (Encoder). Se as credenciais estiverem corretas, assina-se o identifica-
dor do usuário utilizando o gerenciador de tokens (tokenManager.sign). Por fim, o
serviço de autenticação customizado retorna o token juntamente com o identificador
do usuário.
Finalmente, o código do caso de uso de Sign up em si é apresentado na Figura 7.4.
Todos os casos de uso implementam a interface UseCase que, basicamente, possui
uma operação perform que realiza a operação do caso de uso. No caso do cadastro,
ele recebe como request os dados do usuário; tenta criar o usuário utilizando o factory
method da Entidade User e, caso ocorra algum problema, retorna o erro gerado. Aqui
é que acontece a dança das entidades. (Concedo, é uma dança simples pois se resume
na tentativa de criar o usuário: nesse caso não há mais nenhuma regra de negócio do
domínio importante para se realizar. Em um sistema mais complexo as operações
podem envolver a instanciação de várias entidades e a chamada de diversos métodos
nessas instâncias).
Depois de garantir que o usuário é válido, é preciso verificar se ele já foi cadastrado
(e, portanto, já existe no repositório). Caso seja encontrado — por meio da operação
findByEmail do UserRepository —, retorna-se um ExistingUserError. Caso o usuá-
rio seja válido e não esteja cadastrado ainda, ele é adicionado ao repositório com a se-
nha criptografada. Ao final, como comentado anteriormente, ele é logado no sistema
e retorna-se o resultado da operação de autenticação (ou seja, o token e a identificação
do usuário).

Sign In
O caso de uso de Sign In para logar o usuário no sistema é um dos mais simples de
todos. Isso porque ele se resume a simplesmente chamar a operação auth no serviço
de autenticação e retornar o retorno daquela operação. O serviço de autenticação é
também passado para a classe utilizando injeção de dependência. O código do caso
de uso encontra-se na Figura 7.5.

Create note
O código do caso de uso de criação de notas encontra-se na Figura 7.6. Em uma aplica-
ção de bloco de notas, é necessário que o usuário consiga criar notas novas. Para isso,
são necessárias as informações da nota: título, conteúdo e dono. Da mesma forma
que optei por utilizar uma estrutura de dados UserData — uma espécie de DTO —
para lidar com os dados do usuário sem ter que manipular a Entidade em si, também
Figura 7.4: A classe SignUp, que implementa o caso de uso de cadastro do usuário.

criei uma estrutura de dados NoteData para fazer a mesma coisa com as notas.
NoteData contém como atributos title, content, ownerEmail, ownerId e id, todos do
tipo string. Os dois últimos atributos são opcionais, pois nem sempre precisaremos
do email do dono da nota e nem do identificador da nota. De maneira geral, o melhor
é ter uma estrutura de dados dessa para cada requisição, para que não tenhamos dados
que não utilizamos nas requisições. Mais uma vez, dei preferência à simplicidade:
como se trata de uma aplicação pequena, optei por ter somente uma estrutura de
Figura 7.5: A classe SignIn, que implementa o caso de uso de login do usuário.

dados. De qualquer forma, é relativamente simples criar mais estruturas de dados e


realizar uma refatoração na aplicação toda se acharmos necessário.
A lógica do caso de uso de criação de notas é a seguinte: primeiro verifica-se se o dono
da nota é um usuário existente no repositório de usuários; caso contrário, retorna-
se um erro do tipo UnregisteredOwnerError. Depois, o usuário e a nota são criados
utilizando os factory methods de User e Note. Como nesse ponto sabe-se que o usuário
está no repositório, não precisamos nos preocupar com os seus dados serem inválidos,
essa checagem já é feita no caso de uso de cadastro. De qualquer forma criamos uma
instância de User para realizar a dança das entidades. A nota, entretanto, pode conter
dados inválidos e, caso isso se verifique, retorna-se o erro correspondente.
Depois disso é necessário verificar se já não há uma nota do mesmo usuário com o
mesmo título no repositório de notas. Caso isso se verifique, retorna-se um erro do
tipo ExistingTitleError. Repare que isso é uma regra de negócios da aplicação que
decidi implementar: usuários não podem ter mais de uma nota com o mesmo título.
Isso é uma decisão de projeto; se decidíssemos que isso não fosse uma regra interes-
sante, poderíamos não verificá-la: o identificador da nota é que a diferenciaria de ou-
tra nota, mesmo que tivesse o mesmo título.
Repare, por outro lado, que isso poderia ser uma regra de negócios do domínio tam-
bém. Se decidíssemos que essa é uma regra fechada do próprio domínio e que não
queremos que seja alterada em cada aplicação, poderíamos colocar sua implementa-
ção na camada de Entidades. É sempre bom ter isso em mente: as regras de negócio da
Figura 7.6: A classe CreateNote, que implementa o caso de uso de criação de nota.

aplicação são mais voláteis, podem mudar de aplicação para aplicação. As de domínio
tendem a ser mais fixas e, portanto, mudar com muito menos frequência.
Se quiséssemos implementar a regra de negócios de não ter notas com o mesmo título
para o mesmo usuário no próprio domínio, o modelo mudaria bastante. Teríamos
que ter a informação de todas as notas de um dado usuário na Entidade User. A adi-
ção de uma nota poderia ser, nesse caso, um método da classe User e é nesse método
que seria feita a verificação dos títulos duplicados. É bom que fique claro para o leitor
que muitas decisões são subjetivas e que o projeto — o design do sistema — muda
conforme as decisões que tomamos ao longo do desenvolvimento. O que deve ficar
claro é isso: de maneira geral as regras que são mais fechadas no domínio de negó-
cios devem ser implementadas na camada de Entidades; as regras das quais não temos
certeza se serão fechadas, e que podem mudar de aplicação para aplicação, podemos
implementar na camada de Casos de Uso.
Caso a nota seja válida, o usuário dono esteja no repositório e o título não seja du-
plicado, ela é enfim adicionada ao repositório de notas. Repare, novamente, que o
repositório é genérico: não nos importa aqui qual mecanismo de persistência será
utilizado, mas sim que haja um repositório de notas com as operações desejadas.

Load notes
Em uma aplicação de Bloco de Notas é necessário que o usuário possa acessar suas
notas. Dessa forma, é interessante que exista um caso de uso que carregue todas as
notas de um usuário. A implementação desse caso de uso é bem simples também: ela
simplesmente chama a operação de carregar todas as notas para um dado identificador
de usuário no repositório de notas. O código da classe que implementa esse caso de
uso é apresentado na Figura 7.7.

Figura 7.7: A classe LoadNotes, que implementa o caso de uso de carregar notas.

Update note
O penúltimo caso de uso trata da alteração da nota. Confesso que não fiquei comple-
tamente satisfeito com o código desse caso de uso: ficou um pouco complexo. Tudo
isso porque trato das duas possíveis modificações — do título e do conteúdo da nota
— na mesma operação. Talvez o mais correto seria refatorar o código para deixar tudo
mais simples. Preferi deixar do jeito que está principalmente como uma demonstra-
ção do impacto de uma decisão de projeto. Ao final da explicação do código indico
como poderíamos refatorá-lo para deixar tudo mais simples.
Como o caso de uso trata da possível alteração do título ou do conteúdo da nota, a
estrutura de dados que representa a requisição possui duas propriedades opcionais: o
nome e o conteúdo (porque assim pode-se alterar um ou outro, ou os dois). O fato da
requisição possuir dados opcionais faz com que o código fique mais complexo, pois
precisamos verificar quais atributos serão alterados: título, conteúdo ou ambos. A es-
trutura de dados de requisição — UpdateNoteRequest — é apresentada na Figura 7.8.
Para alterar a nota precisamos do email e identificador do dono da nota, do identifi-
cador da própria nota e os dados a serem alterados, título ou conteúdo.

Figura 7.8: O tipo UpdateNoteRequest, que define a estrutura de dados de requisição


para a alteração da nota.

Bem, vamos para a explicação do caso de uso em si. Primeiramente os dados do usuá-
rio são recuperados. Depois, recupera-se a nota original em si e verifica-se se ela existe.
Caso contrário retorna-se o erro UnexistingNoteError. Cria-se o usuário dono da
nota a partir de seus dados e cria-se a nota alterada. Para a criação da nota alterada,
precisamos saber se o título a ser usado será o original ou o alterado (daí a chamada
à função getTitleToBeUsed) e o mesmo com o conteúdo. Se a nota for inválida,
retorna-se o erro correspondente (por enquanto temos somente a possibilidade do
título ser inválido mas do jeito que implementei é possível haver outros tipos de erro
futuramente).
Uma vez que a nota alterada foi instanciada corretamente — o que quer dizer que
trata-se de uma nota válida — precisamos realizar as alterações no repositório. Como
podemos alterar o título e/ou o conteúdo, utilizei dois condicionais para isso. As fun-
ções shouldChangeTitle e shouldChangeContent verificam se as propriedades title e
Figura 7.9: A classe UpdateNote, que implementa o caso de uso de alteração de nota.

content estão na requisição, respectivamente. Para a alteração do título, que é feita


a partir da operação updateTitle no repositório de notas, antes precisamos verificar
se o título novo não foi utilizado anteriormente para outra nota do mesmo usuário
(lembre-se da regra de negócios que decidimos implementar — não pode haver notas
com títulos duplicados para um mesmo usuário). Se o título já existir, retornamos um
erro do tipo ExistingTitleError; senão alteramos o título no repositório. A alteração
do conteúdo da nota no repositório é feita a partir da operação updateContent. Ao
final, retorna-se a nota alterada buscando-a no repositório com a operação findById.
Repare que o código ficou mais complexo justamente por conta das propriedades
title e content: pelo fato de elas serem opcionais, precisamos verificar quais delas
vieram na requisição. Uma refatoração que deixaria o código bem mais simples se-
ria manter as duas propriedades como requeridas e fazer o front-end mandá-las todas
mesmo que, por exemplo, somente o título tenha sido alterado. Assim poderíamos
fazer a atualização das duas propriedades mesmo que não haja alterações nas duas,
necessariamente. Com isso poderíamos inclusive ter somente uma operação de atu-
alização no repositório de notas — update — que simplesmente substitui a nota an-
tiga pela nova, sem a necessidade de verificar quais propriedades mudaram e quais
não. Isso inclusive facilitaria o código para possíveis modificações futuras quando,
por exemplo, fosse adicionada alguma outra propriedade à nota.
De qualquer maneira preferi deixar o código como está para mostrar como uma deci-
são de projeto pode afetar negativamente na complexidade da implementação de uma
operação. É importante também ter essa mentalidade de refatoração contínua: o có-
digo provavelmente nunca estará em um estado perfeito e, assim, sempre pode ser ob-
jeto de melhorias. Eu fiz o que pude para enxugar o código do caso de uso, principal-
mente extraindo as funções shouldChangeTitle, shouldChangeContent, getTitleTo-
BeUsed e getContentToBeUsed. Isso certamente melhorou o código mas a refatoração
mais essencial comentada anteriormente simplificaria a implementação ainda mais
(preferi utilizar funções em vez de métodos para evitar a necessidade do uso do this,
ou do nome da classe — no caso de um método estático —, para chamar a operação,
como comentamos ser possível no começo do capítulo 5).

Remove note
Em uma aplicação de Bloco de Notas é necessário que o usuário possa remover uma
dada nota que ele possui. A lógica do caso de uso de remoção de nota é bem simples:
procura-se a nota no repositório e, caso seja encontrado, chama-se a operação de re-
moção da nota no repositório remove. Caso a nota não seja encontrada, retorna-se um
UnexistingNoteError. O código do caso de uso é apresentado na Figura 7.10.

Figura 7.10: A classe RemoveNote, que implementa o caso de uso de remoção de nota.

Próxima camada: adaptadores de interface


Na próxima camada analisaremos a implementação dos adaptadores de interface. Em
particular, trataremos principalmente dos elementos mais próximos da interface: os
controladores e um middleware que representam nossa camada de apresentação.
8 | Adaptadores de Interface

camada de Adaptadores de Interface é constituída por um conjunto de adaptado-


res que convertem dados de e para os casos de uso. Esses adaptadores convertem
dados que vêm das camadas mais externas para um formato conveniente para os
casos de uso; e o inverso: convertem dados dos casos de uso para um formato conve-
niente para os serviços externos.
Antes de discutir o que decidi incluir nesta camada, gostaria de discutir o que decidi
não incluir nesta camada. Meus adapters que possibilitam a interação da minha apli-
cação com o mundo externo — por exemplo, com o banco de dados, com a biblioteca
de criptografia, etc. — se comunicam diretamente com esses serviços. Por exemplo,
meu adaptador MongoDB do repositório de usuários chama operações do driver do
MongoDB diretamente (por exemplo, para recuperar um usuário com base em um
email). Sendo assim, apesar de conceitualmente serem adaptadores — no sentido do

69
padrão de projeto adapter — e aparentemente encaixarem nesta camada, eles depen-
dem diretamente de serviços externos. Dessa forma, se eu os colocasse aqui, feriríamos
a regra de dependência da Arquitetura Limpa, a regra mais restrita e forte desta arqui-
tetura, que diz que as dependências devem sempre se apresentar de fora para dentro,
e nunca ao contrário. De fato, poderiam existir adaptadores intermediários e gené-
ricos como gateways de banco de dados que ainda não se comunicam diretamente
com um banco específico e, nesse caso, seriam implementados nesta camada. Porém,
os adaptadores que se comunicam diretamente com os serviços externos não cabem
nesta camada, mas sim na próxima, pois implementam interesses de infraestrutura e
se comunicam diretamente com ela.
Consequentemente, os adapters que se comunicam diretamente com código externo
à aplicação são implementados na próxima camada, de frameworks e drivers (que eu
também gosto de chamar de camada externa e que também é identificada comumente
como camada de infraestrutura). Em nossa aplicação, aqui na camada de Adaptadores
de Interface fica a implementação dos controladores e do middleware que verifica o
token de acesso a algum endpoint da API (lembrando que a ideia é que o MVC de
uma aplicação pode ser implementado inteiramente na camada de Adaptadores de
Interface). Isso porque, com exceção da Web — porque de fato aqui nesta camada já
estamos definindo que nossa aplicação é uma API Web —, essas implementações são
independentes de tecnologias específicas: elas implementam adaptadores que serão
concretizados e configurados nas próximas camadas.

Controladores Web
O MVC é um padrão de projeto utilizado originalmente em aplicações desktop cuja
ideia é auxiliar no desenvolvimento de interfaces com o usuário que dividem a lógica
do programa em três elementos interconectados: Modelos, Visões e Controladores.
Os modelos são os componentes principais do padrão: eles representam a estrutura de
dados dinâmica da aplicação, independente da interface gráfica — o cerne do sistema.
As visões constituem quaisquer representações de informação; como gráficos, diagra-
mas ou tabelas. Múltiplas visões da mesma informação devem ser possíveis, como um
gráfico de barras para a gerência de uma empresa e uma visão tabular para os conta-
dores da mesma empresa. Os controladores fazem o meio de campo entre os modelos
e as visões, aceitando dados de entrada e convertendo em comandos para os modelos
ou as visões.
No caso de uma API Web como a nossa, os controladores têm o papel de receber os
dados de requisição ao endpoint, desempacotá-los e convertê-los em comandos para
os nossos casos de uso. Ou seja, são os controladores que recebem uma requisição e
chamam a operação correspondente nos casos de uso. Repare que o controlador Web
em nossa aplicação é o adaptador por excelência, conforme a Arquitetura Hexagonal
(ou Portas e Adaptadores) (Cockburn, 2005). Isso porque uma das ideias principais
da Arquitetura Hexagonal é exatamente essa: quando um driver quer utilizar a apli-
cação em uma porta, ele manda uma requisição que é convertida por um adaptador
da tecnologia específica do driver — em nosso caso, HTTP — em uma chamada proce-
dimental ou mensagem, que passa isso à porta da aplicação. A porta em nosso caso é
o caso de uso específico que executa a operação requisitada.
Como nossos controladores são controladores Web, de maneira geral os retornos que
eles gerarão serão códigos de estado HTTP (por exemplo, 200 representando um re-
torno ok e 400 representando um retorno de requisição inválida) e, opcionalmente,
quaisquer informações adicionais importantes em um body para as aplicações clien-
tes que fizeram a requisição. As entradas dos controladores Web são requisições HTTP
com os dados necessários igualmente em um body. Sendo assim, para a implementa-
ção de nossos controladores, desenvolvi duas interfaces, uma para as requisições HTTP
(HttpRequest) e outra para as respostas HTTP (HttpResponse). O código dessas inter-
faces é apresentado na Figura 8.1.

Figura 8.1: As interfaces HttpRequest e HttpResponse, que representam requisições e


respostas HTTP.

Essas interfaces permitem que nossos controladores sejam independentes de quais-


quer tecnologias Web (por exemplo, de frameworks como o servidor HTTP Express, que
de fato será utilizado na próxima camada). Se quiséssemos, por outro lado, migrar
nossa API de uma estilo arquitetural REST para o GraphQL, por exemplo, podería-
mos fazer uma interface de requisição mais abstrata — por exemplo, sem o body —,
mas não teríamos muita dificuldade em fazê-lo. Além dessas interfaces, criei um mó-
dulo HttpHelper com funções convenientes para cada retorno HTTP. O código dessa
classe é apresentado na Figura 8.2.

Figura 8.2: O módulo HttpHelper com funções convenientes para cada retorno HTTP.

A versão que apresentarei dos controladores aqui será uma que cheguei depois de um
tempo de desenvolvimento e algumas refatorações. Note que precisamos de um con-
trolador para cada um de nossos casos de uso: Sign up, Sign in, Create note, Load
notes, Update note e Remove note. Em particular note que a lógica desses controlado-
res é muito parecida: todos precisam verificar se os parâmetros requeridos vieram na
requisição (e, caso contrário, retornar um Bad Request — 400) e envolver a chamada
da operação do caso de uso com um try-catch, para retornar um erro interno de servi-
dor caso alguma exceção seja lançada (Internal Server Error — 500). Ou seja, a lógica
resume-se no seguinte: (1) checar se os parâmetros requeridos vieram na requisição;
(2) retornar um 400 se algum parâmetro estiver faltando; (3) chamar a operação espe-
cífica do controlador envolta em um try-catch. Esse tipo de design parece muito com
o problema recorrente que é resolvido pelo padrão de projetos template method.
Dessa forma, resolvi implementar uma variação desse padrão para não ter que repetir
toda essa lógica para cada um dos controladores. Digo variação porque na versão ori-
ginal do template method são utilizados métodos abstratos e herança. Como herança
não está muito na moda e, de fato, no próprio livro da GoF (Gamma et al., 1995)
recomenda-se composição à herança, resolvi utilizar a primeira em vez da última.
A implementação ficou assim: criei uma classe WebController que implementa a ló-
gica genérica discutida acima e que, para realizar a operação específica do contro-
lador, recebe por injeção de dependência um objeto do tipo ControllerOperation.
ControllerOperation é uma interface que possui a operação specificOp, ou seja, a
operação específica do controlador, e também uma propriedade requiredParams, que
serve para informar ao WebController quais são os parâmetros requeridos para aquela
operação (ela é um vetor de strings). Dessa forma cada controlador será, na verdade,
um WebController que contém como atributo uma implementação da interface Con-
trollerOperation que, por sua vez, define a operação específica que será chamada no
caso de uso (este será recebido também por injeção de dependência) e define também
quais são os parâmetros requeridos da requisição.
A Figura 8.3 apresenta o código da classe WebController. Repare na lógica genérica
implementada no método handle: começa-se com um try; verifica-se os parâmetros
faltantes e, caso algum esteja ausente, lança-se um MissingParamError; executa-se a
operação específica do controlador e termina-se com um catch para caso ocorra al-
guma exceção. O catch simplesmente retorna um erro interno do servidor (500) com
o erro gerado no body.
Repare que o handle acessa os parâmetros requeridos da operação — requiredParams
— na chamada ao método getMissingParams. Esse método, por sua vez, é um método
auxiliar que, para cada parâmetro requerido, verifica se ele está contido no body da
requisição. Ao final ele retorna uma lista de parâmetros faltantes entre ‘,’. Isso é feito
para que a mensagem de erro indique quais parâmetros faltaram na requisição.
Na Figura 8.4 é apresentado o código da classe SignUpOperation, que define a ope-
ração específica do controlador de Sign up (cadastro do usuário). Repare que os pa-
râmetros requeridos são as informações do usuário necessárias pelo caso de uso. A
operação em si somente chama o perform do caso de uso e verifica se o retorno foi de
sucesso. Caso seja, ele retorna um código de recurso criado (201). Caso contrário é ne-
cessário verificar se o erro é de usuário existente e, nesse caso retorna-se um forbidden
(403), ou outro tipo de erro (como senha incorreta). Nesse último caso retorna-se
um erro de requisição inválida (400).
Aqui cabe um comentário sobre o uso da interface UseCase. Essa interface é bem gené-
rica, pois representa qualquer caso de uso. Isso facilita na hora de definir a interface
ControllerOperation, pois podemos declarar que ela tem como atributo qualquer
Figura 8.3: A classe WebController que define a lógica genérica de um controlador
Web.

caso de uso, que será injetado no construtor. Também facilita na hora de testar cada
caso de uso, pois posso criar somente um stub de UseCase que, por exemplo, lança uma
exceção, em vez de ter que criar um para cada caso de uso. Por outro lado, corremos o
perigo de receber um caso de uso incorreto (por exemplo, o SignUpOperation receber
o caso de uso de Sign in, em particular no momento da configuração da aplicação).
Assim, uma outra opção de projeto seria termos uma interface específica para cada
caso de uso, oferecendo a possibilidade de testar os controladores com stubs (como já
é possível nessa implementação com a interface mais genérica), mas sem correr o risco
de receber um caso de uso incorreto. Eu preferi deixar a implementação assim pela
simplicidade do exemplo. Porém, fica aqui o alerta para outra solução de projeto mais
segura (que, de fato, não possibilitaria a injeção de casos de uso incorretos mas que,
por outro lado, não possibilitaria a implementação da interface ControllerOperation
como fizemos aqui).
As outras operações de controladores são bem parecidas com o SignUpOperation.
Para apresentar mais um exemplo semelhante, na Figura 8.5 é listada a operação do
Figura 8.4: A classe SignUpOperation que define a operação do controlador de cadas-
tro do usuário e seus parâmetros requeridos.

controlador de criação de notas (CreateNoteOperation). Os parâmetros requeridos,


nesse caso, são o título, conteúdo e email do dono da nota. A lógica da operação
também se resume a chamar o caso de uso correspondente. Nesse caso, criei um
noteRequest com os dados da requisição e um useCaseResponse, que guarda o retorno
da realização do caso de uso. Aqui pode-se observar uma limitação do uso da interface
de caso de uso mais genérica: o seu tipo de retorno é um genérico Promise<any>. Dessa
forma, defini o tipo concreto de retorno para o useCaseResponse. Enfim: trade-offs.
Finalmente, se o retorno é de sucesso, retorna-se um código de recurso criado (201),
caso contrário, retorna-se um erro de requisição inválida (400).
As outras operações são bem semelhantes às duas apresentadas, o que muda são so-
mente os parâmetros requeridos e os tipos concretos de retorno dos casos de uso. Por
outro lado, a operação do controlador atrelado ao caso de uso de atualização de notas
é um pouco diferente. Isso por causa da complicação comentada anteriormente de se
ter parâmetros opcionais na atualização da nota: título e/ou conteúdo. Mais uma vez
essa decisão deixou o código mais complexo. De qualquer maneira acho interessante
apresentar o código e a explicação dele aqui (é possível que para algum caso de uso es-
Figura 8.5: A classe CreateNoteOperation que define a operação do controlador de
criação de notas e seus parâmetros requeridos.

pecífico a lógica seja, de fato, pela própria natureza da operação, um pouco diferente;
sendo assim acho até bom termos uma operação que diverge um pouco dos outros
controladores).
Na Figura 8.5 é listada a operação do controlador de atualização de notas (UpdateNote-
Operation). No caso dessa operação, temos dois tipos de parâmetros: os parâmetros
requeridos para identificar a nota e seu dono para poder realizar a operação (iden-
tificador da nota, do email do dono da nota e do identificador do dono da nota); e
os parâmetros que representam os dados que estão sendo alterados na nota (título
e/ou conteúdo). Os primeiros são resolvidos pelo WebController, basta listá-los no
atributo requiredParams. Como podemos ter título e/ou conteúdo para serem mo-
dificados, esses parâmetros têm que ser verificados na própria operação. Isso é feito
logo no começo do método specificOp: precisamos de pelo menos um dos parâme-
tros definidos (título ou conteúdo); se ambos estão faltando, um erro de requisição
inválida é retornado (400). Caso um dos dois (ou ambos) estejam presentes, segue-se
para a realização da operação do caso de uso. Se tudo ocorrer bem, retornamos um
200, caso contrário um erro de requisição inválida é retornado (400).
Figura 8.6: A classe UpdateNoteOperation que define a operação do controlador de
alteração de notas e seus parâmetros requeridos.

Middleware de autenticação e autorização


Um middleware é software que se coloca entre uma camada e outra de um sistema.
Em aplicações Web, middleware podem ser utilizados, por exemplo, para verificar
a autenticidade de um usuário quando ele realiza uma requisição (autenticação), e
também se ele tem acesso ao recurso requisitado (autorização). No caso de aplica-
ções Node.js que utilizam o servidor Express isso é muito comum: de fato, o Express
inteiro é baseado em interposição de funções de middleware. Essas funções são exe-
cutadas durante o ciclo de vida de uma requisição HTTP ao Express.
De fato, aqui nesta camada não estamos necessariamente falando de tecnologias espe-
cíficas. Porém, a ideia de realizar uma função antes de uma operação específica requi-
sitada é, na verdade, independente de tecnologia. Em nosso caso, a cada requisição
HTTP, precisamos verificar se ela contém um token válido de acesso e se o usuário que
fez a requisição pode de fato realizar aquela operação. Para nossa aplicação isso é sim-
ples: como geramos o token de acesso no Sign in ou Sign up do usuário, esperamos
que o frontend (ou qualquer outra aplicação-cliente) envie-nos esse token para veri-
ficarmos se o usuário está validamente logado. Além disso, ao realizar uma operação,
precisamos verificar se o recurso — em nosso caso, a nota —, é de fato daquele usuá-
rio. Dessa forma, resolvi implementar um middleware que verifica o token enviado
na requisição e se o identificador contido na requisição é o mesmo do encapsulado
no token. Com isso autenticamos e autorizamos o usuário a cada requisição.
Para isso, primeiro desenvolvi uma simples interface de middleware. Seu código é
apresentado na Figura 8.7. Essa interface é bem simples e similar a uma requisição
HTTP: ela contém um método handle que recebe uma requisição e retorna uma res-
posta.

Figura 8.7: Interface de Middleware para nossa aplicação.

O middleware de autenticação é apresentado na Figura 8.8. Basicamente ele acessa o


token e o identificador do usuário que está realizando a requisição; verifica se estão de
fato presentes na requisição (retornando um erro de forbidden — 403 — caso con-
trário); verifica se o token é válido e não expirou (retornando novamente um erro de
forbidden — 403 — caso contrário); decodifica o payload e retorna ok — 200 — caso
o id do payload case com o id do usuário que realizou a requisição; e um forbidden
novamente — 403 — caso contrário.
A operação toda também é envolta de um bloco try-catch: caso alguma exceção seja
lançada, retorna-se um erro interno de servidor — 500. O tratamento de exceção
aparece mais por excesso de zelo do que por necessidade; de fato, a única operação
que pode lançar exceção aqui é a verificação do token. Porém, como o middleware
é executado logo na entrada da requisição, é importante manter o try-catch, como
nos controladores, porque assim não existe maneira de ocorrer uma exceção e não
retornarmos nada: todas as possíveis entradas à API são guardadas pelo tratamento
de exceção.
Um detalhe importante é que os erros aqui nesse middleware estão genéricos. Uma
Figura 8.8: A classe Authentication que define um middleware de autenticação e au-
torização para nossa aplicação.

refatoração recomendada seria criar classes de erros específicos para utilizá-los aqui
(por exemplo, poderíamos ter um UserUnauthorizedError para o caso do id do usuá-
rio no payload ser diferente do id da requisição).
Outro detalhe importante é que aqui poderíamos fazer modificações para permitir à
aplicação realizar a autenticação e autorização dos usuários a partir de serviços de re-
des sociais, como Twitter e Facebook, e do Google. Aqui precisaríamos receber, jun-
tamente com a requisição, a informação de qual serviço de autenticação está sendo
utilizado no cliente, para controlar o acesso dos usuários. Com base nessa informa-
ção, chamaríamos os serviços específicos — possivelmente por meio de interfaces para
evitar o acoplamento —, dependendo do serviço específico utilizado.
Próxima camada: frameworks & drivers
Na próxima camada analisaremos a implementação dos detalhes de implementação
relacionados com as tecnologias utilizadas por nossos casos de uso. Em particular,
veremos a implementação dos repositórios — no caso, escolhi o MongoDB como
tecnologia de banco de dados para nosso exemplo —, da criptografia — escolhi a
biblioteca bcrypt para isso —, e o gerenciador de tokens — nesse caso, escolhi a bi-
blioteca jsonwebtoken. Essa camada contém basicamente adapters com glue code para
conectar as chamadas às interfaces às implementações concretas utilizadas.
9 | Frameworks & Drivers

este capítulo apresento os detalhes de implementação relacionados com as tecno-


logias utilizadas por nossos casos de uso: esta é a camada de frameworks & drivers.
Como comentado anteriormente ela consiste em basicamente glue code para que
possamos concretizar as interfaces que representam abstrações de serviços utilizados
pelas camadas mais internas da arquitetura.
Repare que interessante: nos capítulos referentes às camadas mais internas, as discus-
sões giravam em torno das regras de negócio do domínio e da aplicação. Eram questões
mais relacionadas com a nossa aplicação: usuários e notas, senhas, títulos e conteúdos,
usuários podendo ou não conter notas com títulos duplicados, etc. Quanto mais
caminhamos para as camadas mais externas, mais as discussões passaram a ser sobre
detalhes de implementação, sobre tecnologias específicas. Neste capítulo falaremos so-
bre mecanismos de bancos de dados, schemas, conexão com o banco, JSON, ORMs,

81
criptografia, formato do hash, JSON Web Tokens; assim por diante. Isso concorda
bastante com nossas discussões sobre arquitetura no Capítulo 3: lá comentávamos
sobre a ideia de colocar os dados e regras de negócio críticos no cerne de nossa aplica-
ção, separando-os dos detalhes mais sórdidos de implementação.
Em particular, no nosso exemplo tínhamos principalmente três tipos de serviços abs-
traídos em interfaces: os repositórios, o serviço de criptografia, utilizado para cifrar a
senha de um usuário, e o gerenciador de tokens.

Repositórios
A abstração de repositórios de dados como implementada em nossa aplicação é sim-
plificada. O padrão de projeto Repository, originalmente descrito no livro Patterns of
Enterprise Application Architecture de Fowler (2002), e também discutido no Domain-
Driven Design: Tacking Complexity In the Heart of Software de Evans (2003), é bem
mais complexo, envolvendo mais de uma camada de abstração (um DataMapper e
um Repository com operações configuráveis). Não estou dizendo aqui que o padrão
original não é útil, apenas que no atual estágio de nossa aplicação, com o tamanho que
ela tem, e para o propósito que temos de aprender a Arquitetura Limpa, não faz sen-
tido implementar o padrão completo. Da maneira como fizemos, utilizando apenas
uma interface para cada repositório — UserRepository e NoteRepository —, a solu-
ção é mais simples: basta prover uma implementação concreta de cada repositório na
tecnologia desejada e configurar tudo na camada de configuração.
Para simplificar, aqui também não estou lidando com questões de concorrência, que
poderiam envolver a utilização de transações e locks para lidar com possíveis confli-
tos. Em nosso caso esses poderiam ocorrer, por exemplo, se um usuário estivesse edi-
tando a mesma nota em dispositivos diferentes (ou em abas diferentes em um mesmo
browser, por exemplo). Isso de fato poderia causar algum conflito: por exemplo, em
um dispositivo faz alguma alteração E, concomitantemente, requisita a remoção da
mesma nota em outro dispositivo. No atual estágio de nossa implementação, não sei
precisar o que aconteceria em tal situação. Dessa forma, friso a importância de lidar
com esses possíveis conflitos em uma aplicação real utilizada em produção.
Parece-me igualmente possível tratar desses interesses em uma Arquitetura Limpa.
Por exemplo, é possível deixar as operações dos repositórios atômicas, para evitar al-
guns tipos de conflito; por outro lado, seria possível também deixar as operações dos
próprios casos de uso igualmente atômicas, evitando outros tipos de problemas. Re-
pare, de outro modo, que uma possível solução na utilização de transações seria a
invocação delas no controlador, já que se trata de um interesse de mais baixo nível.
Outra opção, um pouco mais sofisticada, seria utilizar o padrão Unit of Work — uni-
dade de trabalho — em conjunto com o Repository. Esse padrão permite manter uma
lista de objetos afetados por uma transação e coordena as escritas das mudanças e a
resolução de problemas de concorrência. Basicamente uma unidade de trabalho re-
presenta várias coisas que precisam acontecer juntas. Ela geralmente permite guardar
objetos em memória pela vida útil de uma requisição para que não seja necessário fa-
zer chamadas repetidas ao banco. Uma unidade de trabalho é então responsável por
fazer checagens de modificações nos objetos e fazer a descarga de quaisquer mudanças
de estado ao final da requisição. Quando se finaliza a transação, a unidade de traba-
lho consegue saber o que precisa ser feito para alterar a base como resultado de uma
operação. Esse padrão é descrito no livro de Fowler (2002) e também em sua página
na Web1 .
Enfim, a solução adotada dependerá do tipo de conflito que pode ocorrer na aplica-
ção, da escala da aplicação, da tecnologia utilizada, etc2 .
Voltando ao nosso caso, onde deixamos a cargo do banco em si de tratar possíveis
conflitos, resolvi utilizar um banco não-relacional, o MongoDB3 , pelo mesmo mo-
tivo de simplicidade. No MongoDB os dados são armazenados como documentos no
formato JSON (JavaScript Object Notation). O JSON, por sua vez, é formatado em
pares chave/valor. Nos documentos JSON, campos e valores são separados por um
‘:’, pares de campo e valor são separados por ‘,’, e conjuntos de campos são encapsu-
lados por chaves (‘{}’). O uso do JSON no MongoDB facilita sua integração com o
JavaScript e, consequentemente, com o TypeScript, já que o formato do JSON é sin-
taticamente idêntico ao código para criar objetos no JS. Por conta dessa similaridade,
um programa JS (e, consequentemente, TS) pode facilmente converter dados JSON
em objetos nativos JS. Isso, em contrapartida, favorece o uso do MongoDB.
No MongoDB, documentos são armazenados em coleções (collections). Uma coleção
1
Unit of Work – https://martinfowler.com/eaaCatalog/unitOfWork.html
2
Uma questão interessante no StackOverflow discute algumas dessas soluções
em um caso específico – https://stackoverflow.com/questions/50871171/
how-do-you-use-transactions-in-the-clean-architecture. Além disso, o conjunto de posts no fi-
nal da seguinte página explica de uma maneira simples e efetiva como implementar o Unit of Work em conjunto com o
Repository com um ORM (apesar do código estar em Python, imagino que ele possa ser portado para outras linguagens
como o TypeScript) — https://www.cosmicpython.com/.
3
MongoDB – https://www.mongodb.com/pt-br
é análoga a uma tabela em um banco de dados relacional; porém, a coleção é bem
mais flexível pois não estabelece uma estrutura fechada (um schema). De fato, docu-
mentos dentro de uma mesma coleção podem ter diferentes campos. O MongoDB,
como um banco de dados NoSQL, é considerado, então, schemaless, pois não requer
a pré-definição de schemas como exige um banco de dados relacional. Na verdade,
seu sistema de gerenciamento de banco de dados (SGBD) apenas requer um schema
parcial à medida que os dados são escritos, explicitamente listando coleções e índices.
Para nós isso significa que não precisamos definir as coleções à priori, podemos sim-
plesmente ir adicionando documentos em uma dada coleção e o SGBD se encarrega
de armazená-los da forma mais adequada.
Para o uso do MongoDB no TypeScript, decidi utilizar o driver nativo oficial para o
Node.js fornecido pelo próprio MongoDB. Para conectar com o banco, é necessário
criar um cliente MongoDB a partir de uma conexão. Esse cliente é criado a partir de
uma chamada a função connect. Uma vez conectado, a ideia é reutilizar o mesmo
objeto cliente, já que o próprio driver se encarrega de gerenciar o pool de conexões.
Dessa forma, criei um objeto MongoHelper para realizar a conexão e oferecer funções
úteis de acesso ao banco. O código desse objeto é apresentado na Figura 9.1. A função
connect é utilizada quando se sobe a aplicação para realizar a conexão; disconnect
fecha a conexão. A função getCollection dá acesso a uma coleção do banco e a função
clearCollection limpa uma coleção, apagando todos os seus documentos. A partir
do objeto Collection retornado por getCollection é possível adicionar, atualizar e
remover documentos de uma coleção.
Para dar acesso aos usuários e notas em si implementei dois repositórios MongoDB:
o MongodbUserRepository, que implementa a interface UserRepository (descrita no
Capítulo 7) e o MongodbNoteRepository, que implementa a interface NoteRepository
(também mencionada no Capítulo 7). O código do MongodbUserRepository é mos-
trado na Figura 9.2 e o código do MongodbNoteRepository é mostrado na Figura 9.3.
Para tratar dos identificadores, resolvi utilizar os próprios ids gerados pelo MongoDB
na aplicação. De fato, por padrão, o MongoDB gera um identificador único que é
atribuído ao campo _id em um novo documento antes de adicioná-lo a uma coleção.
Porém, precisamos mapear os identificadores dos campos _id do MongoDB para os
campos id de nossa aplicação. Para isso, criei duas estruturas de dados: o MongodbUser,
que possui todos os campos do usuário — email e password — e, além disso, o _id;
e o MongodbNote, seguindo a mesma lógica. Para mapear os identificadores criei o mé-
todo withApplicationId, que recebe a estrutura de dados no formato armezanado
no MongoDB — por exemplo, MongodbUser —, e retorna a estrutura de dados cor-
Figura 9.1: Objeto auxiliar MongoHelper para lidar com conexões e funções de acesso
ao banco MongoDB.

respondente da aplicação — por exemplo, UserData —, mapeando o campo _id para


id.

Os outros métodos são utilizados para acessar os dados conforme as operações defini-
das nas interfaces dos repositórios. No MongodbUserRepository (Figura 9.2) há uma
implementação do findAll, que retorna todos os usuários no banco. Aqui precisa-
mos acessar o objeto MongoHelper descrito anteriormente para acessar as coleções. No
caso, acessamos a coleção de nome users e, para retornar todos os usuário, chamamos
o método find, encadeado de uma chamada ao método toArray, que já transforma
os resultados em um vetor no próprio TypeScript. Como os usuários no banco pos-
suem o identificador no campo _id, como comentado anteriormente, para mapeá-lo
ao campo id da aplicação, chamo a função de ordem maior map passando como parâ-
metro o método withApplicationId. Isso faz com que o método seja aplicado a cada
um dos elementos do vetor, retornando, assim, um vetor de usuários como especi-
ficados na aplicação (veja como o uso da higher-order function deixou o código bem
mais elegante aqui, tornando desnecessário o uso de um laço).
O método findByEmail é utilizado para recuperar um usuário a partir de seu email.
Aqui também utilizamos a função withApplicationId para mapear o identificador.
Repare que caso o usuário não seja encontrado, retornamos null. Essa não é uma boa
prática: o uso do null é um bad smell e de fato pode trazer problemas para a aplica-
ção. Como o utilizo em um contexto bem restrito, preferi mantê-lo aqui, lembrando
Figura 9.2: Classe MongodbUserRepository que implementa o repositório de notas
utilizando o banco de dados MongoDB.

da ideia de que nossos sistemas são orgânicos e deveriam passar por refatorações e al-
terações contínuas. De fato, comecei a realizar essa alteração, utilizando o Either para
retornar um NotFoundError em vez do null, mas no meio do caminho desisti e resolvi
deixar como está. Fiz isso justamente para lembrar do fato que nossas aplicações não
são estáticas e geralmente contêm algum detalhe que necessita de melhoria.
A alteração é simples mas requer modificar diversas classes do sistema, pois o uso do
repositório aparece em muitas delas. Como eu havia definido a interface dos repositó-
Figura 9.3: Classe MongodbNoteRepository que implementa o repositório de notas uti-
lizando o banco de dados MongoDB.
rios como retornando uma Promise empacotando a estrutura de dados — UserData
ou NoteData, conforme o caso —, a única forma de mostrar que o dado não foi en-
contrado seria retornando null. Se fôssemos realizar a alteração, poderíamos come-
çar modificando as próprias interfaces para que retornassem uma Promise empaco-
tando um Either<NotFoundError, UserData>, por exemplo, no caso do repositório
de usuários. Outra opção seria usar o Option, uma alternativa mais simples ao Either
que pode representar um objeto retornado ou nada (None)4 .
Apesar de retornar null ser de fato uma má prática, nesse caso específico não há tanto
problema porque só temos duas opções: ou achamos o usuário — ou a nota — e,
nesse caso, o retornamos; ou não o achamos e, nesse caso, retornamos null (algo como
que representando a ausência do objeto buscado). Nos outros casos em que há mais
tipos de erros ficaria muito mais complicado usar o null, já que a sua semântica não
seria óbvia.
O método add é utilizado para adicionar um usuário ao banco. Repare que eu crio
um clone do usuário antes de inseri-lo. Faço isso porque a função insertOne do dri-
ver do MongoDB altera o objeto enviado como parâmetro, adicionando o _id nele.
Como não queremos alterar os dados originais, inserimos um clone com o _id nulo.
O método retorna o usuário adicionado com o identificador mapeado para o id da
aplicação.
Os métodos na classe MongodbNoteRepository seguem a mesma lógica. Além dos mé-
todos para encontrar todas as notas ou uma nota específica, temos métodos para
remover — remove — e alterar uma nota — updateTitle e updateContent. Esses
métodos utilizam as funções do driver do MongoDB para realizar essas operações:
deleteOne para remover a nota e updateOne para alterar a nota.

E os “Mapeadores” Objeto-Relacionais (ORMs)?


Muitos sistemas utilizam ORMs para relacionar objetos do sistema com tabelas do
banco de dados. Como ficaria o uso de ORMs com a Arquitetura Limpa?
A primeira coisa que devia ficar clara é que não existe mapeamento entre objetos e
relações (tabelas): objetos são entidades diversas de tabelas. Tabelas são, na verdade,
estruturas de dados. Uma das ideias principais dos objetos é que os seus dados são
4
Acabei de conhecer a biblioteca fp-ts (https://github.com/gcanti/fp-ts) que implementa algumas carac-
terísticas de programação funcional no TypeScript. Ele oferece implementações tanto do Either quanto do Option.
implícitos e, de maneira geral, ficam encapsulados (não são acessíveis diretamente). O
que os objetos expõem são seus métodos que, na realidade, implementam comporta-
mento.
Em uma estrutura de dados, por outro lado, os dados estão completamente acessíveis
de fora, as funções é que ficam implícitas. Deste ponto de vista podemos até conside-
rar objetos e estruturas de dados como entidades opostas5 :

• Um objeto é um conjunto de funções que operam sobre elementos de dados


implícitos; e
• Uma estrutura de dados é um conjunto de elementos de dados operados por
funções implícitas.

Sendo assim, o que pode haver, na verdade, são mapeadores de dados que transfor-
mam dados vindos de tabelas em dados utilizados em estruturas de dados dentro do
programa (lembre-se que utilizamos DTOs para os dados dos usuários e das notas;
dessa forma, os dados de uma tabela User podem ser transferidos para os dados de
um UserData, por exemplo). Esses dados podem em um segundo momento serem
carregados em um objeto (por exemplo, no momento de sua instanciação).
Ok, mas e os ORMs, como podemos utilizá-los em uma Arquitetura Limpa? Uma
coisa que deve ficar clara é que se utilizarmos ORMs que requerem a anotação de
entidades do domínio, então estaremos ferindo a regra de dependência, pois elemen-
tos da camada de entidades dependeriam de elementos de uma tecnologia específica
(no caso, de um framework, por exemplo). Não sou contra tais frameworks utiliza-
dos desta forma, a única coisa que afirmo é que se fizermos assim, ferimos a principal
restrição da Arquitetura Limpa: a regra de dependência.
Há alternativas? Sim. Se utilizamos, por exemplo, a definição de esquemas separados
das entidades (alguns ORMs apoiam esse tipo de mecanismo), então é possível utili-
zar um ORM sem sujar as entidades. Dessa forma, poderíamos, por exemplo, man-
ter esses esquemas na camada de frameworks & drivers, pois tratam-se de detalhes de
implementação. O conjunto de posts referenciado na nota de rodapé do começo da
seção sobre Repositórios — https://www.cosmicpython.com/ — mostra uma
maneira efetiva de se utilizar um ORM — o SQLAlchemy — juntamente com os pa-
drões Unit of Work e Repository em Python (de fato isso é explicado com detalhes no
5
Classes vs. Data Structures – https://blog.cleancoder.com/uncle-bob/2019/06/16/
ObjectsAndDataStructures.html
livro Architectural Patterns with Python de Harry Percival e Bob Gregory, uma ótima
pedida para quem se interessa por esse assunto). Atualmente estou testando o uso do
Prisma (https://www.prisma.io/) juntamente com esses padrões — UoW e Re-
pository — no Node.js. Parece-me completamente possível e temos as vantagens de
que o ORM de maneira geral é mais seguro do que o uso de queries puras.
Por outro lado, o uso do padrão Active Record 6 diretamente nas entidades, que re-
quer adicionar a elas operações de acesso ao banco de dados me parece impossível
na Arquitetura Limpa. Isso porque a própria definição do padrão, que exige que
se utilize código relacionado com persistência na própria entidade de domínio, fere
frontalmente a ideia de separar dados e regras de negócio do domínio de detalhes de
implementação. Por outro lado me parece ser possível utilizar o Active Records nas
estruturas de dados, o que, de fato, não feriria a regra de dependência.
O mais interessante é notar que, na Arquitetura Limpa, essas decisões de projeto po-
dem ser feitas sem que as regras de negócio sejam afetadas. Mais que isso: é bem menos
oneroso fazer uma alteração dessa natureza quando as coisas estão bem separadas.

Criptografia
Para criptografar a senha do usuário a ser guardada no repositório, precisamos de um
cifrador que receba a string com a senha original e retorne-a criptografada. A inter-
face criada para abstrair essa operação comentada no Capítulo 7 é o Encoder. Essa in-
terface contém duas operações: encode para cifrar uma string; e compare para, dada
uma string normal e uma criptografada, verificar se batem de acordo com o algo-
ritmo utilizado.
No JavaScript é muito comum a utilização da biblioteca bcrypt para criptografia. O
bcrypt é um método de criptografia do tipo hash para senhas. Ele é baseado no Blow-
fish e foi criado por Niels Provos e David Mazières e apresentado na conferência da
USENIX em 1999. Esse método utiliza um salt, ou seja, um dado aleatório utilizado
como entrada adicional em funções de uma via que criptografam dados. O salt é
incorporado no próprio dado criptografado. O bcrypt gera um hash no seguinte for-
mato:

$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
6
Active Record – https://www.martinfowler.com/eaaCatalog/activeRecord.html
Onde:

• $2b$ é o identificador do algoritmo de hash (bcrypt);

• 10 é o fator de custo (210 i.e. 1.024 iterações);

• N9qo8uLOickgx2ZMRZoMye é o salt de 16 bytes (128-bit), codificado com Radix-64


com 22 caracteres; e

• IjZAgcfl7p92ldGxad68LJZdL17lhWy é o hash de 24 bytes (192-bit), codificado


com Radix-64 em 31 caracteres.

Na Figura 9.4 é apresentado o código da classe BcryptEncoder que implementa um


cifrador utilizando a biblioteca bcrypt. O fator de custo (rounds) pode ser configu-
rado no momento da instanciação do objeto, mas tem um valor padrão de 10. Como
será visto na camada Principal e de Configuração, esse parâmetro será configurado a
partir de uma variável de ambiente. O método encode simplesmente delega a cripto-
grafia ao método hash do bcrypt, que recebe uma string simples e o fator de custo, e
retorna o hash correspondente. O salt é criado a partir da chamada ao hash utilizando
o fator de custo desejado e embutido na própria string retornada, de acordo com o
formato descrito anteriormente. O método compare simplesmente delega a compa-
ração da string original (plain) com a string criptografada (hashed) ao compare do
bcrypt. Como o salt é embutido no próprio hash, não precisamos nos preocupar com
ele.

Gerenciador de tokens
Para gerenciar os tokens em nossa aplicação, decidi usar o JSON Web Token — JWT.
JWT (“jot”) é um padrão para transmitir declarações (claims) de maneira segura na
Web. Ele é utilizado atualmente nos principais frameworks Web, como o django7 e o
Express8 .
O JWT é formado por três partes9 :
7
django – https://www.djangoproject.com/
8
Express – https://expressjs.com/pt-br/
9
JWT – https://jwt.io/introduction
Figura 9.4: Classe BcryptEncoder que implementa um cifrador utilizando a biblioteca
bcrypt.

1. Um cabeçalho com informação do algoritmo de criptografia utilizado (no campo


alg) e o tipo de token (no campo tok e, geralmente, com o valor jwt). Por exem-
plo:

2. O dado em si que está sendo transmitido (o payload). O payload pode conter


alguns campos pré-definidos como iss (issuer — o gerador do token) e exp (a
informação de expiração). Um exemplo de payload é o seguinte:

3. A assinatura. Para criar a parte da assinatura, junta-se o cabeçalho criptogra-


fado, o payload codificado com Radix-64, uma senha (parecida com o salt uti-
lizado no algoritmo bcrypt comentado anteriormente, mas não pública) codi-
ficado com Radix-64, o algoritmo especificado no cabeçalho, tudo isso cripto-
grafado. A senha só é conhecida pelo servidor e é utilizada para garantir que
não houve modificação do token pelos clientes.
Por exemplo, utilizando o algoritmo HMAC SHA256, o cabeçalho e o payload
exemplo descritos anteriormente, a assinatura será criada da seguinte forma:

A saída final do JWT é formada por três strings em Base64-URL separadas por pontos
(‘.’) e que podem ser analisadas em ambientes HTML e HTTP, sendo, de maneira geral,
mais compacta quando comparada com padrões baseados em XML como o SAML. O
JWT formado pelo cabeçalho e payload exemplos descritos anteriormente e assinados
com uma senha fica assim:

No JavaScript, a biblioteca jsonwebtoken é muito utilizada para a manipulação de


JWTs. Escolhi ela para utilizar em nossa aplicação. Sendo assim, desenvolvi um adap-
ter chamado JwtTokenManager que implementa a interface TokenManager, comentada
no Capítulo 7. Essa interface contém duas operações: sign, que recebe o payload e um
período de expiração opcional e retorna um JWT assinado; e verify que, dada uma
string representando um possível token, verifica se ele de fato é válido. Uma grande
vantagem do JWT é que ele é stateless. Ou seja, não precisamos salvá-lo no banco; com
as informações contidas no próprio JWT, a função verify é suficiente para verificar a
validade do token.
Na Figura 9.5 é listado o código da classe JwtTokenManager. A senha utilizada para
criptografar o JWT é configurada pelo atributo secret (ela funciona como um salt,
comentado anteriormente). Como veremos no próximo capítulo, a secret pode ser
configurada a partir de uma variável de ambiente. O método sign assina o JWT a
partir da informação que queremos guardar, o payload. Em nosso caso trata-se ape-
nas do id do usuário, mas isso pode ser modificado conforme a necessidade (Payload
é um tipo definido juntamente com a interface TokenManager na Camada de Casos de
Uso). Caso o método seja chamado com uma expiração, esta é utilizada para definir
quando o JWT expira (por meio da propriedade expiresIn). Caso esta não seja defi-
nida, utilizamos a expiração padrão de um mês. O método verify verifica se o JWT
é válido já decodificando o payload e retornando-o empacotado no Either. Se algum
erro ocorrer, ele é retornado da mesma forma empacotado no Either.

Figura 9.5: Classe JwtTokenManager que implementa um gerenciador de JWTs utili-


zando a biblioteca jsonwebtoken.
10 | Principal & Configuração

camada Principal & de Configuração contém todos os módulos necessários para


configurar e executar a aplicação. Em particular, fazem parte desta camada todos
os factories utilizados na instanciação dos elementos necessários a serem injetados
nas políticas de alto nível. Aqui também temos o módulo principal que será o ponto
de entrada da aplicação (em nosso caso um server.ts). Além disso, aqui configura-
mos todas as rotas de nossa API REST, nossos Middleware e variáveis de ambiente.

Adaptadores Express
Ao implementar os detalhes de como nossa aplicação executará, é necessário escolher
como isso será feito. Em particular como se trata de uma API REST, precisamos de

95
um framework de aplicativos Web do Node.js. Como comentado anteriormente, es-
colhi o Express, que se trata de um framework minimalista, por sua simplicidade.
Para utilizar o Express, precisamos adaptar as rotas que utilizaremos para funciona-
rem com nosso controlador Web definido na camada de adaptadores de interface (o
WebController). Em particular, precisamos adaptar as requisições e respostas (Request
& Response) do Express para nosso controlador. De fato, quando uma requisição for
realizada — por exemplo, por meio de um POST —, precisamos acoplar nosso contro-
lador para que ele realize a operação correspondente. Na Figura 10.1 é apresentado
nosso adaptador de rotas para o Express.

Figura 10.1: Função adaptRoute que adapta um WebController para ser utilizado com
o Express.

Precisamos de um adaptador para middleware também, já que a interface que cria-


mos é independente de tecnologia (só temos uma implementação, o middleware de
autenticação, como visto no Capítulo 8, mas poderíamos ter outras). A ideia aqui é
semelhante ao adaptador de rotas: precisamos acessar os dados vindos da requisição
e adaptá-los para o middleware. Em particular, o nosso middleware de autenticação
espera dois dados na requisição: o token de acesso — accessToken —, e o identifica-
dor do usuário que realizou a requisição — requesterId. Para o token de acesso, optei
por esperá-lo vindo em um header x-access-token; já o identificador do usuário de-
fini como vindo em um parâmetro ownerId no body da requisição. Na Figura 10.1 é
apresentado nosso adaptador de middleware para o Express.

Variáveis de ambiente (.env)


Uma boa maneira de tornar mais configurável a execução de aplicações de acordo
com o ambiente em que rodam é a utilização de variáveis de ambiente. Isso permite,
Figura 10.2: Função adaptMiddleware que adapta um Middleware para ser utilizado
com o Express.

por exemplo, utilizar um banco específico em um ambiente de teste e outro em um


ambiente de produção. No caso de nossa aplicação, deixei alguns parâmetros variáveis
de acordo com o ambiente. As variáveis utilizadas são apresentadas na Figura 10.3.

Figura 10.3: Variáveis de ambiente utilizadas em nossa aplicação com valores de exem-
plo.

Em particular, o caminho do banco — MONGO_URL — é configurável, fazendo com que


seja possível configurar um banco diferente para cada ambiente de execução. A porta
onde a aplicação aguardará requisições também é configurável por meio da variável
PORT. O desenvolvedor também pode configurar aqui a palavra-chave que será utili-
zada na assinatura dos JWTs (discutidos no Capítulo 9) e o custo do algoritmo Bcrypt
utilizado na aplicação (também discutido no Capítulo 9).
Para fazer uso dessas variáveis de ambiente em nossa aplicação, utilizei a biblioteca
dotenv1 , que permite carregar as variáveis no ambiente de execução do Node.js. Para
1
dotenv – https://www.npmjs.com/package/dotenv
isso, é necessário criar um arquivo chamado .env na raiz do projeto que contenha to-
das as variáveis. Depois, na primeira entrada da aplicação, basta adicionar o comando
require(‘dotenv’).config(), que as variáveis serão carregadas e acessíveis a partir do
process.env (um objeto que contém o ambiente do usuário e, assim, conterá também
as variáveis carregadas).
Como boas práticas de utilização de variáveis de ambiente, acho conveniente deixar
claro aqui o seguinte:

• Não é recomendável incluir o arquivo .env no controle de versão; o ideal é


apenas incluí-lo em cada ambiente em que a aplicação executará;
• Não é recomendável ter vários arquivos .env (por exemplo, um principal e um
de teste) dentro do mesmo projeto. As configurações devem variar de acordo
com as implantações, e não é desejável compartilhar valores entre ambientes.

Uma alternativa interessante ao dotenv é o dotenv-safe2 , que permite a utilização


de um arquivo exemplo com as variáveis de ambiente necessárias para a aplicação —
.env.example (esse arquivo poderia ser colocado em controle de versão, diferente-
mente do arquivo que contém os valores da variáveis).

Factories das bibliotecas: Repositórios, Criptografia e Ge-


renciador de Tokens
Para criar as instâncias necessárias para as políticas de alto nível de nossa aplicação —
em particular, para os casos de uso —, criei factory methods para gerar os repositórios
e os serviços de criptografia e gerenciamento de tokens. O código dessas fábricas é
apresentado na Figura 10.4. Cada função dessas está em um arquivo separado mas
deixei junto aqui por questões de espaço. Repare no uso das variáveis de ambiente
para configurar o Bcrypt e o JWT.
Uma vantagem da utilização desses factories é poder reutilizá-los em todos os lugares
em que esses objetos são necessários. Isso faz também com que seja mais fácil tro-
car essas tecnologias, pois bastaria ir em apenas um lugar — no factory — para, por
exemplo, trocar o tipo de serviço de criptografia, caso desejado.
2
dotenv-safe – https://www.npmjs.com/package/dotenv-safe
Figura 10.4: Factories para as tecnologias utilizadas em nossa aplicação.

Factories dos controladores e do middleware de autentica-


ção
Para que possamos utilizar os controladores descritos no Capítulo 8, é necessário criá-
los e configurá-los. Isso é feito aqui, na camada principal e de configuração. Basica-
mente, de acordo com o design que estabelecemos e discutimos no Capítulo 8, um
controlador é uma instância da classe WebController na qual é injetada a operação
específica daquele controlador. Por exemplo, no caso do controlador de cadastro de
usuários — Sign Up —, instanciamos o WebController injetando um objeto do tipo
SignUpOperation e neste, por sua vez, injeta-se o objeto do tipo SignUp, que refere-se
ao caso de uso de cadastro de usuários.
Qualquer configuração de concreções necessária para cada caso de uso também é feita
aqui. Por exemplo, o caso de uso de cadastro de usuários deve receber, por injeção
de dependência, um repositório de usuários, um serviço de criptografia e um serviço
de autenticação. O serviço de autenticação, por sua vez, necessita do repositório de
usuários, do serviço de criptografia e do serviço de gerenciamento de tokens. Como
todos os factories desses serviços já foram implementados, como mostrado na seção
anterior, basta usá-los aqui (por exemplo, o makeUserRepository para o repositório
de usuários e o makeTokenManager para o gerenciador de tokens).
Todos os controladores seguem essa mesma lógica. O código de cada um deles foi
incluído na Figura 10.5. Como comentado anteriormente, o middleware de autenti-
cação é bem parecido com os controladores pois é também uma porta de entrada na
API (eles fazem parte da camada de apresentação da API). Sendo assim, incluí o có-
digo do factory desse middleware na mesma figura. Embora o código de cada factory
apareça contiguamente aqui por questões de espaço, na aplicação eles são localizados
cada um em seu arquivo próprio.

Configuração dos outros middleware


No Express é necessário utilizar alguns middleware para que as requisições ao servidor
sejam corretamente tratadas. Em nosso caso, configuramos primeiramente o parsing
do body das requisições como JSON. Isso faz com que o middleware realize a análise
sintática do corpo da requisição em JSON, o formato esperado nas requisições.
Em segundo lugar, configuramos o middleware de CORS (cross-origin resource sha-
ring), mecanismo de segurança que permite estabelecer quais recursos restritos de
uma aplicação Web podem ou não ser requisitados a partir de quais domínios (di-
ferentes dos quais os recursos foram servidos originalmente), e também quais cabe-
çalhos e métodos podem ser utilizados nas requisições, entre outros. Em resumo,
o CORS define uma forma pela qual um navegador e um servidor podem interagir
para determinar a segurança (ou falta dela) de uma solicitação de origem cruzada.
Em nosso caso queremos permitir o acesso a nossa aplicação a requisições originadas
em quaisquer domínios (access-control-allow-origin), com quaisquer cabeçalhos
(access-control-allow-headers) e com quaisquer métodos HTTP (access-control-
allow-methods).

Por último é necessário definir o tipo de conteúdo do corpo da resposta também


como JSON. Na Figura 10.6 é apresentado o código de configuração dos middleware
(setup-middleware) que aplica as funções de cada configuração ao app Express. Os
comentários indicam que cada função foi escrita em seu próprio arquivo, separada-
mente da função que aplica as configurações ao aplicativo Express.

Configuração das Rotas


Para que nossa API possa ser acessada remotamente por meio dos diferentes métodos
HTTP, é necessário configurar as rotas do Express, segundo o estilo arquitetural REST.
Figura 10.5: Factories para os controladores Web e middleware de autenticação utili-
zados em nossa aplicação.
Figura 10.6: Funções para configuração dos middleware do Express.

Na Figura 10.7 é apresentado o código para a realização dessas configurações. Para


a realização do cadastro do usuário, os clientes realizarão um POST na URL da API
acrescida de /signup; similarmente para o login do usuário. Para a criação de notas
também deverá ser realizado um POST na URL da API acrescida de /notes.
Para a remoção de uma nota, deverá ser realizado um DELETE na URL da API acrescida
de /notes/:noteId (onde noteId representa o identificador da nota a ser removida).
A mesma lógica é utilizada para a atualização de uma nota, entretanto com o método
PUT. Finalmente, para carregar todas as notas de um usuário, deverá ser utilizado um
GET na URL da API acrescida de /notes. Repare nos usos dos respectivos factories dos
controladores para cada endpoint da aplicação.

Módulo principal
Finalmente chegamos ao módulo principal por onde nossa aplicação começará a ro-
dar. Antes de poder rodar a aplicação, precisamos de um módulo que realizará as
Figura 10.7: Código para configuração das rotas de nossa aplicação.

configurações das rotas e dos middleware no aplicativo do Express. Esse aplicativo —


app — será então utilizado no ponto de entrada a aplicação. A partir daí, temos um
módulo — server.ts — que utiliza o dotenv para carregar as variáveis de ambiente,
conecta no servidor do MongoDB e coloca o aplicativo Express para escutar na porta
configurada. O código do módulo principal de nossa API, juntamente com o código
de configuração do aplicativo é mostrado na Figura 10.8

Figura 10.8: Código do módulo de entrada de nossa API.


11 | Conclusão
hegando ao final deste livro espero que você esteja convencido de que Arquite-
tura Limpa não passa de Arquitetura de Software com um mínimo de bom senso;
da mesma forma que código limpo deveria ser chamado apenas de código (pois todo
ele deveria ser, de qualquer forma, minimamente limpo). Como bem colocado pelo
Maurício Aniche no prefácio deste livro, devemos desenvolver arquiteturas simples,
mas não simplórias (pois as últimas serão verdadeiramente um “tiro no pé”, privando-
nos de adicionar valor ao sistema de maneira sustentável e por um longo período de
tempo).
Eu realmente acredito que os princípios delineados neste livro terão um impacto his-
tórico no desenvolvimento de software. Frequentemente observa-se uma certa resis-
tência ao desenvolvimento de arquiteturas limpas, como se em todo caso fosse algum
tipo de overengineering. Mas, lembre-se, quando Dijkstra recomendou não se utilizar
o GOTO de maneira indiscriminada em 1968, muitos se revoltaram, julgando-o louco.
No fim, o tempo o deu razão, assim como acredito que em um futuro breve o desen-
volvimento de arquiteturas limpas será mais comum do que o desenvolvimento de
arquiteturas sujas.
Espero que este livro seja útil para você em seu caminho de desenvolvedor de soft-
ware e que, no mínimo, você possa desenvolver arquiteturas mais bem estruturadas
quando for o caso.

104
Bibliografia
Beck Test driven development: By example. USA: Addison-Wesley Longman Pu-
blishing Co., Inc., 2002.

Beck, K.; Andres, C. Extreme programming explained: Embrace change (2nd edi-
tion). Addison-Wesley Professional, 2004.

Booch, G. Object-oriented analysis and design with applications. 3 ed. USA:


Addison Wesley Longman Publishing Co., Inc., 2004.

Cherny, B. Programming typescript: Making your javascript applications scale. 1st


ed. O’Reilly Media, 2019.

Cockburn, A. Hexagonal architecture. 2005.


Disponível em https://alistair.cockburn.us/
hexagonal-architecture/

Coplien, J.; Reenskaug, T. The dci paradigm: Taking object orientation into the
architecture world. In: Babar, M. A.; Brown, A. W.; Mistrik, I., eds. Agile
Software Architecture, cap. 2, Elsevier, p. 25–59, 2013.

Coplien, J. O.; Reenskaug, T. M. H. The data, context and interaction paradigm.


In: Proceedings of the 3rd Annual Conference on Systems, Programming, and Ap-
plications: Software for Humanity, Association for Computing Machinery, 2012,
p. 227–228 (SPLASH ’12, v.1).

Evans, E. Domain-driven design: Tacking complexity in the heart of software. USA:


Addison-Wesley Longman Publishing Co., Inc., 2003.

Feathers, M. Working effectively with legacy code. USA: Prentice Hall PTR, 2004.

105
Fielding, R. T.; Taylor, R. N. Architectural styles and the design of network-based
software architectures. Tese de Doutoramento, University of California, Irvine,
2000.

Fowler, M. Analysis patterns: Reusable objects models. USA: Addison-Wesley


Longman Publishing Co., Inc., 1996.

Fowler, M. Patterns of enterprise application architecture. USA: Addison-Wesley


Longman Publishing Co., Inc., 2002.

Fowler, M. UmlAsSketch. 2003a.


Disponível em https://martinfowler.com/bliki/UmlAsSketch.html

Fowler, M. Who needs an architect? IEEE Software, v. 20, n. 5, p. 11–13, 2003b.

Freeman, S.; Pryce, N. Growing object-oriented software, guided by tests. 1st ed.
Addison-Wesley Professional, 2009.

Gamma, E.; Helm, R.; Johnson, R.; Vlissides, J. Design patterns: Elements
of reusable object-oriented software. USA: Addison-Wesley Longman Publishing
Co., Inc., 1995.

Jacobson, I. Object-oriented software engineering: A use case driven approach.


USA: Addison Wesley Longman Publishing Co., Inc., 2004.

Martin, R. C. Clean code: A handbook of agile software craftsmanship. 1 ed. USA:


Prentice Hall PTR, 2008.

Martin, R. C. Clean architecture: A craftsman’s guide to software structure and


design. 1st ed. USA: Prentice Hall Press, 2017.

Parnas, D. L. On the criteria to be used in decomposing systems into modules.


Commun. ACM, v. 15, n. 12, p. 1053–1058, 1972.

Reenskaug, T. The common sense of object oriented programming. 2009.


Disponível em https://dci.github.io/documents/commonsense.pdf

Richards, M.; Ford, N. Fundamentals of software architecture: An engineering


approach. O’Reilly Media, 2020.
Stemmler, K. GraphQL’s greatest architectural advantages. 2019.
Disponível em https://khalilstemmler.com/articles/graphql/
graphql-architectural-advantages/

Stemmler, K. Client-side architecture basics [guide]. 2020.


Disponível em https://khalilstemmler.com/articles/
client-side-architecture/introduction/

Taylor, R. N.; Medvidovic, N.; Dashofy, E. M. Software architecture: Foun-


dations, theory, and practice. Wiley Publishing, 2009.

Vaggalis, N. C# guru – an interview with Eric Lippert. 2014.


Disponível em https://www.i-programmer.
info/professional-programmer/i-programmer/
7154-c-guru-an-interview-with-eric-lippert.html

Wirfs-Brock, R.; McKean, A.; Jacobson, I.; Vlissides, J. Object design: Roles,
responsibilities, and collaborations. Pearson Education, 2002.

Xu, A. System design interview: An insider’s guide. Publicado Independentemente,


2020.

Você também pode gostar