Escolar Documentos
Profissional Documentos
Cultura Documentos
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
6 Entidades 45
7 Casos de Uso 54
8 Adaptadores de Interface 69
11 Conclusão 104
viii
1 | Introdução
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!
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.
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
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.
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.
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.
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.
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 .
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.
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.
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):
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
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.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.
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.
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
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.
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.
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:
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.
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:
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.
Figura 10.3: Variáveis de ambiente utilizadas em nossa aplicação com valores de exem-
plo.
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.
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.
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.
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.
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.
Wirfs-Brock, R.; McKean, A.; Jacobson, I.; Vlissides, J. Object design: Roles,
responsibilities, and collaborations. Pearson Education, 2002.