Você está na página 1de 239

Machine Translated by Google

SEBASTIAN BUCZY ÿ ESQUI


IMPLEMENTAÇÃO

A ARQUITETURA LIMPA
Machine Translated by Google

Prefácio 5 ................................................ .................................................. ......

Por que escrevi este livro? .................................................. ...........................................5 Era


impulsionada por ferramentas .. .................................................. .................................................. ..5

Para quem é este livro? .................................................. ................................... 7

O que você encontrará neste livro? .................................................. .................. 7

Os princípios básicos da Arquitetura Limpa .................................................. ....................... 8

Para que serve tudo isso? .................................................. .................................................. ..8

Organização do código - fatiamento horizontal............................................. .........................


Resumo do capítulo 12......................... .................................................. ............................18

Implementação referencial 20 ............................................. ...........................


Isenção de responsabilidade ................................................. .................................................. ...........20
Fluxo de controle na Arquitetura Limpa .......................................... ...........................20

Requisitos de negócio................................................ .............................................22


Implementação .. .................................................. .................................................. .23 Resumo
do capítulo ............................................. .................................................. ...30
As modificações da Arquitetura Limpa 32 ............................................. ...............

Dilema do apresentador................................................. .................................................. .32

Livrando-se do limite de entrada ............................................. ....................................35 Design


alternativo de casos de uso ....... .................................................. ......................36 Resumo do
capítulo ........................... .................................................. ........................40 Injeção de

dependência .................................................. ................................. 42

Abstrações e classes em todos os lugares!......................................... ............................42


Abstrações na Arquitetura Limpa ............... .................................................. .....43
Inversão de controle............................................... .................................................. 45
Contêiner IoC vs Localizador de Serviço ............................................. ................................47

Injeção de Dependência vs Configuração......................................... ........................48 Resumo


do capítulo ....................... .................................................. ...........................49
CQRS 50 ................................................ .................................................. ..........

Introdução ................................................. .................................................. .........50


O que isso tem a ver com a Arquitetura Limpa?......................................... .........52

Pilha de leitura separada - por quê?......................................... ...........................................52


Machine Translated by Google

Pilha de leitura separada - como? .................................................. ...................................53


CQRS versus API REST ............ .................................................. ....................................57
CQRS x GraphQL................................................... .................................................. .58
Resumo do capítulo ............................................. .................................................. ...58
Limite nítido 59 ............................................. ...........................................

Uma palavra sobre complexidade ............................................. ................................................59


Dois mundos .................................................. .................................................. .........60
Limite entre a Aplicação e o Mundo Externo ........................................... ......61 Escrevendo
DTOs de entrada........................................ .................................................. ........61 Objetos
de valor ............................................. .................................................. .................61 Resumo
do capítulo .............................. .................................................. ...................66
Exemplo de ponta a ponta 67 .......................................... ...........................................
Onde começar? .................................................. .................................................. ...67

Esqueleto ambulante.................................................. .................................................. ...68


Caso de uso/interator de colocação de lances.................................. ....................................68
Entidades de Leilões e Licitações ........ .................................................. ....................................72
Interface de acesso a dados (repositório de resumos).................................... ...................76
Acesso a dados (repositório)......................... .................................................. ..............76
Concluindo nosso primeiro caso de uso .............................. .................................................. ......78
Código da embalagem ........................................... .................................................. ...........81
Implementando o caso de uso EndingAuction...................... ...................................90
Operações somente leitura............ .................................................. ................................101
Invertendo o controle com eventos ............. .................................................. ...................108
Lidando com outras preocupações transversais ....................... ....................................123
Resumo do capítulo....... .................................................. ...........................................128
Modularidade 130 ..... .................................................. ...........................................

O fardo do sucesso – crescimento e mudanças contínuas.................................. .....130


Coesão e módulos ........................................... ..................................................130
Código de empacotamento por recurso.................................... ..............................131 Módulos
e flexibilidade de design de interiores.... .................................................. ...........131 Módulos
versus microsserviços................................... ................................................132
Módulos versus usuário................................................. ................................................133
Módulos versus contextos limitados ............................................. ....................................133
Machine Translated by Google

Implementação de módulos................................................ ........................................134


Módulos dependendo um do outro.... .................................................. ...................135 Estudo
de caso - plataforma de leilões......................... .................................................. ..139 Resumo
do capítulo............................................. .................................................. ...171

Teste 173 ................................................ .................................................. ......

Estratégia de teste e variações de recursos ............................................. ...........................173


Redescobrindo o teste unitário .................. .................................................. ...................183
testes orientados a estado e interação ........................... .............................................188 Teste
unitário de um módulo inteiro ................................................ ................................193 Resumo
do capítulo.................. .................................................. ................................201
Palavras finais 203 ................................................ ..................................................

Apêndice A: Migrando do legado 204 ........................................... ...............

Devo mesmo migrar? .................................................. ........................................204 Como


fazer?.... .................................................. .................................................. 204

“Não consigo parar de entregar novos recursos” ........................................... ........................205

Apêndice B: Introdução ao Event Sourcing 206 .......................................... .....

O que é fornecimento de eventos? .................................................. ...................................206


Exemplo simples de um agregado de Event Sourcing .... ................................................209
Persistência em Fornecimento de Eventos................................................. ...................................215
Projeções............. .................................................. .............................................232 Event
Sourcing versus um aplicação modular ................................................ ..........235

Bibliografia 237 ................................................ ................................................


Machine Translated by Google

PREFÁCIO

POR QUE ESCREVI ESTE LIVRO?

Este livro pretende ser um complemento ao livro Clean Architecture: A Craftsman's Guide to Software
Structure and Design, de Robert C. Martin. Está focado em aspectos práticos da aplicação da Arquitetura
Limpa em projetos de TI. Considero insatisfatória a escassez de exemplos de implementação de
boa qualidade. Como usei essa abordagem com sucesso em alguns projetos, acredito ter muitos
insights esclarecedores para compartilhar com a comunidade.

Ao mesmo tempo, me deparei com algumas limitações que tive que superar. Às vezes a cura era usar
outra técnica (como CQRS - Command Query Responsibility Segregation), às vezes seria melhor nem
usar a Clean Architecture.

Resumindo, este livro foi concebido para compartilhar toda a experiência que eu e meus colegas
obtivemos durante a implementação da Arquitetura Limpa.

ERA MOVIDA POR FERRAMENTAS

World of Python é um lugar mágico e encantador. Imagine que você deva escrever algum código padrão
necessário para implementar um recurso real. Praticamente toda vez que você está prestes a cair sob um
feitiço tão maligno, você pode quebrá-lo lançando um contra-feitiço:

pip install <nome de uma biblioteca de terceiros que resolve seu problema>

A facilidade de uso deste comando combinada com a profusão de bibliotecas permite que todos, incluindo
aprendizes de feitiçaria, resolvam problemas aparentemente complexos com um pequeno gasto de mana.
Hoje em dia, a magia chamada desenvolvimento de software pode ser aprendida e praticada quase
sem esforço, sem conhecer seus arcanos, embora a natureza da magia em si não tenha mudado em nada.
Isto cria a ilusão de que o conhecimento sobre princípios e padrões não é mais necessário.
Embora o ponto de entrada seja reduzido, os aprendizes de feitiçaria iludidos estão longe de serem
iluminados.

Literalmente, todas as ferramentas que os desenvolvedores de python usam diariamente são uma
implementação de algum tipo de padrão conhecido há muito tempo (mais de décadas) e extensivamente
descrito. Django ORM? É um exemplo de implementação do padrão Active Record, amplamente
conhecido graças ao Ruby On Rails que segue o mesmo padrão. Por exemplo, foi descrito em Patterns of
Enterprise Application Architecture de Martin Fowler usando estas palavras:
Machine Translated by Google

"É fácil construir Active Records e eles são fáceis de entender. Seu principal problema
é que eles funcionam bem apenas se os objetos do Active Record corresponderem
diretamente às tabelas do banco de dados: um esquema isomórfico. (...) Outro argumento
contra o Active Record é o fato de que ele acopla o design do objeto ao design do
banco de dados. Isso torna mais difícil refatorar qualquer design à medida que o projeto
avança.”

Que tal algo mais sofisticado, como a sessão do SQLAlchemy? Acontece que o padrão por trás disso
é chamado de Unidade de Trabalho e é descrito no mesmo livro. De repente, uma impressão de
magia que alimenta os pacotes PyPi desaparece e eventualmente desaparece. Esse
conhecimento é uma ajuda inestimável para escolher as ferramentas certas para o trabalho. Ao mesmo
tempo, uma ferramenta que resolva o seu problema mais agudo causará vários problemas menores, mas
com os quais você pode conviver. Por exemplo, a sessão do SQLAlchemy força um desenvolvedor a
registrar qualquer modelo recém-criado usando o método add. Sem ele, nenhum dado será persistido
após a confirmação. A necessidade de gerenciamento manual de modelos vale a pena, ou talvez o Django
ORM seja adequado para este projeto em particular?

A cura mais eficaz para a indecisão é permanecer pragmático e flexível. Na verdade, é disso que este
livro realmente trata. Embora explique uma abordagem interessante que destaca a importância das
preocupações empresariais, não omite convenientemente as desvantagens.

Sempre que um novo projeto é iniciado, os desenvolvedores devem se perguntar: qual abordagem/
framework/biblioteca eles devem usar? Posso perguntar em troca: Que problemas você preferiria ter? Que
problemas você deve evitar?
Machine Translated by Google

PARA QUEM É ESTE LIVRO?

Este livro é destinado a desenvolvedores de software de nível intermediário/sênior que desejam


ampliar seus conhecimentos com diversas técnicas de engenharia de software que surgiram
nos últimos anos.

Quase todos os exemplos de código são escritos em Python, portanto, o conhecimento do leitor com sua sintaxe será
útil. Felizmente, a engenharia de software é, em sua maioria, uma disciplina independente da tecnologia; portanto,
mesmo que os leitores não ganhem a vida escrevendo código em Python, eles ainda poderão usar este livro
para aprender algo novo. Todos os trechos de código foram escritos usando Python 3.7 e posteriormente modernizados
para Python 3.8.

O QUE VOCÊ ENCONTRARÁ NESTE LIVRO?

"Na teoria, teoria e prática são a mesma coisa. Na prática, eles não são."

Este livro tem como objetivo principal fornecer muitos conselhos práticos sobre a implementação
da Arquitetura Limpa. Tudo é baseado em minhas experiências, aprendidas da maneira mais difícil. Às vezes,
ficava imediatamente aparente que determinada solução era uma má ideia. Às vezes, precisávamos de uma
refatoração trabalhosa semanas depois para desfazer um design ruim. Poucas vezes não tivemos a
oportunidade de melhorar algo que precisava desesperadamente.

Por tentativa e erro, descobrimos como podemos evoluir nosso software usando
técnicas cada vez mais sofisticadas, como CQRS, Event Sourcing ou Domain-
Driven Design. O melhor foi a capacidade de escolher um deles sempre que
realmente precisássemos, sem investir muito tempo e esforço em um grande
design inicial ou ter que reescrever tudo do zero.

Implementar a Arquitetura Limpa é um pouco como um bufê - o leitor é incentivado a sair dele com o
que achar melhor para suas necessidades e humor. Não faz sentido seguir rigorosamente todas as
regras e recomendações se uma abordagem mais simples for suficiente.
Machine Translated by Google

OS BÁSICOS DA ARQUITETURA LIMPA

PARA QUE SERVE TUDO?

TI é um setor que muda rapidamente o tempo todo. Novas linguagens e frameworks surgem diariamente,
apenas para serem esquecidos vários anos depois. Soluções que antes eram populares tornam-se
uma enorme dívida técnica logo após o último colaborador abandonar o projeto. Por outro lado, existem
poucos projetos bem-sucedidos e duradouros que são continuamente mantidos e desenvolvidos. Embora
recebamos novos recursos e atualizações de segurança regularmente, ainda é necessário algum
esforço para nos mantermos atualizados com as versões mais recentes de sua estrutura da web favorita.

“A única coisa constante é a mudança” – Heráclito

Essa tarefa se torna complicada se a lógica de negócios de um projeto estiver fortemente


acoplada a uma estrutura. Cada atualização incompatível com versões anteriores na base de
código da estrutura quebra algo no aplicativo real. Tal situação é inconveniente tanto para os
mantenedores quanto para os usuários do framework. O primeiro grupo está sob constante
pressão para não quebrar nada com um novo lançamento. Imaginem o quão desanimadora é a situação.

Algumas aplicações são bastante simples. Tudo o que eles precisam fazer é buscar alguns dados de um banco
de dados, modificá-los e salvá-los novamente. Um nome comum para um navegador de banco de dados é CRUD
(Criar leitura, atualização, exclusão). Adicionar API REST aumenta apenas um pouco a complexidade. Usar
Django para tal projeto é uma das melhores escolhas que alguém pode fazer no mundo Python.

A situação fica muito mais complicada quando lidamos com domínios mais complexos. Na verdade, eles
são muito fáceis de reconhecer. Um dos sintomas pode ser um grande número de verificações a serem
realizadas. Invariantes que abrangem vários objetos são ainda mais interessantes. Digamos que vamos
construir um novo projeto onde as pessoas possam dar lances em leilões. Um leilão pode ter 0, 1 ou
vários vencedores ao mesmo tempo. Um leilão tem um horário de término após o qual ninguém pode dar lances.
Machine Translated by Google

Se usássemos a abordagem CRUD como Django/RoR, provavelmente terminaríamos com modelos separados para
leilão e lance:

Leilão de classe (models.Model):


título = modelos.CharField (max_length = 255)
preço_inicial = MoneyField (max_digits
= 19, decimal_places
= 4, default_currency =
"USD",

) preço_atual =

MoneyField( max_digits=19,
decimal_places=4, default_currency="USD",
)

lance de classe (modelos.Model):


preço =

MoneyField(max_digits=19,
decimal_places=4, default_currency="USD",

) licitante =
models.ForeignKey( get_user_model(), on_delete=models.PROTECT
)

leilão = models.ForeignKey( Leilão,

related_name="lances",
on_delete=models.CASCADE,
)

O problema é que em termos de processo licitatório, esses dois estão fortemente conectados. Não podemos
simplesmente salvar um novo lance em um banco de dados sempre que alguém clica em um lance! botão. O novo
lance deve ser verificado em relação ao leilão. Este último não acabou? Se o novo lance for o mais alto, teremos
que defini-lo como vencedor do Leilão. Ao mesmo tempo, temos que alterar o preço atual do Leilão. O lance
anteriormente vencedor agora é considerado perdedor. Como você pode ver, essas duas entidades aparentemente
distintas não podem ser tratadas de forma independente. Em outras palavras, existem invariantes no domínio que
abrangem tanto o Leilão quanto o Lance. Não faz sentido raciocinar sobre eles separadamente, pelo menos não no
processo licitatório.

Este exemplo não foi muito complicado. No entanto, o código que impõe essas regras de negócios não se enquadra
em nenhum dos blocos de construção incluídos no Django (ou em qualquer outro framework web, para ser justo).
Os invariantes vão além de um único modelo. Ao mesmo tempo, é difícil imaginar colocá-los
Machine Translated by Google

em uma visualização (uma função ou classe que trata de uma única solicitação HTTP). Embora não haja obstáculos físicos,

simplesmente não parece certo.

Outro problema com o código acoplado a um framework é visível quando se tenta testá-lo - não é possível testar a lógica de negócios

sem envolver máquinas pesadas. Inicializar tudo, inserir linhas no banco de dados, executar o código da estrutura da web (por

exemplo, para roteamento de URL), limpar o banco de dados posteriormente - tudo leva tempo. Na verdade, o tempo é o

único custo para executar os testes. Se a execução do conjunto de testes demorar muito, ele não será executado com muita frequência.

Acontece que domínios complexos têm vários casos a serem verificados. Se alguém quiser cobrir todos eles, ficará preso a um

longo tempo de execução de um conjunto de testes.

A última categoria de coisas que podem dar dor de cabeça são as integrações com serviços de terceiros.

Usar o maior número possível de serviços externos é uma abordagem moderna atualmente. Os serviços de terceiros não

podem ser evitados em muitos projetos aparentemente simples. O primeiro exemplo que vem à mente é o comércio eletrônico.

Os clientes têm que pagar pelas suas compras, por isso fazer amizade com alguma plataforma de pagamentos é uma ideia que vale

a pena. A receita dessas plataformas vem de taxas cobradas. Agora imagine que você vai substituir uma integração por outra

porque diferentes plataformas de pagamentos são um pouco mais baratas. Quantos pedidos devem ser feitos para compensar o tempo

de desenvolvimento? A integração imprudente e ingênua unirá fortemente os processos de pagamento dentro do aplicativo, da

mesma forma que as estruturas da web fazem. Portanto, será difícil mudar.

Até o momento foram descritos apenas problemas, sem propor nenhuma solução. Todos eles podem ser abordados com

elegância e estilo. É aqui que entra a Arquitetura Limpa. Simplesmente dizendo, é uma abordagem à arquitetura de software que

dá tratamento especial às regras de negócios. É inaceitável que uma estrutura, banco de dados ou serviço de terceiros vaze e

envenene a lógica de negócios. Aplicada corretamente, a Arquitetura Limpa nos dá o seguinte:

• independência das estruturas - atualizar a estrutura ou mesmo trocá-la deve ser muito
menos dor de cabeça do que antes

• testabilidade - todas as regras de negócios podem ser testadas usando testes unitários, sem inserir nada
em um banco de dados

• independência da UI/API - o mecanismo de entrega não deve moldar a lógica

• independência do banco de dados - a forma de armazenar dados não deve limitar um desenvolvedor

• independência de qualquer terceiro - as regras de negócios não precisam saber quais pagamentos

plataforma que você está usando


Machine Translated by Google

• flexibilidade - certas decisões arquitetônicas podem ser adiadas sem interromper o desenvolvimento

• extensibilidade - os projetos podem ser facilmente estendidos com técnicas mais sofisticadas como
CQRS, Event Sourcing ou Domain Driven Design, se necessário

Esses são os benefícios de uma separação estrita de interesses, organizando a base de código em
camadas claramente separadas e aplicando a Regra de Dependência entre elas.
Machine Translated by Google

ORGANIZAÇÃO DO CÓDIGO - CORTE HORIZONTAL

Numa forma básica da Arquitetura Limpa, existem quatro camadas. Naturalmente, pode-se usar mais se
for justificado.

Figura 1.1 Camadas da Arquitetura Limpa

MUNDO EXTERNO

O mais externo, Mundo externo, representa todos os serviços e códigos que o projeto utiliza, mas não
pertence à mesma base de código. Simplesmente dizendo, esta camada engloba tudo o que foi
implementado fora do projeto.

A INFRAESTRUTURA

A segunda camada é chamada de Infraestrutura. Ele contém todo o código necessário para o projeto usar
itens do mundo externo. Por exemplo, se usarmos o MariaDB como nosso armazenamento de dados
primário, as classes e funções responsáveis pela comunicação com o MariaDB ficarão na camada de
infraestrutura. O mesmo se aplica a qualquer serviço de terceiros com o qual tenhamos que nos integrar. Para
Machine Translated by Google

por exemplo, se estamos construindo uma solução de e-commerce, vamos colocar aqui aulas de
implementação de integração com provedores de pagamento. Os tipos de integrações dependem do tipo de
projeto.

APLICATIVO

A terceira camada é para regras de negócios específicas de aplicativos. Aí está o código que especifica
o que um projeto realmente faz. A camada de aplicação é o lar dos Casos de Uso (também
conhecidos como Interatores). Caso de uso é uma operação única dentro do projeto que leva à mudança
do estado do sistema, assumindo que tudo dê certo. Usando o exemplo do leilão, poderíamos ter um
Caso de Uso para fazer um lance e outro para retirar um lance. Se estivéssemos construindo uma
solução de comércio eletrônico, poderíamos ter um caso de uso para adicionar um item ao carrinho e outro
para remover um item do carrinho. Caso de uso representa uma ação única de um usuário (ou outro
ator) que é significativa do ponto de vista do negócio. Se você estiver familiarizado com Scrum, isso
pode ser mais ou menos traduzido em histórias de usuários.

O segundo tipo de bloco de construção que sempre residirá nesta camada é uma Interface (também
conhecida como Porta). Estas são abstrações sobre qualquer coisa que esteja na camada
acima - Infraestrutura e seja exigida por pelo menos um Caso de Uso. Em Python, isso pode ser
implementado usando o módulo de classes base abstratas (abc) .
Machine Translated by Google

#aplicativo/interfaces/email_sender.py
importar abc

classe EmailSender(abc.ABC):
@abc.abstractmethod
def send(self, mensagem: EmailMessage) "- Nenhum:
passar

# infraestrutura/adaptadores/email_sender.py
importar smtplib

da importação de application.interfaces.email_sender
( EmailSender,
)

classe LocalhostEmailSender(EmailSender):
def send(self, message: EmailMessage) "- Nenhum: server
= smtplib.SMTP("localhost", 1025) # etc.

Esses trechos de código mostram uma relação entre interfaces da camada de aplicação e seus
adaptadores da infraestrutura. O que é importante - um caso de uso NÃO DEVE saber se estamos usando
LocalhostEmailSender ou qualquer outra classe herdada de EmailSender. Mais sobre isso mais tarde.
Resumindo, as camadas de aplicação contêm código para todas as ações e definem interfaces para o
mundo externo executar a lógica das ações.

DOMÍNIO

Esta camada é um local para todas as regras de negócios que devem ser aplicadas independentemente do
contexto em que foram usadas. O bloco de construção básico a ser usado aqui é chamado de Entidade.
Usando o exemplo do leilão mais uma vez - poderíamos ter uma Entidade para Leilão com métodos para
fazer e retirar um lance:

leilão de classe :
def place_bid( self,
user_id: int, quantidade: Decimal
) "- Nenhum:
passar

def retirada_a_bid(self, bid_id: int) "- Nenhum:


passar
Machine Translated by Google

Por que alguém colocaria essa lógica aqui em vez de implementá-la dentro do
PlacingBidUseCase? Poderíamos simplesmente alterar uma instância de Leilão diretamente:

classe ColocandoBidUseCase:
def execute(self, _args):
"".

leilão.winners = [new_bid.bidder_id]
leilão.current_price = new_bid.amount

Tal abordagem tornaria efectivamente as nossas Entidades anémicas. Essas criaturas também são chamadas de
1
Classes de Dados (não confundir com classes de dados da biblioteca padrão!) ou Objetos Python Antigos
Simples. Eles são apenas sacos fictícios para dados e não possuem métodos (comportamento). Toda a lógica
seria implementada fora dessas classes. Este padrão é conhecido como Transaction Script .2

Isto pode funcionar em certas circunstâncias, mas certamente não neste caso, porque o domínio de leilão tem
invariantes a proteger. Por exemplo, cada mudança do vencedor afeta o preço atual. Já conhecemos pelo
menos duas situações em que isso acontece - quando alguém oferece mais do que o vencedor anterior e quando
devemos retirar a oferta atualmente vencedora.
Teremos casos de uso separados para PlacingBid e WithdrawingBid, portanto, uma abordagem ingênua com
classes sem método e scripts de transação implica que teríamos que duplicar a lógica de cálculo do preço atual, o
que é inaceitável. Quando o Transaction Script é mal utilizado e implementa a lógica que deveria ser encapsulada
pela Entidade, estamos falando de um antipadrão denominado Entidades Anêmicas. Ainda outro princípio
que alerta contra a alteração externa dos dados do objeto é Diga, não pergunte.
3

class PlacecingBidUseCase: def


execute(self, _args) "- Nenhum: # Diga, não
pergunte violado
# se leilão.preço_atual < new_bid.amount:
leilão.winners = [new_bid.bidder_id]
#
# leilão.preço_atual = novo_bid.amount

# deixe o leilão cuidar disso! Ele sabe melhor


leilão.place_bid("".)

1
Martin Fowler, Refatoração: Melhorando o Design do Código Existente 2ª edição, Capítulo 3, Dados
Aula

2
Martin Fowler, Padrões de Arquitetura de Aplicativos Corporativos, Capítulo 9, Script de Transação
3
Martin Fowler, TellDontAsk https://martinfowler.com/bliki/TellDontAsk.html
Machine Translated by Google

A REGRA DA DEPENDÊNCIA

Agrupar classes e funções em camadas não é suficiente para obter uma base de código clara e sustentável.
Obviamente, o fluxo de controle precisa atravessar pelo menos algumas (se não todas) camadas para
realmente fazer algo em projetos que usam a Arquitetura Limpa. Tendo em mente os benefícios e
objetivos desta abordagem, as interações entre as camadas não podem ser deixadas ao acaso. Seria
possível importar e chamar algum código específico da estrutura na camada de domínio se não fosse pela
Regra de Dependência. Diz que nenhuma camada inferior pode conhecer e usar qualquer coisa de qualquer camada su
Por exemplo, não é permitido utilizar nenhuma classe, função ou módulo da Infraestrutura se
estivermos na camada Domínio . A Regra de Dependência não apenas proíbe o desenvolvedor de importar
explicitamente símbolos da camada externa, mas também desencoraja aceitá-los como argumentos de
funções. A Regra de Dependência é ilustrada com setas no diagrama de arquitetura. A direção das setas
é a mesma das dependências: Infraestrutura utiliza Aplicação, Aplicação utiliza Domínio, mas não é
permitido ao Domínio utilizar Infraestrutura ou Aplicação
etc.

Naturalmente, em Python, tudo deve ser tratado como um acordo de cavalheiros.


A linguagem não fornece nenhum método nativo para impor regras de camadas. São certas meias-
medidas descritas mais adiante no livro. Em linguagens como Java ou C#, um desenvolvedor pode contar
com pacotes e modificadores de acesso para obter uma separação clara e agradável.

LIMITES

Por último, mas definitivamente não menos importante, as camadas precisam ter limites nítidos. O limite
define um protocolo de comunicação com a camada. Um código de grupos de camadas. Ele contém
classes e funções. A maioria deles não se destina a ser usada externamente. Eles são privados, por assim
dizer. Isto implica que ninguém de fora deve sequer se incomodar com a sua existência. Para apontar a
direção certa aos desenvolvedores perdidos, deve-se expor a API da camada e fazer com que pareça
um caminho óbvio a ser seguido sempre que alguém precisar da funcionalidade da camada. Efetivamente,
um limite é um conjunto de interfaces. Seus métodos são como portas e os argumentos desses métodos
são como fechaduras. Eles esperam um argumento específico que os abra, como uma chave.

Tipos arbitrários não devem ser passados entre camadas. Seguindo a Regra de Dependência, é estritamente
proibido passar a estrutura de dados da camada superior para a camada inferior.
Por exemplo, passar um modelo ORM para o Domínio viola a regra, porque implica que o Domínio
sabe algo sobre o mundo exterior. Idiomas sem digitação estática ou anotações de tipo (como Python
antes da versão 3.4) podem facilmente ignorar isso. Felizmente, importar algo de uma camada superior
apenas para anotar um argumento já causa sentimentos ruins.
Machine Translated by Google

Os argumentos de entrada fazem parte de um limite e devem pertencer a uma camada que os aceita.

Em uma API do mundo real, uma camada consistirá em muitos métodos que aceitam um número variável de argumentos.

Esta complexidade não pode ser menosprezada. Assim, faz todo o sentido agrupar parâmetros de limite para pontos de

entrada individuais – métodos – em estruturas de dados. Esse padrão é chamado de Objetos de Transferência de Dados

(DTOs).

@dataclass(frozen=True) classe
EmailDto:
fonte: endereço de e-mail
resposta_para: endereço de e-mail
conteúdo: str

O limite mais importante é colocado na borda da camada de Aplicativo. O limite da aplicação que será usada no mundo

externo é formado por Casos de Uso. Para evitar a exposição de classes concretas (e, portanto, o acoplamento) com os

clientes da Aplicação, outra interface pode ser introduzida que irá abstrair um Casos de Uso - Limite de Entrada. Da

perspectiva do mundo externo, o limite de caso de uso/entrada é apenas uma interface que comunica a intenção de negócios

de um aplicativo. Para chamá-lo, é preciso preparar um DTO e passá-lo como único argumento. Analogamente, outro DTO é

resultado de ações executadas por um Caso de Uso (embora não seja retornado diretamente - falaremos mais sobre isso

posteriormente). Esses três (DTO de entrada, DTO de saída, limite de entrada) juntos formam um limite sólido que oculta

todos os detalhes da camada de aplicativo. Outros nomes que podem ser usados para se referir a DTOs de entrada e

saída são chamados respectivamente de Solicitação e Resposta. Entretanto, para evitar confusão com o protocolo HTTP,

irei me referir a eles usando DTOs de entrada e saída ao longo do livro. Os objetos de transferência de dados são imutáveis

(frozen=True). Não há razão para alguém querer alterar os dados internos. São como mensagens – uma entrando e outra

saindo.

@dataclass(frozen=True) classe
PlacecingBidInputDto: bidder_id: int

leilão_id: int
quantidade: decimal

@dataclass(frozen=True) class
PlacecingBidOutputDto: is_winning:
bool
preço_atual: Decimal
Machine Translated by Google

class PlacingBidInputBoundary:
@abc.abstractmethod def
execute( self,
request: PlacingBidInputDto ) "- Nenhum:

"".

MVC ALGUÉM?

Se você é um Pythonista que escreveu algum código em Django, Flask ou Pyramid, pode ficar um
pouco confuso com a nomenclatura. O controlador no diagrama corresponde a um conceito que você
conhece como visualização, enquanto a visualização se assemelha ao modelo. Essa agitação tem
origem na adoção de diferentes padrões entre Python e outras comunidades de programação.
O diagrama pressupõe que os leitores tenham conhecimento do Model-View-Controller, enquanto
o Django adota algo conhecido como Model-Template-View. Mais informações podem ser
encontradas em djangobook.com - Estrutura do Django – Visão de um Herege https://djangobook.com/

mdj2-django-estrutura/.

No entanto, toda a confusão desaparecerá quando analisarmos a implementação


referencial.

RESUMO DO CAPÍTULO

O valor real dos projetos de TI está próximo da complexidade mais significativa que eles possuem.
Desde que um projeto seja algo mais do que apenas um navegador para um banco de dados relacional,
haverá muitas regras de negócios que deverão ser aplicadas. A Arquitetura Limpa trata estes últimos como
cidadãos de primeira classe. Em vez de esconder esta lógica mais valiosa em uma sopa de estruturas
e ORMs, ela expõe regras e processos de negócios em camadas separadas - Domínio
e Aplicação. A lógica de negócios destilada pode ser facilmente testada, pois desconhece completamente o
mundo externo. O código responsável pela comunicação com ele está na Infraestrutura
camada. Este último pode usar Aplicativo, mas Aplicativo não deve saber nada sobre Infraestrutura.
Isso é imposto pela Regra de Dependência:

Mundo Externo "- Infraestrutura" - Aplicação "- Domínio

Obviamente, durante a execução de um cenário de negócio, será necessário inserir linhas em um banco
de dados ou chamar um serviço externo em algum momento. A Arquitetura Limpa proíbe o acoplamento da
lógica de negócios com o mundo externo, então o Aplicativo define um conjunto de Interfaces (também
conhecidas como Portas) que são uma forma de plugins abstratos. Implementações concretas serão
eventualmente fornecidas pela Infraestrutura.
Machine Translated by Google

Manter tudo em ordem exige traçar limites nítidos e distintos. As camadas expõem algumas
funcionalidades por meio de interfaces que aceitam DTO de entrada (às vezes chamado de
solicitação) como argumentos. Todos os detalhes estão ocultos atrás da fronteira. Do mundo exterior, só
podemos ver assinaturas de métodos e estruturas de dados necessárias para chamar métodos que estão na frontei
Machine Translated by Google

IMPLEMENTAÇÃO REFERENCIAL

ISENÇÃO DE RESPONSABILIDADE

Este capítulo apresenta exemplos de implementação de acordo com a ideia original apresentada por Robert C.
4 5
Martin no artigo The Clean Architecture descrito em seu livro , poucas palestras proferidas em conferências e
6.

Devo admitir que nunca tentei implementar a Arquitetura Limpa em um projeto comercial seguindo
rigorosamente a visão original do Tio Bob. Achei que algumas peças poderiam ser removidas ou feitas de
forma diferente sem perder muito. Embora minhas implementações pareçam um pouco diferentes, decidi ilustrar o
conceito original com código para completar este livro. No próximo capítulo, descrevo possíveis
simplificações que podem ser feitas sem comprometer grande parte da qualidade e dos benefícios.

FLUXO DE CONTROLE NA ARQUITETURA LIMPA

Este exemplo é um aplicativo web padrão que usa um banco de dados para armazenar dados. O fluxo de controle
começa no Controller, que é invocado por uma estrutura da web no momento do envio da solicitação.
A função do Controlador é reembalar os dados da solicitação HTTP no Input DTO e passá-los para o Input
Boundary, implementado pelo Use Case (também conhecido como Interactor). Este último usa dados do DTO de
entrada para buscar as entidades necessárias do banco de dados usando a interface de acesso a dados.
Em seguida, o Use Case orquestra as entidades para executar a lógica de negócios e, opcionalmente, as salva
usando a interface de acesso a dados. Use Case termina sua tarefa construindo Output DTO e passando-o para Output Boundar
implementação - Apresentador. Sua função é reformatar os dados para que sejam convenientes para exibição na
Visualização final. View recebe dados em outro DTO, chamado View Model. Caso de uso que implementa limite
de entrada não retorna nada. O apresentador que implementa o limite de saída deve realmente apresentar o
resultado usando Output DTO.

Esta é uma descrição completa em poucas palavras. Antes de nos aprofundarmos na implementação real,
observe os limites e as camadas. Começando no canto superior direito, Entidades

4
Robert C. Martin, A Arquitetura Limpa https://blog.cleancoder.com/uncle-bob/ 2012/08/13/the-clean-
architecture.html

5 Robert C. Martin, Arquitetura dos anos perdidos https://www.youtube.com/watch?


v=WpkDN78P884
6
Robert C. Martin, Arquitetura Limpa: Guia do Artesão para Estrutura e Design de Software
Machine Translated by Google

pertencem à camada Domínio. A maioria dos elementos no diagrama (DTO de entrada, limite de entrada, limite de

saída, DTO de saída, caso de uso, interface de acesso a dados) pertence à camada de aplicativo. O resto é menos importante.

A implementação do acesso a dados está na camada de infraestrutura, enquanto o banco de dados

pertence ao Mundo Externo.

Figura 2.1 O diagrama de implementação referencial da Arquitetura Limpa


Machine Translated by Google

REQUISITOS DE NEGÓCIO

O código deve ser derivado de um conjunto de regras de negócios. Portanto, apresento-os antes da
implementação ser mostrada:

• Os licitantes podem fazer lances em leilões para ganhá-los

• Um leilão tem um preço atual que é visível para todos os licitantes

ÿ o preço atual é determinado pelo valor do lance vencedor mais baixo

ÿ para se tornar um vencedor, é preciso oferecer um preço superior ao preço atual

• Leilão tem preço inicial. Novos lances com valor inferior ao preço inicial
não deve ser aceito
Machine Translated by Google

IMPLEMENTAÇÃO

DIAGRAMA DE SEQUÊNCIA

Figura 2.2 Um diagrama de sequência da implementação referencial da Arquitetura Limpa

Pode parecer confuso que não haja nenhuma seta do Presenter para View logo antes do final. Há uma razão
para isso descrita abaixo.

LIMITE DE ENTRADA

A funcionalidade do nosso aplicativo é visível no nível da estrutura como um limite de entrada - uma
interface que aceita um DTO de entrada. Esta última é uma estrutura de dados relativamente simples com
campos digitados, compreensíveis pela camada inferior - neste caso, aceitamos apenas os tipos de dados
padrão do Python:

@dataclass(frozen=True)
classe PlacecingBidInputDto:
bidder_id: int
leilão_id: int
quantidade: decimal

Assumimos que o aspecto da autenticação deve ser tratado no nível da estrutura da web - apenas aceitamos o
ID simples que pertence a uma pessoa que faz uma oferta e confiamos nele. O DTO de entrada deve ser
passado para o Caso de Uso abstraído por um Limite de Entrada:
Machine Translated by Google

classe ColocaçãoBidInputBoundary (abc.ABC):


@abc.abstractmethod
def

execute( self, input_dto:


PlacingBidInputDto, apresentador:
PlacingBidOutputBoundary, ) "- Nenhum:
passar

LIMITE DE SAÍDA

Ao mesmo tempo, esperamos que nossa operação produza alguns dados na forma de Output DTO:

@dataclass(frozen=True)
class PlacecingBidOutputDto:
is_winning: bool
preço_atual: Decimal

Esta estrutura de dados deve ser aceita pelo único método de PlacingBidOutputBoundary (uma interface
para apresentadores):

classe ColocaçãoBidOutputBoundary (abc.ABC):


@abc.abstractmethod
def presente(
self, output_dto: ColocandoBidOutputDto
) "- Nenhum:
passar

APRESENTADOR

Uma classe que implementa PlacingBidOutputBoundary é chamada Presenter. Sua função é converter os
dados de saída para o formato mais conveniente para uma camada de apresentação. Em nosso
exemplo, retornamos a flag booleana para indicar se o licitante foi vencedor e uma informação
sobre o preço atual do leilão. O apresentador deve formatar o número decimal para o número desejado de
campos decimais, adicionar o símbolo da moeda - em suma, converter os dados em uma string, apropriada
para mostrá-los a um licitante.
Machine Translated by Google

classe ColocaçãoBidWebPresenter(
ColocandoBidOutputBoundary
):
definitivamente presente (

self, output_dto: PlacingBidOutputDto ) "-


Nenhum:
formatted_data =
{ "current_price": f'${output_dto.current_price.quantize(".01")}', "is_winning":
"Parabéns!"
se output_dto.is_winner else ":
(",
}
"".

Como você deve ter deduzido do diagrama de sequência, o fluxo de controle termina em uma implementação
do Presenter , ou seja, no método presente . Não devolvemos nada ao Controlador
(ou visualizar em MVT). Um licitante deverá ver novos dados imediatamente após o término da presente chamada.
Isso é difícil de imaginar nos frameworks web Python mais populares, quando se espera que o Controller retorne
algo que o framework enviará ao cliente posteriormente.

No entanto, a abordagem com fluxo terminando no presente método funciona perfeitamente para aplicativos
móveis que podem construir a próxima tela dependendo do conteúdo de PlacingBidOutputDto
e mostre ao usuário. Também é possível chegar a esse comportamento em estruturas que criam um objeto de
resposta antecipadamente e permitem manipulá-lo. Exemplos para este caso específico serão mostrados posteriormente
neste livro. Por uma questão de simplicidade, seria preferível estender a interface
PlacingBidOutputBoundary com outro método que pode ser usado para recuperar dados no Controller:

classe ColocaçãoBidOutputBoundary (abc.ABC):


@abc.abstractmethod
def presente(
self, output_dto: ColocandoBidOutputDto
) "- Nenhum:
passar

@abc.abstractmethod
def get_presented_data(self) "- ditado:
passar
Machine Translated by Google

Qualquer implementação concreta seria essencialmente apenas devolver dados formatados:

classe ColocaçãoBidWebPresenter(
ColocandoBidOutputBoundary
):
definitivamente presente (

self, output_dto: PlacingBidOutputDto) "-


Nenhum:
self._formatted_data = {
"current_price": f'${output_dto.current_price.quantize(".01")}', "is_winning":
"Parabéns!"
se output_dto.is_winner else ":
(",
}

def get_presented_data(self) "- ditado: return


self._formatted_data

Encontrar um tipo de dados de saída apropriado para Apresentadores que retorne por meio
de get_presented_data pode ser complicado. Em Python, retornar dict é a melhor aposta, pois pode ser
transmitido para a função de renderização de modelo. Mecanismos de modelagem populares aceitam
objetos de modelo e uma instância de dict com dados para espaços reservados preparados. No entanto,
isso diminui a responsabilidade do Apresentador . Este problema não existe quando um Presenter não retorna
dados, mas consegue realmente apresentar o resultado do processo. Este tópico será discutido mais
detalhadamente no próximo capítulo.

VER MODELO

Isso nada mais é do que outro Objeto de Transferência de Dados obtido de um Presenter para ser passado
para a Visualização. Nesse caso, um simples dict resolve o problema, porque a maioria dos mecanismos de
modelagem usados em estruturas web Python aceitam esse formato. No entanto, se houver necessidade de
mais controle sobre a estrutura de um modelo View, a introdução de uma classe resolveria.

CASO DE USO

O Caso de Uso é a parte mais interessante da Arquitetura Limpa, onde algo finalmente está acontecendo. O
caso de uso implementa limite de entrada e orquestra todo um processo de negócios:
Machine Translated by Google

classe PlacingBidUseCase (PlacingBidInputBoundary):


def _init_(self,

data_access: AuctionsDataAccess,
output_boundary: PlacingBidOutputBoundary,) "-
Nenhum:
self._data_access = data_access
self._output_boundary = output_boundary

def execute( self,


input_dto: PlacingBidInputDto ) "- Nenhum:
leilão =
self._data_access.get( input_dto.auction_id

)
leilão.place_bid( input_dto.bidder_id, input_dto.amount

) self._data_access.save(leilão)

output_dto =

PlacengBidOutputDto( input_dto.bidder_id em leilão.winners, leilão.preço_atual,

) self._output_boundary.present(output_dto)

Este exemplo é intencionalmente mantido simples. Ele não leva em consideração nenhum caso extremo ou
tratamento de erros - é apenas para refletir o que foi mostrado no diagrama de sequência. Primeiramente,
recuperamos a Entidade do Leilão usando uma implementação de AuctionsDataAccess. Ter uma entidade
Por exemplo, chamamos o método place_bid. O último é um comando - serve para alterar o estado de uma
Entidade, mas não retorna nenhum valor. Na próxima etapa, persistimos as alterações usando
uma implementação de AuctionsDataAccess. Por fim, montamos uma instância de
PlacingBidOutputDto, alimentando-a com dados obtidos de métodos de consulta na Entidade Leilão -
vencedores e propriedade preço_atual. Na última etapa, passamos output_dto para a
chamada do método presente do limite de saída.

Uma coisa interessante aqui é como data_access e output_boundary são criados. Eles não são
explicitamente instanciados por PlacingBidUseCase - em vez disso, eles são passados para _init_ (um
equivalente aproximado do construtor em Python). Sabemos com certeza que os objetos não
podem ser instâncias de AuctionsDataAccess ou PlacingBidOutputBoundary porque são abstratos.
Na verdade, temos implementações concretas dessas interfaces, nomeadamente
PlacingBidWebPresenter e DbAuctionsDataAccess respectivamente. É crucial para o
PlacingBidUseCase não saber qual implementação exata ele está usando. Por que? Porque eles
Machine Translated by Google

pertencem à camada superior e seria contra a Regra de Dependência para Caso de Uso saber algo sobre as
camadas superiores. Por outro lado, AuctionsDataAccess e
PlacingBidOutputBoundary pertencem à camada de aplicativo , portanto podem ser referenciados com
segurança no caso de uso.

Uma técnica de passar dependências para o construtor de um objeto é chamada de Injeção de Dependência.
Normalmente, não se faria isso manualmente e automatizaria usando um contêiner de injeção de
dependência. Resumindo, este último se comporta um pouco como um dicionário que mantém implementações
concretas nas interfaces que implementam. Deve haver uma configuração em algum lugar da base de código que
instrua o contêiner de injeção de dependência sobre o que deve fazer sempre que alguém solicitar uma instância
de um determinado tipo. Para o snippet acima, se usarmos a biblioteca inject , ficaria assim:

importar injetar

def di_config(binder: inject.Binder) "- Nenhum:


fichário.bind(
AuctionsDataAccess, DbAuctionsDataAccess()

) binder.bind_to_provider(
ColocandoBidOutputBoundary,
ColocandoBidWebPresenter,
)

injetar.configure(di_config)

Uma vez configurado, o inject armazena um mapeamento entre tipos (geralmente classes abstratas) e suas
implementações. Mais informações sobre esse assunto serão apresentadas posteriormente. Por enquanto,
basta saber que PlacingBidUseCase não cria suas dependências nem sabe quais implementações de classes
abstratas são utilizadas.

INTERFACE DE ACESSO A DADOS

Isso especifica uma interface para recuperar/armazenar entidades. A interface mais simples consistirá em dois
métodos - obter a chave primária e salvar.
Machine Translated by Google

classe LeilõesDataAccess(abc.ABC):
@abc.abstractmethod
def get(self, leilão_id: int) "- Leilão:
passar

@abc.abstractmethod
def save(self, leilão: Leilão) "- Nenhum:
passar

ACESSO DE DADOS

DbAuctionsDataAccess é uma implementação concreta da classe abstrata AuctionsDataAccess que usa


qualquer armazenamento de dados que usamos para armazenar nossos queridos leilões. Poderíamos estar
mantendo dados em arquivos, um RDBMS, banco de dados NoSQL ou algum serviço externo escondido atrás de REST
API.

ENTIDADES - BID

Finalmente, chegamos a uma camada de domínio para modelar uma oferta como uma entidade separada.

@dataclass
lance de classe :

id: Opcional[int]
bidder_id: int
quantidade: decimal

Esta é uma classe simples que possui três campos: id, bidder_id e amount. O primeiro é opcional, pois os lances
recém-criados (antes de anotá-los em algum lugar) não terão IDs. Existe outra abordagem - usar o UUID e
sempre fornecer um ID aos novos lances. Não estou adicionando campos para 7 . Por uma questão de simplicidade,
o horário de criação, etc.

ENTIDADES - LEILÃO

Um leilão na forma mais simples precisará de um método público para fazer um novo lance, obter a
lista de vencedores e o preço atual.

7
Vaughn Vernon, Implementando Design Orientado a Domínio, Capítulo 5. Entidades, Identidade
Única, Aplicativo Gera Identidade
Machine Translated by Google

leilão de classe:
def _init_( self,
id: int,

preço_inicial: Decimal, lances:


List[Bid], ) "- Nenhum:
self.id = id

self.starting_price = preço_inicial self.bids =


lances

def place_bid( self,


user_id: int, quantidade: Decimal
) "- Nenhum:
passar

@propriedade
def preço_atual(self) "- Decimal:
passar

@propriedade
def vencedores(self) "- Lista[int]:
passar

Observe que place_bid altera um leilão (muda seu estado), enquanto current_price e vencedores não. Cada
método de leilão pertence a uma de duas categorias distintas:

• comandos que alteram o estado e não retornam nenhum valor,

• consultas que retornam valor, mas não podem alterar nada

Esta abordagem é conhecida como Command Query Separation (CQS) e foi originalmente descrita por
8
Bertrand Meyer em seu Object Oriented Software Construction em 1988. Tenha em mente que as consultas
são consideradas seguras - elas podem ser reorganizadas, usadas em qualquer lugar e não afetarão o
estado do sistema, embora seja preciso ter mais cuidado com os comandos. Normalmente, a ordem de
invocação dos comandos é significativa, enquanto as consultas podem ser invocadas em qualquer
sequência. A razão pela qual esse padrão foi aplicado aqui é que ele simplifica e ordena a interface da
classe Auction. Também tornará um pouco mais fácil testar a classe.

RESUMO DO CAPÍTULO

Este capítulo descreveu um fluxo de controle em aplicações que implementam a Arquitetura


Limpa em sua visão original.

8 Bertrand Meyer, Construção de Software Orientado a Objetos


Machine Translated by Google

O controlador reempacota os dados da solicitação na entrada Dto e os passa para o caso de uso abstraído

pelo limite de entrada. O caso de uso aproveita a implementação concreta de acesso a dados para buscar entidades

do armazenamento persistente (por exemplo, banco de dados relacional), fornece comandos para alterar seu estado

interno. Finalmente, as entidades são salvas. A última etapa do Caso de Uso é coletar os dados necessários para a Saída

Dto que será passada para um Presenter, abstraído pelo Limite de Saída.

Todas essas camadas e abstrações estão aqui para extrair código orientado por requisitos de negócios de coisas não
funcionais.
Machine Translated by Google

A ARQUITETURA LIMPA
MODIFICAÇÕES

“A perfeição é alcançada, não quando não há mais nada a acrescentar,


mas quando não há mais nada a tirar” -
Antoine de Saint-Exupéry
DILEMA DO APRESENTADOR

A receita original da Arquitetura Limpa contém muitas partes móveis. Certas suposições, como o
fluxo de controle que termina em Presenter, não são adequadas para fluxos com os quais
estamos acostumados na programação de aplicações web, por exemplo, em frameworks web
convencionais escritos em Python (e muitas outras linguagens de programação, para ser justo). Espera-se
que um desenvolvedor retorne explicitamente algum valor de um Controller (ou View,
usando a terminologia MVT):

def index(request: HttpRequest) "- HttpResponse: # Django não irá


te perdoar se você não retornar HttpResponse de uma view
return HttpResponse(f"Olá, mundo!")

Não se pode simplesmente contornar esse comportamento, então a única opção que resta é fazer
O apresentador retorna um valor para visualizar e avança a partir daí:

def index(solicitação: HttpRequest) "- HttpResponse:


"".

retornar apresentador.get_html_response()

Nem toda estrutura irá forçá-lo a usar esses truques. Por exemplo, o Falcon passa um 9objeto de
resposta para cada controlador. Portanto, o Presenter é capaz de manipular livremente uma
resposta sem a necessidade de retornar nada ao Controller:

9 O Falcon Web Framework https://falcon.readthedocs.io/en/stable/


Machine Translated by Google

class PlacengBidResource: def


on_post( self,
req: Request, resp: Response ) "- Nenhum: #
validação,
"".
montagem de input_data
apresentador = PlacingBidJsonPresenter(resp) use_case_cls
= inject.instance( PlacingBidInputBoundary

) use_case_cls(presenter).execute( input_data

classe ColocaçãoBidJsonPresenter(
Limite de saída de índice
):
PRECISÃO = Decimal("0,01")

def _init_(self, resp: Resposta) "- Nenhum:


self.resp = resp

definitivamente presente (

self, output_dto: PlacingBidOutputDto ) "- Nenhum: if

output_dto.is_winning: message =
( "Parabéns!
Você é um vencedor! :)"

) senão:
message = "Desculpe, seu lance não foi alto o suficiente!"

preço_formatado = saída_dto.preço_atual.quantize(
auto.PRECISÃO

) self.resp.media =
{ "preço_atual": f"${price_formatted}", "mensagem":
mensagem,
}

Mais estruturas compartilham essa abordagem, especialmente as assíncronas. express.js, que é


uma solução baseada em node.js, também permitiria apresentar resposta sem retornos explícitos:

app.get('/', function(req, res) { "/ manipulamos o


objeto de resposta, sem retorno!
res.send('Olá, mundo!');
});
Machine Translated by Google

Outra abordagem é não usar DTOs de Presenter e de Saída em nossos casos de uso. Assumimos que
se o Caso de Uso não lançou uma exceção durante a execução, então foi bem-sucedido. Se for
necessário devolver algo a um cliente de aplicativo, emitimos consultas separadas para criar uma resposta.
Separar leituras de gravações é conhecido como CQRS. Há um capítulo dedicado sobre essa
abordagem no livro.

O fluxo de controle original mudaria para:

Figura 3.1 Um diagrama de sequência da Arquitetura Limpa com consultas em vez de Limite de Saída

Tal abordagem simplifica consideravelmente o Caso de Uso, pois torna desnecessária a montagem de
DTOs de Saída . Como resultado, Output Boundary e Presenter também são eliminados de cena. Por outro
lado, usar Query pode envolver viagens adicionais ao banco de dados (ou outra fonte de dados), o que
parece um desperdício. Isso é verdade, mas somente quando tivermos todos os dados necessários para
produzir a resposta no Caso de Uso. Há uma compensação a ser feita entre simplificar o Caso de Uso e
reduzir ligeiramente o desempenho em certos casos. Caso o Caso de Uso não tenha dados suficientes
para produzir resposta, apesar de tomar cuidado com a execução da lógica de negócios, chamadas
adicionais ao banco de dados serão inevitáveis de qualquer maneira.

Nesse caso, aderir a Output Dto -> Output Boundary -> Presenter força o desenvolvedor a buscar dados
adicionais em algum lugar do fluxo. Talvez o Caso de Uso deva colocar tudo o que é necessário para
apresentação no Output DTO? Duvidoso, pois significaria que a apresentação dos dados impulsiona
a implementação de um fluxo de negócios. É função do Apresentador cuidar de ter todos os dados
necessários que não podem ser fornecidos por um Caso de Uso cujo Limite de Saída é implementado
pelo Apresentador. Deixar isso para o Presenter permite alguma flexibilidade.
Machine Translated by Google

Se tivermos que suportar vários apresentadores para cada mecanismo de entrega possível do sistema (por
exemplo, web ou CLI), então cada um deles poderá ter requisitos diferentes sobre dados adicionais e buscar
apenas o que precisa.

Resumindo, temos duas opções - usar Output DTO -> Output Boundary -> Presenter chain ou abandonar
completamente esta parte da Clean Architecture em favor de consultas inspiradas em CQRS.

LIVRAR-SE DO LIMITE DE ENTRADA

Muitas interfaces ao longo do caminho existem para fornecer acoplamento fraco. Um argumento para
manter as abstrações sempre que possível é que elas promovem a testabilidade. A verdade é que em
linguagens dinâmicas que permitem a correção de macacos, podemos testar unidades de classes mesmo que
elas estejam instanciando explicitamente e usando outras classes concretas. Em outras palavras, o
acoplamento apertado não é realmente um obstáculo, embora diminua a qualidade do projeto. Se você não
consegue evitar de esfregar os olhos, quero te acalmar: não uso nem aprovo remendos de macaco. É um desses
truques sujos que mais cedo ou mais tarde afetarão um desenvolvedor. O que estou tentando enfatizar é que o
acoplamento frouxo em si não é um objetivo. O objetivo é entregar recursos a tempo e ao mesmo tempo ter
uma base de código sustentável e facilmente extensível. Pode não ser grande coisa ter o Controlador
acoplado a um Caso de Uso. Não esperamos que o Controlador faça nada além de reembalar os dados da
solicitação na entrada Dto. Na verdade, essa integração pode (e deve!) ser verificada com testes de nível
superior.

As abstrações são essenciais na camada de aplicativo para estabelecer uma fronteira nítida entre a lógica
de negócios e o mundo externo. Abstrair os casos de uso , ocultando-os atrás do limite de entrada no View/
Controller, não é da mesma importância. A poderosa Regra de Dependência não é violada neste caso, porque
View/ Controller reside em uma camada superior em comparação com Use
Caso.

Livrar-se das interfaces de limite de entrada tem uma consequência: visualizações/ controladores serão
fortemente acoplados a casos de uso/ interatores concretos. Se isso é algo que você pode pagar, não há
razão para manter os limites de entrada. Não deve diminuir muito a testabilidade - Ver/
Os controladores contêm pouca ou nenhuma lógica e são testados de ponta a ponta.
Machine Translated by Google

PROJETO ALTERNATIVO DE CASOS DE USO

FACHADA DE APLICAÇÃO

Se as aplicações fossem hotéis, cada Caso de Uso/ Interator seria um funcionário dedicado a cuidar de um
único serviço que o hotel oferece. Nesse projeto, um hóspede do hotel disposto a usar tratamentos de spa entraria
em contato com o Spa Treatments Keeper. Se eles quisessem usar o campo de golfe no local, eles conversariam
com o Golf Field Keeper, etc. Esta abordagem não é prática - na indústria hoteleira, um hóspede do hotel preferiria
entrar em contato com a recepção - pessoalmente ou por telefone. A recepção do hotel é a interface voltada para
os hóspedes dos diversos serviços oferecidos. A semelhança com o padrão de design Facade é visível a olho
nu. Com relação aos casos de uso /
Interatores, ter cada um deles como uma classe separada é aproximadamente equivalente ao design de um hotel
com tratadores dedicados. O design alternativo pressupõe que temos uma única classe (Facade) com
métodos separados, cada um responsável por um fluxo de negócios. Efetivamente, todos os Casos de Uso são
transformados em métodos de Fachada. Naturalmente, os métodos do Facade podem utilizar classes auxiliares
para realizar o trabalho (assim como a recepcionista, repassando posteriormente as solicitações dos hóspedes),
mas esses colaboradores não devem ser visíveis nem acessados externamente. Se você quiser algo, acesse o
aplicativo Façade. Esta abordagem funciona bem desde que nossos Casos de Uso não sejam sofisticados, por
exemplo, siga o mesmo esquema: obtenha uma Entidade usando a Interface de Acesso a Dados, chame o método
de uma Entidade e salve-o novamente.

Tal padrão é perigoso quando um aplicativo não é modular porque tal fachada rapidamente se tornaria enorme. A
modularidade será discutida mais tarde, provavelmente no capítulo mais importante do livro. Além disso,
tenha em mente dependências de injeção potencialmente complicadas.

class AuctionsFacade: def

place_bid( dto:
PlacingBidInputDto,
) "- Nenhum:
"".

def retirar_bid( dto:


RetiradaBidInputDto,
) "- Nenhum:
"".

MANIPULADORES DE COMANDO + MEDIADOR (BUS DE COMANDO)

Alguns parágrafos antes de ser discutida a possibilidade de renunciar ao limite de entrada . Ainda antes, foi
mostrado um design com Query substituindo DTOs de saída, limite de saída e apresentador .
Machine Translated by Google

Supondo que nossos Casos de Uso não construam Dtos de Saída e ainda vejamos dissociação de
Controladores benéficos, podemos transformar nossos DTOs de entrada em comandos CQRS e introduzir um
Mediador 10, chamado barramento de comando. A partir de agora, não há PlacingBidInputDto - ele se torna um

PlaceBid, um comando. O controlador ainda precisa montar esse objeto de transferência de dados, mas agora
ele o passa para o método de despacho universal do barramento de comando:

def putting_bid_view(request) "- Nenhum:


command = PlaceBid("".) #
command_bus passa o comando para o manipulador apropriado
comando_bus.dispatch(comando)

Durante a inicialização do aplicativo, o Command Bus é configurado para rotear comandos para casos de uso. Para sermos

rigorosos, nossos Casos de Uso tornam-se Manipuladores de Comando.

O principal benefício aqui é que estamos completamente dissociados do manipulador. Isso também simplifica
nossos Controllers, pois eles só precisam conhecer as classes de Comando (ex. PlaceBid) e Command Bus. Com
tal abordagem, o limite de entrada torna-se completamente redundante.

O Command Bus torna possível mais um design não tão óbvio. Nossas classes de comando são objetos de

transferência de dados, o que os torna fáceis de serializar e enviar pela rede. Com esse design, é mais fácil evoluir para
uma arquitetura distribuída e orientada por mensagens.

MODELAGEM DE ENTIDADES USANDO ORM

Se alguém usar uma biblioteca ORM apoiada por um RDBMS, pode ser tentador reutilizar modelos ORM como
entidades. Conhecendo a Regra de Dependência, colocar classes acopladas ao armazenamento no centro da
camada Domínio é inesquecível. Primeiro, as classes de modelo podem causar vazamento de detalhes do mecanismo
de persistência subjacente no Domínio. Por sua vez, este último fica desnecessariamente complicado devido
ao acoplamento extra. Em segundo lugar, o domínio construído em torno de coisas que dependem do banco de dados
pode não ser mais facilmente testado. A testabilidade do aplicativo também será prejudicada porque ele é construído em
torno do domínio. Terceiro, ao reutilizar modelos ORM, não será possível criar abstrações úteis para coisas mais
complicadas do que tabelas em bancos de dados relacionais que são caracterizados por uma estrutura plana e

normalizada. Linhas de tabelas RDBMS são uma metáfora muito fraca para entidades de negócios, especialmente
quando estão envolvidos gráficos de objetos. Pensar através do prisma das linhas do banco de dados incapacita
nossa capacidade inata de modelar coisas mais complexas. Por último, mas não menos importante, o carregamento
lento irrestrito pode ser visto como um convite para efeitos colaterais indesejados em Domínio e Aplicativo.

10 Erich Gamma et al, Design Patterns: Elements of Reusable Object-Oriented Software, Capítulo 5:

Padrões Comportamentais, Mediador


Machine Translated by Google

Os esquemas de banco de dados são construídos em torno dos dados, enquanto as entidades existem para

proteger as invariantes de negócios. Estas últimas são regras que devem ser aplicadas independentemente do contexto da aplicação.

Um exemplo ilustrativo de uma Entidade que possui algumas invariantes para proteger é um carrinho em um projeto de

comércio eletrônico. Exemplos:

• Sempre que alguém adiciona um novo produto ao carrinho, o número de itens aumenta

• Se alguém retirar um item/diminuir sua quantidade, teremos que recalcular o


valor total

• Não se pode adicionar o mesmo produto duas vezes - se isso acontecer, aumentamos a quantidade

• Um cliente não deve encomendar mais de dez peças de um único produto de uma só vez

Se um desenvolvedor não conseguir encontrar invariantes para proteger, isso significa uma de duas coisas: não há informações

suficientes sobre os requisitos de negócios ou o problema a ser resolvido é muito trivial. Neste último caso, a Arquitetura

Limpa não é necessária para o projeto.

Por outro lado, se um desenvolvedor vê claramente que existem invariantes de negócios não triviais, mas ainda está

interessado em usar modelos como Entidades, vamos considerar alguns benefícios que isso pode trazer.

Inicialmente, todas as Entidades terão seus modelos correspondentes com exatamente os mesmos campos. É

completamente natural perceber este fenômeno como uma espécie de duplicação indesejada. O mesmo sentimento pode

ocorrer com alguém que escreve um Caso de Uso/ Interator responsável pela criação de uma Entidade.

“Os campos de entrada DTO são exatamente iguais aos do meu


modelo de leilão e entidade de leilão! Esses três são quase
idênticos. Algo está muito errado com esta abordagem porque
causa duplicação.”

A cura é olhar para o futuro – inicialmente, quando um projecto começa, estes três podem de facto ter campos idênticos. Em

algum momento, eles deixarão de parecer idênticos porque todos têm motivos diferentes para mudar.

Por exemplo, é improvável que a criação de um leilão (como um fluxo no aplicativo) mude dinamicamente.

Definimos o título, escolhemos um produto e definimos o preço inicial. Depois de algum tempo percebemos que seria super

confortável manter um preço atual na Entidade em vez de calculá-lo dinamicamente toda vez com uma lista de lances de leilão,

então adicionamos um campo a ambos AuctionModel

e Entidade Leiloeira . Isso não afeta o DTO de entrada para a criação de leilões.
Machine Translated by Google

Depois de algum tempo, chega uma nova solicitação de recurso - mostrar o número de vencedores de cada
leilão em um painel de um administrador. A maneira mais simples e eficiente de conseguir isso seria apenas
adicionar uma coluna ao AuctionModel e recalcular seu valor quando salvarmos a Entidade do Leilão .
Observe que este último não precisa saber nada sobre a nova coluna. Estruturas de dados que
parecem idênticas no início do projeto podem evoluir em direções muito diferentes se apenas as razões
para a mudança forem diferentes. No entanto, há uma regularidade - quando a Entidade do Leilão muda, é
quase certo que o AuctionModel o seguirá. Tudo bem - vem da Regra de Dependência. Afinal, o modelo
depende da Entidade .

Dito isto, em um mundo ideal, deveríamos ser capazes de escrever uma classe pura arbitrária em uma
linguagem escolhida e usá-la como uma Entidade. Depois codifique responsável por persistir a Entidade
deve ser gerado automaticamente para nós. Por outro lado, dada a forma como JPA (Java
Os modelos Persistence API), Entity Framework (C#) ou SQLAlchemy se parecem, até que ponto estamos
dessa visão ideal?

importar javax.persistence.Entity;
importar javax.persistence.GeneratedValue;
importar javax.persistence.Id;
importar javax.persistence.Table;

@Entidade
@Table(name = "auctions")
public class Leilão {
@Eu ia

@GeneratedValue
ID inteiro privado ;

título da string privada ;


preço inicial privado BigDecimal; private
BigDecimal currentPrice; private
LocalDateTime terminaAt;

"/ Métodos omitidos para facilitar a leitura "*


}

Esta é a aparência de Auction no JPA - é uma classe Java pura com anotações que fornecem
metadados extras para um mecanismo de persistência. Provisoriamente, é aceitável que uma Entidade
seja um híbrido. No entanto, um modelo mental de nossas Entidades ainda será parcialmente restrito
porque precisa caber em tabelas de um RDBMS. Essa classe por si só não

permite qualquer interação direta com o banco de dados sem o EntityManager do JPA , exceto
carregamentos lentos. Seria muito bom bloqueá-los.

O SQLAlchemy do Python oferece essa possibilidade:


Machine Translated by Google

leilão_com_bids =

( sessão.query(Leilão) .options(joinload(Auction.bids), raiseload("*")

) .filter(Auction.id "= 1) .one()

leilão_with_bids do exemplo acima não permitirá o carregamento lento de nada que não tenha sido obtido
durante a consulta original.

Uma observação: ORMs de Active Record , como em Django ou Ruby on Rails, são totalmente inadequados para
construir domínios em torno deles. Eles expõem muito ao Domínio , que não deveria ser capaz de tocar na camada de
persistência de forma alguma. Nenhum JPA, Entity Framework ou SQLAlchemy implementa Active Record. Por
outro lado, ORM do Django ou Ruby on Rails são exemplos emblemáticos desse padrão.

Concluindo, reutilizar modelos ORM como Entidades é tentador, mas tem inúmeras consequências negativas. Pelo
lado positivo, isso aliviaria a tediosa escrita de código responsável pela persistência. Por outro lado, limita a liberdade
na modelagem do domínio e pode prejudicar seriamente a testabilidade.

RESUMO DO CAPÍTULO

O fluxo de controle na Arquitetura Limpa termina em um Presenter, que nem sempre é possível implementar em
frameworks específicos. Existem duas alternativas:

1. adicione outro método à interface do Presenter para obter dados formatados a serem chamados internamente
Controlador

2. Desista totalmente dos Apresentadores e use Consultas do CQRS

É possível se livrar das abstrações de limites de entrada se o acoplamento rígido de controladores a casos de uso
concretos não for um problema em seu projeto. Há também uma abordagem alternativa para desacoplar
com o Command Bus do CQRS, que envolve transformar DTOs de entrada em comandos e casos de uso em
manipuladores de comandos.

É desaconselhado o uso de modelos ORM como Entidades. Um desenvolvedor fica então limitado a ver através do
prisma das linhas do banco de dados. Pensar em termos de uma estrutura de dados tão plana é limitante.
As entidades devem ser construídas para proteger invariantes de negócios. Para fazer isso, um desenvolvedor
muitas vezes precisa ir além de um único objeto e recorrer ao gráfico de objetos. Se não houver
Machine Translated by Google

invariantes a serem protegidas no projeto, então provavelmente a Arquitetura Limpa não é uma abordagem
adequada.
Machine Translated by Google

INJEÇÃO DE DEPENDÊNCIA

ABSTRAÇÕES E AULAS EM TODA PARTE!

A profusão de interfaces nos diagramas da Arquitetura Limpa pode ser um pouco controversa para alguns.
Dentre muitas estruturas de programação, classes e interfaces abstratas são as mais odiadas e mal utilizadas.
Não é tão ruim se um desenvolvedor segue uma convenção de framework que especifica o que deve ser
abstrato e o que não deve. O uso de classes abstratas também é comumente visto com implementações do
11
padrão de design Template Method. No entanto, o uso adequado (se houver!) de abstrações é menos
comum fora dos circuitos convencionais.

A razão para culpar tal estado de coisas é a falta de um design intencional. Diz-se que o código orientado a
objetos de qualidade decente possui baixo acoplamento. Para começar, o que é acoplamento?

classe CreditCardPaymentGateway(PaymentGateway):
passar

# acoplamento apertado
Ordem da classe :
def finalise(self) "- Nenhum:
payment_gateway = CreditCardPaymentGateway( settings.payment["url"],
settings.payment["credenciais"],

) pagamento_gateway.pay(self.total)

# acoplamento solto(r)
Ordem da classe :
def _init_( self,
payment_gateway: PaymentGateway
) "- Nenhum:
self._payment_gateway = Gateway de pagamento

def finalise(self) "- Nenhum:


self._payment_gateway.pay(self.total)

Considere o exemplo acima. Na primeira parte, Order instancia uma classe concreta
CreditCardPaymentGateway para usar um de seus métodos. O pedido precisa saber exatamente o que
é necessário para construir tal instância (observe as configurações de passagem). Este é um exemplo de aperto

11 Erich Gamma et al, Design Patterns: Elements of Reusable Object-Oriented Software, Capítulo 5:
Padrões Comportamentais, Método de Modelo
Machine Translated by Google

acoplamento porque é altamente provável que a alteração do CreditCardPaymentGateway acarrete ajustes no


Pedido. A segunda parte da listagem apresenta acoplamento solto. Ordem
aceita uma instância de uma classe que herda do PaymentGateway abstrato. O pedido não apenas não está mais
sobrecarregado com a instanciação, mas também permanece ignorante do tipo de gateway de
pagamento. Simplificando, o acoplamento é a medida de quão difícil é alterar uma parte do código enquanto
mantém o resto funcionando.

Um leitor deve ter notado que essa refatoração não elimina a necessidade de instanciação, apenas transfere a
responsabilidade para qualquer pessoa que usará a classe Order . Isso significa que se Order for instanciado em 10
locais, será necessário instanciar CreditCardPaymentGateway 10 vezes?
Felizmente, isso não é verdade. Os contêineres de injeção de dependência são uma solução para esse problema.

Antes de começar a introduzir abstrações em seu código, tenha em mente que o acoplamento fraco não é um objetivo
em si. O que idealmente gostaríamos é ser capaz de fazer alterações no código sem quebrar metade das funcionalidades
ao mesmo tempo. Ao mesmo tempo, abstrações extras não vêm de graça e podem causar mais danos do que
benefícios se forem mal utilizadas.

ABSTRAÇÕES NA ARQUITETURA LIMPA

O aspecto mais complicado de usar classes/interfaces abstratas é saber onde colocá-las. A Arquitetura Limpa,
principalmente graças a uma estrutura em camadas, orienta exatamente os desenvolvedores através dessas
complexidades. No exemplo do capítulo anterior, há a interface Input Boundary que abstrai o Caso de Uso, Output
Boundary que abstrai o Presenter e, finalmente, Data Access que é uma abstração para DbAuctionsDataAccess. No
último caso, estamos tratando de um plugin (Data Access) para um fluxo de processo de negócio (Use Case). Para
explicar por que ter uma abstração aqui é tão importante, vamos brincar com cinco porquês:

1. Por que precisamos introduzir uma interface para DbAuctionsDataAccess e não estamos
permitindo que PlacingBidUseCase tenha acesso direto a ele?

Não queremos que eles estejam fortemente ligados. PlacingBidUseCase não se importa onde as entidades são
armazenadas ou de onde são recuperadas. Só precisa de algo para realizar tais operações. Algo que irá cumprir o
contrato de Acesso a Dados em forma de interface, imposta pela camada de aplicação. Ter uma interface intermediária
permite um acoplamento fraco, o que é vital neste caso.

2. Por que eu gostaria que PlacingBidUseCase e DbAuctionsDataAccess fossem vagamente


acoplado?
Machine Translated by Google

Eles representam dois mundos diferentes que você não deseja misturar. PlacingBidUseCase muda
sempre que os requisitos de negócios mudam, o que na maioria dos casos ocorre com bastante frequência.
DbAuctionsDataAccess poderá ser alterado por conta própria somente se o banco de dados usado
atualmente não for mais suficiente e precisar ser substituído. Em outras palavras, o sistema não é mais
capaz de atender requisitos não funcionais. Imagino que esta seja realmente uma situação extremamente
rara, mas que pode acontecer se, por exemplo, o projeto for bem-sucedido e um número de usuários superar
todas as expectativas. Resumindo, estas duas classes mudam por uma razão diferente e numa
proporção diferente. Tê-los acoplados força o desenvolvedor a raciocinar sobre ambos sempre que apenas
um deles precisar ser alterado. Um benefício mais imediato que se obtém é a capacidade de testar ambas
as classes separadamente.

3. Por que eu permitiria testar essas classes separadamente? Em um ambiente de produção eles sempre
serão usados juntos, então que garantia tenho de que tudo ficará bem assim que implantarmos o
projeto?

Muito tempo será economizado. O código dentro do PlacingBidUseCase é executado inteiramente na


memória; não há necessidade de chamar serviços externos, realizar operações em disco, etc. Portanto, é
muito rápido. Ao mesmo tempo, é uma parte crucial onde os requisitos de negócio são materializados. Existem
pelo menos alguns cenários possíveis com resultados diferentes. Por outro lado, DbAuctionsDataAccess é
responsável pelos requisitos não funcionais. Para cumpri-los, DbAuctionsDataAccess
aproveita um RDBMS, portanto, precisa realizar operações de E/S. Isso é algumas ordens de
magnitude mais lento do que a execução de código residente na memória. Finalmente, em DbAuctionsDataAccess
geralmente não há cenários alternativos. Sem instruções if, sem ramificações. Se não houver
possibilidade de testar essas duas classes notavelmente diferentes de forma independente, um
desenvolvedor terá que testá-las juntas. Isso pode significar uma enorme perda de
tempo só porque o DbAuctionsDataAccess não pode funcionar sem interagir com um banco de dados.

No que diz respeito à garantia de correção, mesmo os conjuntos de testes mais exaustivos testando
essas classes separadamente não garantem que todo o projeto funcionará bem. Para ter certeza, precisamos
de testes de nível superior que verificarão se PlacingBidUseCase e DbAuctionsDataAccess funcionam
perfeitamente. É claro que tal teste duplicaria de alguma forma as verificações que estamos fazendo
separadamente para ambas as classes, mas vale a pena fazer essa troca. Um teste ponta a ponta
executado por meio de uma API REST (ou interface do usuário) pode verificar um caminho feliz não apenas
para garantir que PlacingBidUseCase funcione em conjunto com DbAuctionsDataAccess, mas também
garantir que não haja atrito entre todos os outros componentes envolvidos. A estratégia de teste é um
tópico enorme. Um capítulo separado posteriormente no livro foi dedicado a ele.
Machine Translated by Google

4. O que mais posso obter com um acoplamento solto? Quais são os outros problemas que evito ao me livrar
do forte acoplamento entre PlacingBidUseCase e DbAuctionsDataAccess?

Uma carga cognitiva reduzida, pois o desenvolvedor é capaz de raciocinar sobre uma classe por vez
sem se preocupar com a segunda. Apenas sua interface deve ser levada em consideração.

Se PlacingBidUseCase fosse responsável por gerenciar a instância DbAuctionsDataAccess (por


exemplo, por criá-la), ele também teria que gerenciar todas as suas dependências. Podemos facilmente obter o
objeto de conexão do banco de dados no PlacingBidUseCase e passá-lo ainda mais para o
DbAuctionsDataAccess? Ou talvez DbAuctionsDataAccess seja responsável por estabelecer uma
conexão, mas ainda precisa de nome de usuário, senha, nome de host e número de porta. De onde o
DbAuctionsDataAccess obteria essas opções de configuração? O PlacengBidUseCase
deve fornecê-los?

5. Como gerenciar classes fracamente acopladas? Existem padrões, bibliotecas?

A resposta é Inversão de Controle (IoC) e Injeção de Dependência (DI).

INVERSÃO DE CONTROLE

A IoC dispensa as classes de criarem suas dependências, permitindo que elas especifiquem apenas
quais interfaces precisam. Quais implementações concretas serão realmente usadas não é mais da conta
deles. Na Arquitetura Limpa, basicamente significa que as camadas acima da camada de aplicação estão tomando
decisões sobre quais implementações serão usadas pelo Caso de Uso.

A injeção de dependência com Inversão de Controle é usada até mesmo no Django. O melhor exemplo é o cache
módulo. Seu uso básico é muito simples:

# coloca a string 'world' sob a chave 'hello' por 30 segundos


cache.set("olá", "mundo", 30)

# recupera tudo o que está armazenado na tecla 'hello' ou None


cache.get("olá")

Um desenvolvedor obtém o objeto de cache simplesmente importando-o - não há necessidade de instanciar


nada. Está pronto para uso imediato. Parecendo tão simples na superfície, muitas coisas acontecem nos
bastidores. Existem muitos back-ends disponíveis para cache. Um deles mantém os dados na memória. Outro é
12 que nunca salva nada em lugar nenhum e
baseado em memcached. Existe até uma implementação fictícia
sempre retorna None, simulando cache miss.

12 Memcached https://memcached.org/
Machine Translated by Google

No entanto, um desenvolvedor apenas vê um objeto sendo recebido e definido métodos. cache é, na


13 um proxy para implementação concreta.
verdade,

Ainda assim, deve haver uma maneira de definir qual implementação real será proxy pelo cache. Ou falando
de forma mais geral - para mapear interfaces em classes concretas. Injeção de dependência
técnica existe exatamente para esse fim. A chamada Inversão de Contêiner de Controle serve para
orquestrar o processo de instanciação e configuração de objetos. Voltando ao exemplo do início do capítulo -
Pedido que requer uma instância de uma subclasse de PaymentGateway - o desenvolvedor
apenas configuraria o IoC Container para instanciar CreditCardPaymentGateway quando
PaymentGateway fosse solicitado.

Naturalmente, não há razão para escrever seu próprio contêiner de inversão de controle. Existem muitas
bibliotecas excelentes disponíveis para uso. Os programadores C# têm excelente Autofac em seus 14 anos
15
disposição. Pessoas de Java podem usar Guice (Java), enquanto Pythonistas devem dar uma olhada no 16

Injector (Python). Mesmo que você não vá usar nenhum deles, seus manuais por si só fornecem muitos
conhecimentos úteis sobre injeção de dependência e inversão de controle.

Voltando ao Django e seu cache, o framework configura o backend do cache (faz o trabalho do IoC Container).
Um desenvolvedor deve apenas configurar o que gostaria de ter nos bastidores e deixar o Django fazer o resto.

CACHES =
{ "default": { #
escolha uma implementação concreta
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache", #
fornece detalhes extras, por exemplo, localização do servidor memcached
"LOCALIZAÇÃO": "127.0.0.1:11211",
}
}

Inicializar uma classe de back-end concreta e conectá-la ao cache é apenas um estágio na inicialização
de um aplicativo. Este é o momento certo para a Inversão do Contêiner de Controle estar preparada
para qualquer aplicação/linguagem etc.

13 Erich Gamma et al, Design Patterns: Elements of Reusable Object-Oriented Software, Capítulo 4:
Padrões Estruturais, Proxy

14 Autofac https://autofac.org/

15 Guice https://github.com/google/guice
16 Injetor https://injector.readthedocs.io/en/latest/
Machine Translated by Google

afinal, classes fazem parte da configuração. Isso deve ser feito antes de qualquer trabalho real ser realizado
por um aplicativo.

CONTÊINER IOC VS LOCALIZADOR DE SERVIÇO

No Django toda a cerimônia de injeção está oculta, e isso é uma coisa boa. No entanto, como um contêiner
IoC real deve ser usado? Digamos que foi configurado. Deveria ser usado livremente por código arbitrário
no projeto? Por exemplo, sempre que precisarmos de Cache, poderíamos simplesmente chamar
container.get(Cache) e receberíamos uma instância de uma classe concreta. Esse tipo de registro global é
chamado de Service Locator e é amplamente considerado um antipadrão. 17

Vejamos uma implementação referencial da Arquitetura Limpa apresentada anteriormente. Queremos


chamar PlacingBidUseCase da camada externa. Ele é abstraído por PlacingBidInputBoundary, então
pedimos ao contêiner que o forneça. Esperamos obter uma instância de uma classe concreta -
PlacingBidUseCase. Este último requer AuctionsDataAccess, portanto, dentro do PlacingBidUseCase,
usamos o contêiner mais uma vez. Obtemos uma instância de DbAuctionsDataAccess.
Se DbAuctionsDataAccess exigir uma conexão de banco de dados ou um objeto de configurações,
também teremos que usar o contêiner dentro dele para buscar o material necessário. Acho que é óbvio
para onde vai essa coisa. É assim que o Service Locator se parece em estado selvagem.

A maneira correta de fazer isso seria solicitar ao contêiner uma instância


PlacingBidInputBoundary (1ª etapa do exemplo anterior). Um contêiner IoC decente
deve ser capaz de resolver todo o gráfico de dependências para criar tudo para nós. Não há necessidade
de poluir o código com chamadas para o contêiner. Um exemplo mínimo usando Injector:

class AuctionsRepo:
def get(self, leilão_id: int) "- Nenhum:
passar

classe ColocaçãoBidUseCase:
@inject # instrui o injetor a realizar a injeção
def _init_( self,
repo: LeilõesRepo
) "- Nenhum:
self._repo = repositório

17 Mark Seemann, Service Locator é um antipadrão https://blog.ploeh.dk/2010/02/03/


ServiçoLocalizadoranAntipadrão/
Machine Translated by Google

injector = Injector() # injector


monta dependências automaticamente
use_case = injector.get(PlacingBidUseCase)
# AuctionsRepo foi criado e injetado
afirmar isinstance(use_case._repo, AuctionsRepo)

Também é recomendado usar algum código cola com uma estrutura web (ou qualquer outro mecanismo de
entrega) que irá ocultar as chamadas para o contêiner. Um exemplo de código do injetor de frasco deve
esclarecer:

# com integração de framework


@app.route("/auction/bids", métodos=["POST"]) def

leilão_bids( input_boundary: PlacingBidInputBoundary, ) "-


Resposta:
input_boundary.execute("".)
"".

# sem integração de framework


@app.route("/auction/bids", métodos=["POST"]) def leilão_bids():

# chamada manual para o contêiner necessária :(


input_boundary =
container.get( PlacingBidInputBoundary
)
"".

INJEÇÃO DE DEPENDÊNCIA VS CONFIGURAÇÃO

O aplicativo pode ser configurado de forma diferente para ambientes diferentes. Um desenvolvedor certamente
preferirá usar um banco de dados local em vez de um banco de dados de produção em seu computador.
Talvez também ajudasse desabilitar alguns fornecedores externos e usar stubs.
A injeção de dependência brilha aqui - se ao menos tivermos interfaces e contêiner IoC instalados. Isso abre
18 solução para outra. Por exemplo,
outra porta – técnicas como Branch By Abstraction para migração gradual de uma
podemos estar desenvolvendo MongoDbAuctionsDataAccess
por dias/semanas, mas não devemos usá-lo em produção até que a implementação seja concluída.
Um programador que desenvolve MongoDbAuctionsDataAccess pode usá-lo localmente, enquanto o restante
da equipe usa DbAuctionsDataAccess estável ao mesmo tempo. Fazendo injeção de dependência

18 Martin Fowler, Ramificação por Abstração https://martinfowler.com/bliki/


BranchByAbstraction.html
Machine Translated by Google

dependente da configuração torna mais fácil implementar alternâncias de recursos sem 19


sobrecarregando a base de código com instruções if estranhas . Em vez disso, haverá chamadas polimórficas
que são muito mais limpas e fáceis de ler.

O resto depende da biblioteca de injeção de dependência que decidirmos usar.

RESUMO DO CAPÍTULO

O acoplamento fraco obtido pelo uso razoável de classes e interfaces abstratas permite maior testabilidade e
capacidade de manutenção do código. O design produzido é mais elegante e flexível. Isto é especialmente
visível na Arquitetura Limpa, que afirma explicitamente a necessidade de interfaces entre a lógica de negócios e o
código específico da infraestrutura.

O gerenciamento de classes concretas que implementam interfaces exigidas pela camada de aplicação é feito por
um mecanismo denominado Injeção de Dependência. Para aproveitar totalmente a técnica, é necessário um
contêiner de inversão de controle maduro. Este último mantém configuração e conhecimento sobre instanciação de
todas as classes injetadas no projeto. Deve ser capaz de criar gráficos inteiros de objetos quando solicitado.

Em um mundo ideal, as camadas de Aplicativo e Domínio permanecem completamente ignorantes da


existência de Inversão de Contêiner de Controle. Para conseguir isso, é preciso evitar o Service Locator anti
padrão.

19
Pete Hodgson, Feature Toggles (também conhecido como Feature Flags) https://martinfowler.com/articles/
feature-toggles.html
Machine Translated by Google

CQRS
INTRODUÇÃO

A misteriosa sigla CQRS significa Command Query Responsibility Segregation. É um padrão para separar o código que altera

o estado do sistema do código que não altera nada, mas retorna dados. O código que altera o estado do sistema é denominado

Comandos, enquanto as últimas construções para recuperação de dados são chamadas de Consultas.

Pense em uma operação de consulta típica. Em um aplicativo da web de comércio eletrônico, ele pode exibir uma lista de produtos

disponíveis ou obter os endereços de entrega do usuário. Essas consultas têm poucas coisas em comum:

• eles não têm quaisquer efeitos colaterais

• o estado do aplicativo permanece intacto, não importa quantas vezes por dia solicitemos
dados


não importa se solicitamos primeiro os produtos ou os endereços de entrega - obteremos exatamente os mesmos

resultados

• normalmente, eles se resumem apenas a buscar dados de um banco de dados

Em outras palavras, as consultas são simples e seguras (não alteram o estado do sistema). Quais dados específicos eles devem

retornar são impostos por uma interface de usuário que eventualmente os apresentará ao usuário. Portanto, a única razão para as

consultas mudarem é para estar em conformidade com a interface do usuário.

Os comandos, por outro lado, são criaturas muito diferentes. Seu único propósito é alterar o estado do sistema. Em uma

aplicação de comércio eletrônico, os exemplos podem incluir adicionar um item a um carrinho, adicionar ou remover endereço

de entrega. Existem algumas coisas especiais sobre comandos:

• eles sempre mudam o estado do sistema, desde que sua execução seja bem-sucedida

• uma sequência de execução de comandos é importante - simplesmente não faz sentido remover o endereço de

entrega antes de adicionarmos um

• todas as regras de negócios devem ser aplicadas durante sua execução para garantir que o

o sistema não está em um estado incorreto posteriormente

Concluindo, os comandos não são seguros e são algumas ordens de magnitude mais complexos do que apenas ler dados de um

banco de dados SQL ou outro armazenamento de dados. Os comandos são afetados apenas por
Machine Translated by Google

mudanças nos requisitos de negócios. A nova interface de usuário brilhante após semanas de redesenho
não deve mudar as regras do jogo.

Comandos e consultas são muito diferentes entre si. O CQRS concentra-se nesta dicotomia.
A divisão não se trata de diferentes esquemas de nomenclatura de classes (ou seja, GettingAuction,
PlaceBid). Vai muito além. Na sua forma mais extrema, o CQRS propõe pilhas separadas para comandos
e consultas:

Figura 4.1 Pilhas separadas para comandos e consultas

O lado esquerdo da Figura 4.1 mostra a pilha de gravação (para Comandos). Vemos aqui todas as
camadas da Arquitetura Limpa. O lado direito da imagem é ocupado pela pilha de leitura (para consultas).
Pode-se dizer imediatamente que há menos partes móveis na pilha de consultas. A camada de Consultas
Enigmáticas desempenha o mesmo papel que a camada de Aplicação - conterá classes (Consultas) que
podem ser usadas diretamente pelas camadas superiores, por exemplo, interface web. Se encontrássemos
uma analogia para qualquer componente da Arquitetura Limpa, seriam os Casos de Uso. Tanto a Consulta
quanto o Caso de Uso são deliberadamente expostos para serem usados pelo mundo exterior. A diferença é que a cons
deve garantir que não alterará o estado do sistema de forma alguma, enquanto os Casos de Uso não
fazem tais promessas.
Machine Translated by Google

O QUE ISSO TEM A VER COM A ARQUITETURA LIMPA?

O que foi discutido até agora no livro se encaixa perfeitamente na definição da pilha de gravação.
Estamos lidando com a complexidade essencial do projeto, modelando cuidadosamente regras de negócios em
código. No CQRS, os blocos de construção parecem um pouco diferentes, mas geralmente fazem a mesma coisa.
Na pilha de gravação do CQRS, estaríamos usando Comandos, que são Objetos de Transferência de Dados.
Numa implementação, eles podem ser indistinguíveis dos DTOs de entrada. Este último é passado para Casos
de Uso, enquanto os Comandos são executados por Manipuladores de Comandos. Tanto os casos de
uso quanto os manipuladores de comandos representam um cenário de negócios no código. Como resultado, não
é necessário saber da existência de Command Handlers para utilizar os serviços da aplicação. Como substituímos
a chamada de um Caso de Uso pelo envio de um Comando, é muito mais fácil distribuir um aplicativo escrito
dessa forma. Os comandos tornam-se mensagens que podem ser enviadas pela rede.

@dataclass(frozen=True)
classe PlacecingBidInputDto:
bidder_id: int
leilão_id: int
quantidade: decimal

A Arquitetura Limpa não diz nada específico sobre operações de consulta. Portanto, assume-se que todos os
cenários, como carregar lista de endereços de entrega, obter detalhes do usuário, etc., devem ser implementados
usando Casos de Uso, Entidades, Interfaces de Acesso a Dados e Apresentadores. É muito trabalho para uma
operação simples e segura. Esta é uma excelente oportunidade para complementar a Arquitetura Limpa com um
padrão roubado do CQRS.

PILHA DE LEITURA SEPARADA - POR QUÊ?

O principal argumento a favor do aproveitamento das Consultas é uma implementação muito mais simples.
Simplificando, é necessário consideravelmente menos código para obter o mesmo resultado. Não se sacrifica
nenhuma vantagem da Arquitetura Limpa porque as operações de leitura são seguras. As regras de negócios
não são aplicáveis no contexto de consultas, pois não afetam o estado do sistema.

Em segundo lugar, usar Consultas proporciona maior liberdade na modelagem. Um desenvolvedor não precisa
refletir todos os requisitos sobre a visualização de dados na pilha de gravação e vice-versa. Em outras palavras,
mesmo que os modelos tenham que ser enriquecidos com outro campo porque tem que estar disponível na API ou

interface do usuário, eles não precisam necessariamente ser adicionados às entidades. A pilha de leitura deve
ser fácil de usar e tão prática quanto possível.
Machine Translated by Google

A vantagem final é deixar espaço para otimizações e permitir escalabilidade. A pilha de leitura funciona melhor quando

pode usar um armazenamento de dados desnormalizado. Como as consultas são implementadas separadamente,

temos liberdade para usar tabelas diferentes ou até mesmo outro banco de dados que possa ser alimentado

de forma assíncrona com dados.

Figura 4.2 Banco de dados de leitura especializado

PILHA DE LEITURA SEPARADA - COMO?

Manter os dados para consultas em um banco de dados separado é uma forma extrema de CQRS. Pode haver várias

abordagens diferentes:

• O mesmo banco de dados, as mesmas tabelas, apenas códigos diferentes para acessá-lo

• O mesmo banco de dados, tabelas diferentes preenchidas de forma síncrona

• O mesmo banco de dados, tabelas diferentes, preenchidas de forma assíncrona


Machine Translated by Google

• Diferentes bancos de dados, preenchidos de forma assíncrona

Como você pode ver, existem muitas abordagens possíveis. No livro, abordarei primeiro a maneira mais simples de fazer

CQRS. A principal diferença será implementada no código de acesso aos dados. No entanto, os padrões não devem diferir muito,

mesmo se desnormalizarmos nossos dados para uma leitura especializada do banco de dados.

CONSULTA COMO DTO

Nesta abordagem, cada Consulta é representada por uma única classe sendo um Objeto de Transferência de Dados.

A classe deve representar a intenção de obter alguns dados junto com os parâmetros necessários. Não contém

nenhuma lógica de execução. Este último deve ser colocado em um Query Handler, outra classe ou função. A execução de Query

envolve construir uma classe de consulta e passá-la para o Query Handler. Existe a opção de adicionar outro nível de indireção

entre - pode-se criar uma classe responsável por despachar consultas para manipuladores concretos.

Esse padrão é chamado de Query Bus. Com este último, não precisamos saber nada sobre manipuladores de concreto.

No que diz respeito à Arquitetura Limpa e seu princípio de dependência, a classe Consulta pertence à camada Aplicação. O

Concrete Query Handler obviamente deve ser colocado na camada Infraestrutura.

Esta abordagem se assemelha à combinação Comando - Barramento de Comando - Manipulador de Comando com a diferença

de que ao despachar um Comando nunca retorna nenhum resultado, enquanto despacha um

A consulta tem que ser.

@dataclass(frozen=True) classe
GetListOfDeliveryAddresses(Consulta):
ID_do_usuário: int

Dto = Lista[Endereço de Entrega]

def query_handler(consulta:
GetListOfDeliveryAddresses,
) "- GetListOfDeliveryAddresses.Dto:
"".

# usando via QueryBus


consulta = GetListOfDeliveryAddresses (user_id = 1) resultado =
query_bus.dispatch (consulta)
Machine Translated by Google

CONSULTAS COMO AULAS SEPARADAS

Na segunda abordagem, ainda criamos novas classes para cada consulta, mas desta vez não há nenhum Query
Bus ou Query Handler envolvido. Cada consulta é uma classe abstrata colocada na camada de Aplicação e tem
sua implementação concreta na Infraestrutura.

# em algum lugar na camada de aplicação


classe GettingListOfDeliveryAddresses (Consulta):

Dto = Lista[Endereço de Entrega]

def _init_(self, user_id: int) "- Nenhum: self.user_id =


user_id

@abc.abstractmethod
def execute(self) "- Dto:
passar

# na camada de infraestrutura
classe SqlGettingListOfDeliveryAddresses(
ObtendoListOfDeliveryAddresses
):
def execute( self,

) "- GettingListOfDeliveryAddresses.Dto: models =


self.session.query( Endereço

.filter((Address.type "= Endereço.DELIVERY)


& (Endereço.user_id "= self.user_id)
)

return
[ self._to_dto(model)
para modelo em modelos
]

Esta abordagem parece mais familiar no contexto da Arquitetura Limpa, especialmente quando pensamos nas
relações entre o Limite de Entrada abstrato e sua implementação - Caso de Uso.
Sempre que alguém deseja invocar a lógica a partir da visualização da web, eles solicitam o limite de entrada.
O mecanismo de injeção de dependência é então responsável por instanciar o Caso de Uso concreto
correspondente ao Limite de Entrada solicitado. Com consultas , isso pode parecer o mesmo. Solicita-
se uma abstração (consulta na camada de aplicativo ) e o maquinário interno retorna uma implementação
concreta da infraestrutura.
Machine Translated by Google

#configuração de injeção de dependência


@inject(config=Config) def
configure(fichário, configuração):

binder.bind(GettingListOfDeliveryAddresses,
to=SqlGettingListOfDeliveryAddresses,
)

# na visualização da web

@app.route( "/
delivery_addresses", métodos=["POST"]

) def leilão_bids(query:
GettingListOfDeliveryAddresses, ) "- Resposta:
resultado =
query.execute(
user_id = usuário_atual.id
)
"".

LER MODELO FACHADA

A terceira abordagem é a mais flexível, embora um pouco controversa. Tudo se resume a expor a interface de
consulta da infraestrutura subjacente diretamente para a camada de visualização, ignorando completamente
as camadas de Aplicativo ou Domínio.

Figura 4.3 Ignorando camadas de aplicativo e domínio com fachada de modelo de leitura
Machine Translated by Google

A flexibilidade vem da eliminação da necessidade de escrever consultas especializadas para cada visualização e
da restrição desses detalhes à camada de infraestrutura.

Dado o que sabemos sobre a pilha de leitura, mesmo que Read Model Facade pareça controverso, ainda é 100%

seguro. Pelo menos enquanto pudermos garantir que ninguém poderá usar a fachada do modelo de leitura exposta
para alterar quaisquer dados no armazenamento de dados.

Conseguir isso é um pouco complicado com ferramentas Python comuns. Django ORM requer um gerenciador
dedicado que irá gerar exceções para operações de inserção/atualização/exclusão. Com SQLAlchemy, a única
maneira segura é executar a consulta usando uma conexão separada e somente leitura com o banco
20
de dados. Os desenvolvedores .NET possuem um método mais conveniente para obter o mesmo resultado.

API CQRS versus API REST

O CQRS parece muito bom no papel, mas se um desenvolvedor tentar encaixá-lo na API REST, poderá ter algumas
surpresas desagradáveis. Todos eles se originam em diferenças entre o tempo de vida de uma solicitação
HTTP e a dicotomia de pilhas de leitura/gravação.

A expectativa comum para APIs REST é que, após a mutação dos dados, obtenhamos uma entidade alterada em
resposta. Se nossos comandos forem síncronos e soubermos imediatamente se foram bem-sucedidos, então, em
vista, emitimos uma consulta imediatamente após a execução de um comando.

# comandos nunca retornam nenhum resultado!

command_bus.dispatch(UpdateEmailCommand(user_id=1, email="sebastian@cleanarchitecture.io",
)

) result = query_bus.dispatch(GetUser(user_id=1)) # usa o


resultado para construir o modelo de visualização

Se executarmos comandos de forma assíncrona, preferiremos responder com HTTP 202 Accepted e então usar
outros mecanismos de transporte, por exemplo, WebSockets, para notificar o cliente sobre o término da operação.

Porém, se for REALMENTE necessário retornar uma resposta, pode-se recorrer à votação dos resultados
do comando. Naturalmente, isso só é acessível quando usamos asyncio, node.js ou qualquer outra solução
com corrotinas quando esperar por E/S não é um problema. Em um pré-garfo clássico

20
Nick Chamberlain, The Read Model Facade https://buildplease.com/pages/fpc-22/
Machine Translated by Google

modelo (que é, por exemplo, comum em PHP), bloquearíamos um dos trabalhadores durante a
pesquisa e limitaríamos drasticamente o rendimento do aplicativo.

CQRS versus GRAPHQL

Um garoto mais novo na cidade - GraphQL - joga muito bem com CQRS. Isso ocorre porque ele também
divide comandos de consultas, usa nomes diferentes - mutações e ...consultas. Cabe inteiramente ao
cliente da API especificar quais dados eles precisam após executar uma mutação ou não.

RESUMO DO CAPÍTULO

CQRS é uma abordagem atraente. Este capítulo o descreveu brevemente, sem entrar muito em
detalhes. Uma parte crucial do ponto de vista dos aprendizes de Arquitetura Limpa é que o uso do
conceito de pilha de leitura beneficiará e simplificará consideravelmente os projetos. Apenas ler os
dados já é uma operação segura (em vez de alterá-los), portanto não há benefício real em impor uma
disciplina de sempre ter Caso de Uso entre o Controlador e a camada de Infraestrutura .
Machine Translated by Google

LIMITE AFIADO

UMA PALAVRA SOBRE COMPLEXIDADE

A complexidade é o principal antagonista dos desenvolvedores de software. É uma medida de quão difícil
é ler e compreender o código. Sem falar na mudança. Alta complexidade significa que é necessário muito
esforço mental para compreender totalmente o fluxo de controle. É melhor não ser incomodado durante
uma luta tão desesperada. Cada interrupção ou mudança de contexto custa muito – é ao mesmo
tempo irritante e cansativo tentar voltar ao caminho certo. A complexidade se manifesta no código com
instruções if, loops e muitas outras estruturas que levam ao aninhamento do código. A complexidade
21
ciclomática é uma medida comumente usada de quão complexo é um fragmento de código. Existem
muitas ferramentas de análise de código estático que monitoram a complexidade ciclomática. Por
exemplo, os Pythonistas têm o flake8 à sua disposição.

A engenharia de software está travando uma batalha feroz contra a complexidade. Para obter vantagem
sobre qualquer oponente, é preciso primeiro conhecê-lo. Fred Brooks escreveu em seu famoso artigo
22
No Silver Bullet (incluído no livro The Mythical Man-Month) que existem dois tipos de complexidade:
acidental e essencial.

Lidamos com complexidade acidental quando o código que lemos é escrito de uma forma que
podemos melhorá-lo com um esforço finito. Você não ouviu falar sobre o operador módulo (%), então
escreveu seu próprio código que calcula o restante. Alguém escreveu uma função que possui mais de
200 linhas de código, mas com as ferramentas fornecidas pelo IDE você pode dividi-la em quatro menores
e eliminar a lógica duplicada, obtendo eventualmente 80 linhas de código. Resumindo, é assim
que a complexidade acidental se parece - ela é efetivamente combatida com código limpo,
mantendo as funções curtas - em outras palavras, sendo um engenheiro educado que utiliza
ferramentas modernas.

O segundo tipo de complexidade tem uma natureza notavelmente diferente. A complexidade essencial
reflete o quão complicado é o problema que se tenta modelar em código. Se um aplicativo precisa
fornecer 50 recursos para vários tipos de usuários, então um código escrito de forma limpa não é
suficiente para que o sistema seja fácil de entender. Para ser brutalmente franco, não há como livrar-se
deste tipo de complexidade. Nosso último recurso como desenvolvedores de software é aprender como
gerenciá-lo. Por outro lado, se for possível negociar a redução do escopo e descartar alguns recursos
menores, então a complexidade essencial será reduzida. Quando participei de um evento

21
Thomas J. McCabe, Uma medida de complexidade http://www.literateprogramming.com/
mccabe.pdf

22 Fred Brooks, o mês mítico do homem


Machine Translated by Google

23 tempestade de 2018, passamos mais de uma hora discutindo o fluxo de um processo específico
Na sessão de
no aplicativo. Quando cobrimos as paredes com post-its, tivemos algum tempo para nos aprofundar em processos
específicos. Estávamos tentando formular um algoritmo para cobrar taxas de investidores que saem e
ingressam em um investimento. Em ambos os casos, o dinheiro deve ser transferido. Nossa ideia inicial era fazer com
que os investidores que saíssem arcassem com todas as taxas de transferência impostas por um mecanismo de
pagamento. Após uma hora de discussão infrutífera, chamamos um especialista no domínio que encerrou nosso
debate em menos de 3 minutos. Decidimos que todos os investidores do nosso cenário contribuirão com taxas,
independentemente de estarem ingressando ou saindo. Como resultado, simplificamos drasticamente o algoritmo.
Observe que estávamos longe da fase de implementação. Ainda era quando usávamos apenas quadro branco,
marcadores e post-its para definir o fluxo. A moral desta história é que simplificar os requisitos de negócios poupa
aos desenvolvedores dias, senão semanas, de trabalho. Codebase permanece mais simples.

“Se algo tiver que ser comprometido – custo, cronograma ou escopo –


a escolha padrão deveria ser rotineiramente o escopo” 2 4
DOIS MUNDOS

Foi mencionado que a complexidade essencial não é algo que possa ser simplesmente eliminado,
mas que deve ser gerido. Um objetivo da Arquitetura Limpa é ter toda a complexidade possível que esteja enraizada
nos requisitos de negócios contidos em duas camadas internas – domínio e aplicativo. É por isso que eles não
deveriam ter conhecimento sobre o mundo exterior – eles são bastante complexos sem tais detalhes. As camadas
de Domínio e Aplicativo juntas formam um núcleo – um local onde, idealmente, são tomadas todas as decisões
justificadas pelos requisitos de negócios. Ambas as camadas principais são facilmente testáveis com testes de
unidade porque não possuem nenhuma dependência ou todas são abstraídas. Tenha em mente que os testes unitários
são os mais rápidos e fáceis de escrever entre todos os tipos de testes. Conseqüentemente, é barato obter alta
cobertura nesta parte do projeto, onde cada instrução if é significativa e foi escrita devido aos requisitos do
negócio.

As camadas acima do núcleo, nomeadamente Infraestrutura e superiores, são criaturas completamente diferentes. É
quase impossível colocar código de teste de unidade lá porque ele depende muito de um mundo externo assustador
- redes, discos, bancos de dados. Portanto, pretendemos não ter nenhuma tomada de decisão lá. O fluxo de controle
deve ser uma linha reta - sem ramificações, sem instruções if . Não

23 https://en.wikipedia.org/wiki/Event_storming

24 Tom Poppendieck, Mary Poppendieck, Liderando o desenvolvimento de software enxuto: os resultados não são os
Apontar
Machine Translated by Google

cenários alternativos sempre que possível. Para testar de forma confiável o código residente nessas camadas, é
necessário recorrer à integração ou a testes de nível superior. É claro que tais testes serão algumas ordens
de magnitude mais lentos e mais complicados do que os testes unitários que cobrem duas camadas principais.

LIMITE ENTRE APLICAÇÃO E EXTERNO


MUNDO

Entre duas camadas internas (Domínio englobado pela Aplicação) e todas as restantes existe um abismo. O
fluxo de controle não deve cruzá-lo descuidadamente. A Arquitetura Limpa torna difícil fazer algo imprudente
ao introduzir o Limite de Entrada e o DTO de Entrada. Embora o primeiro possa ser omitido sob certas
condições, ainda é preciso prestar atenção aos DTOs de entrada.

ESCREVER DTOS DE ENTRADA

É crucial sempre ter a Regra de Dependência em mente ao projetar DTOs de entrada. Esta é a única
coisa que fica entre as camadas centrais e o assustador mundo exterior.

Um desenvolvedor não deve passar nada que não possa ser compreendido por Domínio e Aplicativo
camadas. Isso significa que os DTOs de entrada podem ser criados apenas usando tipos de dados integrados ou classes

definidas em uma das camadas principais.

No entanto, não está claro qual componente da Arquitetura Limpa é responsável pela validação
dos dados. A verificação da exatidão dos dados pode não ser uma função dos DTOs de entrada.
Por outro lado, é absolutamente certo que quando um campo do DTO é acessado por Use Case,
esperamos que ele esteja correto, pelo menos no que diz respeito aos tipos.

@dataclass
classe ColocandoBidInputDto:
bidder_id: int
leilão_id: int
quantidade: decimal

Por exemplo, devemos ser capazes de confiar em PlacingBidInputDto que o campo bidder_id é um número
inteiro válido, mas é claro, não somos obrigados a saber se um licitante com tal id existe antes mesmo
que o fluxo de controle atinja o Caso de Uso.

OBJETOS DE VALOR

Ter certeza sobre os tipos é algo muito bom de se ter (agora os desenvolvedores que trabalham com
linguagens de tipo estaticamente sorriem), mas é insuficiente (agora eles não estão mais sorrindo).
Machine Translated by Google

Nos exemplos de código que foram mostrados até agora sempre que precisei representar alguma quantia
de dinheiro, usei a classe interna do Python - Decimal.

O problema é que nem todo decimal válido faz sentido como quantia em dinheiro.

Figura 5.1 Apenas um subconjunto de possíveis objetos


decimais pode ser considerado uma quantia válida de dinheiro

Exemplos de objetos decimais que podem ser usados como quantia em dinheiro:

• Decimal('0,01')

• Decimal('10,99')

• Decimal('5,49')

Respectivamente, Decimal que não pode ser tratado como quantia em dinheiro:

• Decimal('-1,99')

• Decimal('3,1415')

• Decimal('-1.0E3')

O objetivo desses exemplos é ilustrar que o tipo Decimal integrado não é suficiente para expressar o
conceito de dinheiro. Não podemos impor a precisão desejada a duas casas decimais. Além disso,
não existe noção de moeda, que é uma propriedade vital quando falamos de dinheiro. É claro que
precisamos de outro tipo dedicado. Que tal Decimal?

Antes de nos aprofundarmos nos detalhes da implementação, vamos pensar nas características que nosso tipo
Money deve ter:
Machine Translated by Google


deve ser imutável - uma vez criado, não pode ser alterado


não deve ser possível criar tal objeto com um estado inválido.

Decimal('python') gera uma exceção


representa valor, não um objeto de longa vida - não tem identidade

• instâncias com o mesmo valor são sempre consideradas iguais

+ Específico para dinheiro:


deve suportar operações aritméticas, assim como Decimal faz

Esses tipos são chamados de Objetos de Valor. Iremos usá-los extensivamente no exemplo completo apresentado

posteriormente neste livro.

A implementação pode ser conduzida por testes para ilustrar melhor as nossas expectativas. Nós começamos

especificando uma classe base para moeda, para que possamos estender facilmente o Money sempre que

surgir uma nova moeda (criptomoeda). A moeda garantirá que nosso Dinheiro seja aberto para extensões, mas fechado

para modificações. Isso significa que basta subclassificar Moeda para parametrizar o comportamento da classe

Money sem ter que modificar seu código-fonte. A propósito, isso é chamado de Princípio Aberto-Fechado e significa “O”
25
na famosa sigla SOLID.

moeda da classe:
decimal_precision = 2
símbolo = Nenhum

classe USD (Moeda):


símbolo = "$"

A moeda é necessária para criar a instância do Money. Como as classes são objetos em Python, poderíamos

simplesmente passar a subclasse de moeda desejada:

class Dinheiro:
def _init_( self,
moeda:
Tipo[Moeda], quantidade: str,

) "- Nenhum:
"".

25 SÓLIDO https://en.wikipedia.org/wiki/SÓLIDO
Machine Translated by Google

A primeira propriedade dos Objetos de Valor é a imutabilidade. Não se pode realmente garantir isso em
Python devido à sua natureza dinâmica. Geralmente é suficiente acrescentar nomes de variáveis de
instância com um único sublinhado, para que o linter e o IDE possam nos avisar. Expor esses campos como
propriedades somente leitura ainda pode ser uma boa ideia:

classe Dinheiro:
def _init_( self,

moeda: Tipo[Moeda], quantidade:


str, ) "- Nenhum:
self._currency
= moeda self._amount =
Decimal(quantia)

@propriedade
def moeda(self) "- Tipo[Moeda]:
retornar self._currency

@propriedade
def quantidade(self) "- Decimal:
retornar self._amount

A segunda característica dos Objetos de Valor é que é impossível instanciar um objeto com um valor inválido.
Portanto, Value Object realiza a validação durante a inicialização:
Machine Translated by Google

classe Dinheiro:
def _init_( self,

moeda: Tipo[Moeda], quantidade:


str, ) "- Nenhum:
se não
inspecionar.isclass(
moeda)
ou não issubclass(moeda, Moeda):
raise
ValueError( f"{moeda} não é uma subclasse de Moeda!"

)
tente: quantidade_decimal = Decimal(quantidade)
exceto decimal.DecimalException: raise

ValueError( f'"{quantidade}" não é um valor válido!'


)

d_tuple = decimal_amount.as_tuple() if
d_tuple.sign: raise
ValueError( f"quantidade
{quantidade} não deve ser negativa!"

) elif ( -
d_tuple.exponent >
moeda.decimal_precision
):
raise ValueError(f"o
valor determinado tem precisão inválida! Deveria ter"
f"não mais do que {currency.decimal_precision} casas decimais!"
)

self._currency = moeda
self._amount = decimal_amount

Objetos de valor não têm conceito de identidade - eles devem ser indistinguíveis se forem criados usando a
mesma moeda e valor igual:
Machine Translated by Google

classe Dinheiro:
"".

def _eq_(self, other: Money) "- bool: se não for


isinstance(other, Money):
retorna falso
return
(self.currency "= other.currency e
self.amount "= other.amount
)

Dinheiro(USD, 1) "= Dinheiro(USD, "1,00") # Verdadeiro


Dinheiro(USD, 1) "= Dinheiro(USD, "5,00") # Falso

Objetos de valor são ótimos para expressar as complexidades da realidade. Por exemplo, vamos supor que
alguém precise oferecer suporte a diversas moedas. Não faz muito sentido comparar valores brutos quando
um está em dólares americanos e outro em euros. Assim como o Python não gosta quando se tenta
adicionar int ao str. Por que não escrever um código que proteja isso?

RESUMO DO CAPÍTULO

Este capítulo discutiu o significado e a motivação por trás do estabelecimento de uma fronteira entre duas
camadas centrais internas - Domínio abrangido pela Aplicação e o resto do mundo. Todo o código que
toma decisões deve ser colocado dentro do Domínio ou Aplicativo, onde pode ser testado em unidade quase
sem esforço.

A Regra da Dependência permanece em vigor, portanto nada das camadas externas pode cruzar a
fronteira. É preciso traduzir as coisas de fora para conceitos conhecidos nas camadas centrais. Value
Object é um padrão útil que ajuda a conseguir isso. Alivia as camadas internas de validação. Objetos de
valor, graças à sua imutabilidade, podem ser repassados com segurança.
Machine Translated by Google

EXEMPLO DE PONTA A PONTA

ONDE COMEÇAR?

Já é hora de usar o conhecimento da Arquitetura Limpa para construir algo. Este capítulo irá guiá-lo
passo a passo através do processo de desenvolvimento de um projeto exemplar.

Em um mundo ideal, os desenvolvedores receberiam uma extensa documentação descrevendo


todos os requisitos. Esse documento não mudaria com o tempo. Toda a equipe de desenvolvimento
trabalharia em velocidade constante, viveria em relacionamentos felizes e nunca ficaria doente. O
mundo descrito não existe. É comum lidar com mudanças nos requisitos, uma vez que o
desenvolvimento de software é um processo de aprendizagem – as empresas descobrem o
que precisam e o que é esperado pelo mercado durante todo o ciclo de vida de um projeto. Durante uma
palestra na conferência, ouvi uma vez que antes de anexar qualquer interface de usuário, toda a parte
do domínio e suas complexidades podem ser codificadas. Imagino que esta abordagem faça
todo o sentido, pois lida com a incerteza mais significativa em primeiro lugar. No entanto, em um
ambiente Agile, espera-se que as equipes agreguem valor ao negócio após cada iteração, desde o
início. É necessária alguma interface de usuário; caso contrário, a funcionalidade não poderá ser
considerada entregue porque não há como utilizá-la.

As metodologias ágeis não dependem de documentação detalhada que descreva todos os cenários
possíveis. Em vez disso, eles usam uma ferramenta chamada User Story. São frases curtas que parecem
muito mais um convite para falar do que uma especificação real. Alguns exemplos:

• Como licitante quero dar uma licitação para poder ganhar o leilão.

• Como licitante, quero receber uma notificação por e-mail quando meu lance for vencedor.

• Como administrador, quero retirar lances para que um licitante mal-intencionado não ganhe
um leilão.

• Como administrador quero criar leilões para que os licitantes possam fazer lances neles.

A maioria das histórias de usuários são candidatas perfeitas para casos de uso. Como o primeiro geralmente
é tudo o que temos, faz todo o sentido começar a codificar a partir de um Caso de Uso quando se
está em uma História de Usuário nova e brilhante. No entanto, não é a única coisa com que se preocupar em
um novo projeto. Como é preciso garantir que os resultados do seu trabalho estarão apresentáveis desde o
início, é igualmente importante inicializar o projeto normalmente. Em outras palavras, pode-se considerar
a primeira iteração concluída quando pelo menos um cenário básico da User Story é codificado, E pode ser
mostrado aos stakeholders ou outras partes interessadas.
Machine Translated by Google

Foi exatamente assim que comecei minha jornada com a Arquitetura Limpa. Logo após criar uma estrutura básica
para o projeto a partir de um template, sentamos com minha colega Dominika para uma sessão de programação em
pares. Durante duas horas, elaboramos o fluxo do cenário básico que fazia parte do processo mais complicado
da aplicação. Após a sessão que resultou em Casos de Uso, Entidades e testes para eles, Dominika continuou a
trabalhar nelas sozinha.

Ela manteve contato com o cliente, resolvendo ambigüidades e descobrindo casos extremos. Ao mesmo tempo, eu
estava trabalhando na conexão do caso de uso à camada da API REST e no fornecimento de uma implementação
simples de acesso a dados na memória. Depois de menos de uma semana, demonstramos os resultados ao
cliente usando o Postman. A parte do frontend ainda estava em andamento na época, mas isso não nos impediu
de entregar valor ao negócio. Ah, e é claro que o banco de dados ainda não estava conectado neste estágio.

ESQUELETO ANDANDO

Uma certa quantidade de trabalho de base deve ser feita durante a primeira iteração. Esta etapa está
inseparavelmente ligada à tecnologia que se está utilizando, por isso não irei me aprofundar em muitos
detalhes aqui.

Por exemplo, em Python, seria possível usar o startproject do Django e, em seguida, os comandos startapp
para criar estrutura e configuração básicas. Para diferentes estruturas (e também tecnologias), pode-se recorrer ao
cookiecutter. O objetivo desta etapa é simples: conseguir conectar rapidamente o Caso de Uso ao mecanismo
de entrega desejado. Neste caso, quero dizer API REST.

CASO DE USO / INTERATOR DE PLACINGBID

Vamos lidar com a primeira história de usuário:

• Como licitante quero dar uma licitação para poder ganhar o leilão.

Este é um ajuste perfeito para um caso de uso.

NOMEAÇÃO

O nome do Caso de Uso deve refletir o cenário de negócios que está sendo modelado. Neste caso, proponho chamá-
lo de PlaceingBid. Como classe, terá apenas um método público. Pode ser algo genérico, como execute ou
mais específico - place_bid.

class ColocaçãoBid:
def execute(self) "- Nenhum:
passar
Machine Translated by Google

ARGUMENTOS

Se um Caso de Uso requer argumentos (nem sempre é o caso), define-se um DTO de Entrada:

@dataclass(frozen=True)
class PlacengBidInputDto:
bidder_id: BidderId
leilão_id: Valor do AuctionId:
Dinheiro

O decorador @dataclass gera o método _init_, para que seja possível criar uma instância de
ColocandoBidInputDto por:

input_dto = ColocandoBidInputDto(1, 2,
Dinheiro(USD, "10,00")
)

Observe que todos os campos do DTO de entrada são objetos de valor. Aqui temos tipos dedicados
para BidderId e AuctionId. Algumas palavras sobre isso serão fornecidas na seção seguinte sobre Entidades.

O próprio DTO de entrada também é um objeto de valor. Um DTO de entrada não precisa copiar o nome do
Caso de Uso. Faz sentido modelá-lo como uma classe interna de Caso de Uso, permitindo assim
abreviar o nome do DTO de Entrada:

class PlacengBid:
@dataclass(frozen=True)
class InputDto:
bidder_id: BidderId
leilão_id: Valor do AuctionId:
Dinheiro

def executar (
eu mesmo, input_dto: InputDto
) "- Nenhum:
passar

SAÍDA

Se um Caso de Uso gera alguns dados (nem sempre é um caso!), define-se um DTO de Saída:

@dataclass(frozen=True)
class PlacecingBidOutputDto:
is_winning: bool
preço_atual: dinheiro
Machine Translated by Google

A regra que se aplica ao DTO de entrada e saída é que eles devem ser muito rigorosos quanto aos tipos de
campos e verificar se os dados transmitidos são do tipo esperado.

Se for necessário gerar alguns dados de um Caso de Uso, falta uma interface (Limite de Saída) que
eventualmente apresentará PlacingBidOutputDto:

classe ColocaçãoBidOutputBoundary (abc.ABC):


@abc.abstractmethod
def presente(
self, output_dto: ColocandoBidOutputDto
) "- Nenhum:
passar

O limite de saída deve ser injetado durante a construção do PlacingBid, então estendemos seu __init__:

classe PlacingBid: def


_init_(self,

output_boundary: PlacengBidOutputBoundary,) "-


Nenhum:
self._output_boundary = output_boundary

def executar(
self, input_dto: ColocandoBidInputDto
) "- Nenhum:
passar

TESTE DE UNIDADE

Agora que estabelecemos as bases, é possível começar a escrever o código real. Como definimos entrada
e saída, podemos escrever o teste mais direto para o Caso de Uso. Vamos tentar o desenvolvimento orientado
a testes:

classe ColocandoBidTests(unittest.TestCase):
def setUp(self) "- Nenhum:
self.output_boundary_mock = Mock(
spec_set=PlacingBidOutputBoundary

) self.use_case = ColocaçãoBid(
self.output_boundary_mock
)
Machine Translated by Google

def test_presents_data_for_winning(self):
preço = Dinheiro (USD, "10,00")
input_dto = PlacingBidInputDto
(bidder_id = 1,
leilão_id = 2,
quantidade = preço,
)

self.use_case.execute(input_dto)

esperado_output_dto=PlacingBidOutputDto(is_winning=True,
current_price=preço

) self.output_boundary_mock.present.assert_called_once_with(
esperado_output_dto
)

Este é um teste bastante simples e ingênuo. Não apenas se baseia em muitas suposições (caminho feliz -
vitória, leilão existe, licitante existe, etc.), mas também falhará porque PlacingBid.execute
não tem código dentro. TDD trata de dar pequenos passos. Poderíamos tornar o teste verde com este código:

classe PlacingBid:
def _init_(self,

output_boundary: PlacengBidOutputBoundary,) "-


Nenhum:
self._output_boundary = output_boundary

def executar(
self, input_dto: PlacingBidInputDto ) "-
Nenhum:

self._output_boundary.present( PlacingBidOutputDto(is_winning=True, current_price=input_dto.amount,


)
)

Dar passos tão pequenos pode ser benéfico no início, mas quando alguém se sentir mais confiante,
poderá deixar o teste falhando por enquanto e começar a criar as peças que faltam. Além disso, não há muito
que possamos fazer sobre o caso de uso do PlacingBid por enquanto.
Machine Translated by Google

ENTIDADES DE LEILÃO E LICITAÇÃO

Assim que identificamos um nome para um conceito no domínio que possa proteger as regras de negócios da

Empresa, criamos uma Entidade.

NOMEAÇÃO

As entidades geralmente são nomeadas de forma singular. Devem receber um nome inequívoco que indique claramente

a sua função. Se você tiver dificuldade para encontrá-lo, converse com outras pessoas, especialmente especialistas

no domínio e gerentes de projeto.

OBJETOS DE VALOR PARA TIPOS DE IDENTIDADE

Você deve ter notado o enigmático AuctionId ou BidderId na seção anterior sobre casos de uso. Nos bastidores,

esses são apenas apelidos para int, mas graças à sua nomenclatura eles são muito mais significativos do que números

inteiros. É também uma forma de encapsulamento. Apenas muito poucos lugares deveriam realmente se preocupar

com o tipo de identidade, por isso faz sentido ocultar esta informação. As definições de AuctionId e BidderId

são diretas:

ID do lance = int
ID do leilão = int

IMPLEMENTAÇÃO

Idealmente, Entidades são classes escritas em Python puro. Eles não herdam das classes base do ORM, etc. Quando

os escrevemos, nos esforçamos para obter o mínimo de dependências possível, embora ajudantes como o Lombok do
26
Java ou os dataclasses/attrs do Python que podem gerar código repetitivo para nós sejam bem-vindos. Esta biblioteca

pode, por exemplo, nos fornecer um construtor padrão para definir todos os campos definidos em uma classe.

Voltando ao Python, é assim que um esboço de classe pode ser:

26
Projeto Lombok https://projectlombok.org/
Machine Translated by Google

Leilão de classe:
def place_bid( self,
bidder_id: BidderId, amount: Money
) "- Nenhum:
passar

@propriedade
def preço_atual(self) "- Dinheiro:
passar

@propriedade
def vencedores(self) "- Lista[BidderId]:
passar

Este trecho de código define apenas um método e duas propriedades (campo somente leitura com
implementação para não-Pythonistas) que serão necessários para um caso de uso PlacingBid.
Obviamente, é necessário dizer ao Leilão para fazer um lance e, finalmente, pedir uma lista de vencedores
e o preço atual para construir uma instância de PlacingBidOutputDto.

Pare por um segundo e observe novamente a definição do método vencedor e o valor de retorno que foi
anotado - List[BidderId]. Contém muito mais informações que List[int], não é?

TESTE DE UNIDADE

Como as entidades são, em sua maioria, classes puras de Python (ou de qualquer outra linguagem),
sem dependências externas, elas são triviais para teste de unidade. Há uma infinidade de casos de teste que
podemos imaginar em termos da Entidade do Leilão. Para começar, pense no preço atual. O que deveria ser
quando o Leilão acabou de ser criado e ninguém tocou nele? É do conhecimento geral que os leilões têm um
preço inicial para evitar que os itens sejam vendidos muito abaixo do seu valor. Esta é uma das
complexidades do domínio de leilão que acabamos de descobrir ao nos perguntarmos qual deveria ser o preço
atual de um leilão, desde que ninguém tenha feito um lance ainda.

classe AuctionTests(unittest.TestCase):
def test_untouched_auction_has_current_price_equal_to_starting(
self, )
"- Nenhum:
preço_inicial = Dinheiro(USD, "12,99") leilão =

Leilão( preço_inicial=preço_inicial
)

assert
(preço_inicial "=
leilão.preço_atual
)
Machine Translated by Google

Isso falhará por dois motivos: Auction atualmente não aceita nenhum parâmetro durante a construção e a
propriedade current_price sempre retorna None. Tal implementação fará com que o teste passe:

Leilão de classe:
def _init_(self,
preço_inicial: Dinheiro) "- Nenhum:

self._preço_inicial = preço_inicial

@propriedade
def current_price(self) "- Dinheiro: return
self._starting_price

Com facilidade, é possível produzir muitos testes unitários que verificarão vários cenários. Esse conjunto de testes
é bastante extenso e leva muito pouco tempo para ser executado. Isso é exatamente o que queremos alcançar,
puxando a tomada de decisões para a camada de Domínio, onde é ridiculamente barato codificar e testar.
Embora seja tentador testar extensivamente nossas entidades separadamente, evite isso. Uma flexibilidade
muito maior pode ser alcançada se decidirmos fazê-lo por meio de testes de Casos de Uso. Há muito mais
informações sobre estratégias de teste no capítulo Testes.

Anteriormente, também durante discursos públicos, aconselhei fortemente entidades de testes unitários.
Embora ainda possa ser útil acertar certos casos extremos neste nível de teste, não acho mais que essa
deva ser a estratégia padrão. A forma que recomendo atualmente é testar a implementação de Entidades por meio
de testes chamando Casos de Uso. Pelo menos no começo.

IMPLEMENTAÇÃO CONTINUADA

Outra Entidade deverá ser introduzida no processo – Licitação. Representa uma única oferta feita por um licitante.
Observe que não apresentamos uma Entidade para um licitante. Não há necessidade - na verdade precisamos
apenas da identificação deles para identificar os vencedores. Se a existência de uma Entidade separada for
justificada por requisitos comerciais, criaremos também o Licitante. A entidade de licitação é muito simples.
Os lances recém-criados não têm IDs, mas assim que os persistirmos, todos eles obterão uma identidade.

@dataclass
lance de classe:

id: Opcional[BidId]
bidder_id: Valor do
BidderId: Dinheiro

Voltando ao leilão, aqui está uma implementação que realmente faz alguma coisa:
Machine Translated by Google

class Leilão: def


_init_( self, id:

AuctionId, título:
str,
preço_inicial: Dinheiro,
lances: Lista[Bid], )
"- Nenhum:
self.id = id
self.title = título
self.starting_price = preço inicial self.bids =
classificado (lances,
key=lambda lance: lance.valor
)

def place_bid( self,


bidder_id: BidderId, amount: Money ) "- Nenhum: if
amount >
self.current_price: new_bid =
Bid( id=None,

bidder_id=bidder_id,
amount=amount,

) self.bids.append(new_bid)

@propriedade
def current_price(self) "- Dinheiro: se não
for self.bids:
retorne self.starting_price senão:

retornar self._highest_bid.amount

@propriedade
def vencedores(self) "- Lista[BidderId]:
se não for self.bids:
retorne []
retorne [self._highest_bid.bidder_id]

@propriedade
def _highest_bid(self) "- Lance: return
self.bids[-1]

É assim que um exemplo clássico de uma Entidade deve ser. Não há dependências, todo o código é escrito
apenas para fazer cumprir as regras de negócios. Não mexa com bancos de dados ou qualquer outra
coisa externa.
Machine Translated by Google

INTERFACE DE ACESSO A DADOS (REPOSITÓRIO DE RESUMOS)

O código responsável pela lógica que faz os lances reside na Entidade Leilão, mas o Caso de Uso ainda não tem a

possibilidade de buscar a Entidade e persisti-la posteriormente. É hora de escrever uma interface para isso. A Interface

de Acesso a Dados residirá na mesma camada dos Casos de Uso – o Aplicativo.

NOMEAÇÃO

27
Como usarei um padrão chamado repositório orientado a persistência, meu repositório será chamado de

[EntityName]Repository ou [EntityName]Repo. Tenha em mente que também existe um repositório orientado a coleções.
28
Contudo, sua descrição permanece além do escopo deste livro.

IMPLEMENTAÇÃO

Repositório orientado à persistência - em sua forma básica (e suficiente para o exemplo) terá a seguinte aparência:

classe AuctionsRepository(abc.ABC):
@abc.abstractmethod def
get( self,
leilão_id: AuctionId ) "- Leilão:

passar

@abc.abstractmethod def
save(self, leilão: Leilão) "- Nenhum:
passar

O método get existe para recuperar a entidade do leilão usando seu AuctionId, enquanto o método save é responsável

por persistir o leilão.

ACESSO A DADOS (REPOSITÓRIO)

Neste exemplo, uma implementação da Interface de Acesso a Dados será finalmente uma classe concreta que depende de um
banco de dados relacional, por exemplo, PostgreSQL.

27
Vaughn Vernon, Implementando Design Orientado a Domínio, Capítulo 12. Repositórios, Persistência-

Repositórios Orientados

28
Vaughn Vernon, Implementando Design Orientado a Domínio, Capítulo 12. Repositórios, Coleção-

Repositórios Orientados
Machine Translated by Google

IMPLEMENTAÇÃO NA MEMÓRIA

Embora manter os dados em armazenamento persistente seja uma necessidade absoluta na produção,
é benéfico, por vários motivos, começar com um acesso a dados na memória. Em primeiro lugar, é muito mais
fácil escrever para que se possa entregar tal implementação rapidamente. Em segundo lugar, ele pode ser usado
em testes de casos de uso, em vez de simulações ruins. Por último, é possível iterar mais rapidamente com todo o
fluxo - deve ser aceitável apresentar um recurso funcional com um armazenamento de dados na memória para
obter algum feedback das partes interessadas.

EVOLUINDO A IMPLEMENTAÇÃO NA MEMÓRIA COM TDD

Existem apenas dois métodos em AuctionsRepository. Um desenvolvedor poderia cobri-los com um único teste, desde

que pudesse assumir que todos os leilões serão criados (salvos pela primeira vez, para ser mais preciso) usando-o.

Testar dois métodos ao mesmo tempo pode parecer um antipadrão, mas por enquanto é exatamente o que precisamos

para verificar completamente o comportamento de uma classe em teste. Isso é TDD, então dedicamos apenas um esforço

mínimo para levar as coisas adiante:

classe ColocandoBidTests(unittest.TestCase):
def test_should_get_back_saved_auction(
self, )
"- Nenhum:
lances =

[ Bid( id=1,
bidder_id=1,
amount=Money(USD, "15,99"),
)

] leilão = Leilão( id=1,

title="Livro incrível",
preço_inicial=Dinheiro(USD, "9,99"), lances=lances,

) repo = InMemoryAuctionsRepository()

repo.save (leilão)

afirmar repo.get(auction.id) "= leilão

Para que isso funcione, Auction deve suportar um operador de comparação ( "= ). Em linguagens que não suportam

sobrecarga de operadores, usaríamos qualquer convenção presente (por exemplo, o método equals() em Java). Em

Python, uma implementação que faz é possível comparar Entidades pode ficar assim:
Machine Translated by Google

Leilão de classe:
def _eq_(self, other: "Leilão") "- bool:
# verificamos se o tipo e os campos são idênticos
return isinstance(outro,
Leilão) e vars(self)
"= vars(outro)

A implementação do repositório que passa no teste é a seguinte:

classe InMemoryAuctionsRepository(
Repositório de Leilões
):
def _init_(self) "- Nenhum: self._storage:
Dict[ AuctionId, Leilão] = {}

def get( self,


leilão_id: AuctionId ) "- Leilão: return

copy.deepcopy( self._storage[auction_id]

def save(self, leilão: Leilão) "- Nenhum:


self._storage[auction.id] = copy.deepcopy(leilão

Você pode estar se perguntando por que a classe mantém e retorna cópias de objetos? Em Python, os
objetos são passados usando referências. Se alguém usar esse repositório baseado em referência para
obter o leilão, verá alterações sujas, mesmo que não tenham sido salvas explicitamente com o repositório.
Isso não é aceitável.

Com uma implementação na memória, agora podemos voltar ao caso de uso PlacingBid.

FINALIZANDO NOSSO PRIMEIRO CASO DE USO

INJEÇÃO DE DEPENDÊNCIA

Eventualmente, o PlacingBid precisa de uma implementação concreta do AuctionsRepository para fazer o


trabalho. Embora seja aceitável usar InMemoryAuctionsRepository em testes, a implementação na
memória não é algo que possamos executar em produção. Está claro que o caso de uso não deve instanciar
AuctionsRepository por conta própria para evitar o acoplamento. Felizmente, existe uma injeção de
dependência que podemos aproveitar.
Machine Translated by Google

Para começar, é necessário habilitar o Caso de Uso para injeção de dependência, ou seja, aceitar
AuctionsRepository por meio de seu construtor _init_. Uma implementação de
PlacingBidOutputBoundary já foi aprovada dessa forma, portanto, adicionar AuctionsRepository parece o
próximo passo natural.

class PlacingBid: def


_init_( self,

output_boundary: PlacengBidOutputBoundary,
leilões_repo: AuctionsRepository, ) "-
Nenhum:
self._output_boundary = output_boundary
self._auctions_repo = leilões_repo

FAZENDO A PRIMEIRA PASSAGEM NO TESTE RAZOÁVEL

Como consequência, precisamos alterar um pouco nosso teste para acomodar essa mudança. Temos que
criar uma instância do InMemoryAuctionsRepository, salvar um leilão e passá-lo para o Caso de Uso:

classe ColocandoBidTests(unittest.TestCase):
FRESH_AUCTION_ID = 2

def setUp(self) "- Nenhum:


self.output_boundary_mock = Mock(
spec_set=PlacingBidOutputBoundary

) repo = self._create_repo_with_auction()
self.use_case =
PlacingBid( self.output_boundary_mock, repo
)

def _create_repo_with_auction(self,

) "- AuctionsRepository: repo =


InMemoryAuctionsRepository() fresh_auction
= Auction(
self.FRESH_AUCTION_ID,
"meias",
Dinheiro(USD, "1,99"), [],

) repo.save(fresh_auction)
retornar repositório

Finalmente, lá vai uma implementação de um Caso de Uso que passa no teste:


Machine Translated by Google

classe ColocaçãoBid:
def executar(
self, input_dto: PlacingBidInputDto) "- Nenhum:
leilão =
self._auctions_repo.get( input_dto.auction_id

)
leilão.place_bid( bidder_id=input_dto.bidder_id,
quantidade=input_dto.amount,

) self._auctions_repo.save(leilão)

saída_dto = ColocandoBidOutputDto(
is_winning=input_dto.bidder_id em
leilão.winners,
current_price=auction.current_price,

) self._output_boundary.present(output_dto)

Finalmente, o teste original para o Caso de Uso é aprovado. Agora, pode-se escrever outro teste com falha e evoluir

a implementação do caso de uso PlacingBid com entidades subjacentes.

REMOVER CÓDIGO BOILERPLATE COM REFATORAÇÃO

Para que um ciclo TDD seja completo, devemos polir um pouco o código com refatoração. Atualmente, nosso código é

bastante simples e não há muitas oportunidades para melhorá-lo. No entanto, podemos nos livrar dos métodos
29
inicializadores __init__ se aproveitarmos a excelente biblioteca de atributos, que é uma substituição de terceiros com

mais recursos para classes de dados integradas.

importar atributo

@attr.s(auto_attribs=True) classe
PlacingBid:
_output_boundary: PlacingBidOutputBoundary _auctions_repo:
AuctionsRepository

def executar(
self, input_dto: ColocandoBidInputDto
) "- Nenhum:
"".

A mesma refatoração pode ser aplicada à Entidade de Leilão.

https://www.attrs.org/en/stable/ 29
Machine Translated by Google

CÓDIGO DE EMBALAGEM

APLICATIVO DE EMBALAGEM E CÓDIGO DE DOMÍNIO

O código mostrado até agora ainda não foi organizado em camadas. Embora não exista noção de
web, bancos de dados reais etc., já é possível tratar Aplicação com Domínio como um artefato de código
separado. Em Python, isso significa que esse código pode se tornar um pacote independente - como
um daqueles que obtemos usando pip install. Este truque permite aplicar a Regra da Dependência,
mesmo em uma linguagem tão liberal como o Python. Em diferentes linguagens de programação,
como Java, existem blocos de construção mais apropriados para impor a separação de camadas.

Para começar, haverá quatro pacotes separados:

1. Aplicativo e Domínio

2. Infraestrutura

3. Principal

4. Rede

Aplicativo e Domínio serão despojados de dependências externas sempre que possível, enquanto a
Infraestrutura dependerá da primeira. O pacote principal é introduzido para desacoplar a montagem
de objetos de qualquer mecanismo de entrega. Obviamente, deve estar ciente de dois pacotes anteriores.
A Web conhecerá o Main, pois não montará coisas por conta própria e precisará do contêiner IoC para
colocar as mãos nos casos de uso. A estrutura sugerida de diretórios e arquivos é a seguinte:
Machine Translated by Google

/
ÿÿÿ leilões
ÿÿÿ leilões nº 1
ÿÿÿ ÿÿÿ aplicativo nº 2
ÿÿÿ ÿ ÿÿÿ "_init"_.py
ÿÿÿ ÿ ÿÿÿ repositórios
ÿÿÿ ÿ ÿ ÿÿÿ leilões.py
ÿÿÿ
ÿÿÿ ÿ ÿ "_init"_.py
ÿÿÿ
ÿÿ casos de uso
ÿÿÿ ÿÿÿ "_init"_.py
ÿÿÿ
testes colocação_bid.py
#4 ÿ ÿ ÿ ÿÿÿ domínio #3
ÿ ÿÿÿ entidades
ÿ ÿ ÿÿÿ leilão.py
ÿ ÿ ÿÿÿ bid.py
ÿÿÿ
ÿÿÿ "_init"_.py
ÿÿÿ exceções.py
ÿ ÿÿÿ "_init"_.py
ÿÿÿ
ÿ
valor_objetos.py
ÿÿÿ
"_init"_.py

ÿ ÿÿÿ aplicativo nº 5
ÿ ÿ ÿÿÿ "_init"_.py
ÿÿÿ
ÿ ÿ ÿ ÿÿÿ test_placing_bid.py
fábricas.py
ÿÿÿ
ÿ ÿÿÿ "_init"_.py
requisitos-dev.txt # 6
ÿÿÿ requisitos.txt # 7
ÿÿÿ setup.py # 8

Um código empacotado para Aplicativo e Domínio é mantido em um diretório separado denominado


leilões. Dentro, existe outro diretório com o mesmo nome (1) e "_init"_.py dentro.
Em Python, isso é necessário para poder importar código. Aplicativo (2) e Domínio (3) estão dentro de leilões
(1) contêm dois subdiretórios com código de produção real.

O conjunto de testes (4) é mantido dentro do pacote. Observe que isso não reflete a estrutura dos
diretórios do aplicativo e do domínio. Existe apenas um (5) pacote de aplicativos para mostrar onde procurar
testes de Casos de Uso que afinal são API do pacote. Deve-se evitar espelhar a estrutura nos testes para
evitar acoplamentos desnecessários. Essa abordagem também incentiva práticas como testes unitários de
cada classe/função separadamente, o que pode tornar muito mais difícil a refatoração no futuro.

Os arquivos restantes - 6, 7 e 8 - são outros arquivos específicos do pacote Python. Respectivamente,


eles mantêm informações sobre dependências de pacotes e metadados de pacotes, como versão ou nome.
Machine Translated by Google

Nota para Pythonistas: pode parecer um pouco estranho criarmos arquivos separados para cada
entidade, repositório etc. e não os mantermos juntos em um arquivo - entidades.py ou repositories.py.
Com tão pouco código mostrado até agora, pode parecer desnecessário, mas no longo prazo, manter
as coisas separadas é uma abordagem muito mais limpa. Para encurtar os caminhos de importação em
outros módulos, pode-se importar todas as entidades dos submódulos para domínio/entidades/"_init"_.py

Este é um pacote independente, independente de banco de dados e estrutura, que possui seu próprio conjunto de testes.

Ele será mencionado nos arquivos de requisitos de outros pacotes - ou seja, outros pacotes
dependerão dele.

CÓDIGO DE INFRAESTRUTURA DE EMBALAGEM

Eventualmente, InMemoryAuctionsRepository será usado apenas em testes, então deixaríamos com


Application & Domain. Atualmente, é nossa única implementação, então, para fins de exemplo,
vamos fingir que é uma solução de nível de produção e colocá-la em um pacote separado:

/raiz
ÿÿÿ leilões
ÿ ÿÿÿ… # veja a estrutura de diretórios anterior
ÿÿÿ
leilões_infraestrutura
ÿÿÿ leilões_infraestrutura
ÿ ÿÿÿ "_init"_.py
ÿ ÿÿÿ repositórios
ÿ ÿ ÿÿÿ "_init"_.py
ÿÿÿ
ÿ ÿ in_memory_auctions_repository.py
ÿÿÿ
ÿ configurações.py
ÿÿÿ testes
ÿÿÿ
ÿ repositórios
ÿ ÿÿÿ in_memory_auctions_repository.py
ÿÿÿ requisitos-dev.txt
ÿÿÿ requisitos.txt
ÿÿÿ setup.py

Aí está, um pacote coeso e independente que depende do pacote de leilões apresentado


anteriormente.

PRINCIPAL - COLOCANDO TUDO JUNTO

Existe uma implementação concreta, embora mantendo os dados apenas na memória, em um pacote
(auctions_infrastructure). Em outro pacote (leilões) reside a sua abstração. Já é hora de conectá-
los - sempre que a abstração for solicitada; uma classe concreta deve ser retornada. Em outras
palavras, estamos prestes a configurar o contêiner IoC.
Machine Translated by Google

Ele será colocado em outro pacote chamado apenas main. Cada mecanismo de entrega (web, CLI, fila
de tarefas em segundo plano, etc.) deverá utilizá-lo para montar o projeto.

/
ÿÿÿ leilões
ÿ ÿÿÿ "".
ÿÿÿ leilões_infraestrutura
ÿ ÿÿÿ "".
ÿÿÿ principal

ÿÿÿ principal
ÿÿÿ
ÿ ÿÿÿ "_init"_.py
requisitos-dev.txt
ÿÿÿ requisitos.txt
ÿÿÿ setup.py

A estrutura do main é simples e inicialmente consistirá em apenas um arquivo. Com o crescimento


do projeto, mais arquivos apareceriam, por exemplo, para tratar de diversos aspectos de
configuração. Para começar, main será responsável apenas pela construção do contêiner IoC.

Ele será colocado em outro pacote chamado apenas main. Cada mecanismo de entrega (web, CLI, fila
de tarefas em segundo plano, etc.) deverá utilizá-lo para montar o projeto.

Existem muitas implementações diferentes de contêineres IoC por aí. Alguns são configurados com XML,
outros com YAML. Ainda outro simplesmente usa código. Para fins de exemplo, suponha que o
injetor tenha sido escolhido. Ele foi projetado com base no Guice do Java, portanto deve parecer familiar
mesmo para quem não é Python. Antes de injetarmos tudo em todos os lugares, pare por um momento
e deixe-me lembrar que todo o objetivo do empacotamento é organizar o código, ocultando informações
irrelevantes fora do pacote e expondo apenas as classes que são absolutamente necessárias. Para
que isso funcione com o Injector, fazemos com que cada um de nossos pacotes defina uma classe herdada de
principal.Módulo:
Machine Translated by Google

de leilões importar AuctionsRepository # 3

classe LeilõesInfraestrutura(injector.Module):
@injector.provider def
leilões_repo( self, ) "-

AuctionsRepository: # 2
example_auction = Leilão( id=1,

title="Leilão exemplar",
preço_inicial=Dinheiro(USD, "12,99"), lances=[],

) repo = InMemoryAuctionsRepository() # 1
repo.save(example_auction)
retornar repositório

A infraestrutura fornecerá uma implementação concreta (1) do repositório abstrato (2), portanto deverá importar
a classe abstrata do pacote de leilões (3). No trecho acima, construímos uma instância de
InMemoryAuctionsRepository com um exemplo de entidade de leilão predefinida. Isso ajuda a verificar se
tudo funciona bem após anexar a interface web. Na produção, preferiria não adicionar dados falsos como
esses, mas imagine o quão poderoso pode ser ter arquivos principais dedicados para diferentes ambientes. Por
exemplo, testes automatizados com repositórios na memória ou outro principal personalizado para
desenvolvedores front-end que zomba de dependências problemáticas. Olhando pela perspectiva do nosso
primeiro pacote com código de Aplicativo e Domínio, ele deve expor AuctionsRepository.

leilões de classe (injector.Module):


@injector.provider def
posicionamento_bid_uc( self,
limite:
PlacingBidOutputBoundary, repo:
AuctionsRepository, ) "-
PlacingBidInputBoundary: return
PlacingBid(limite, repo)

O módulo definido no pacote de leilões fornece Casos de Uso usando suas abstrações. Se utilizarmos esta
camada extra de indireção, os leilões também deverão expor todos os limites de entrada. Caso contrário,
expomos todos os Casos de Uso diretamente:
Machine Translated by Google

leilões de classe (injector.Module):


@injector.provider def
colocação_bid_uc(
"".,

) "- PlacenBid: return


PlacenBid(limite, repo)

A palavra “expor” foi usada várias vezes, mas o que realmente significa? Foi mencionado que um
pacote deve ocultar todas as informações que não sejam relevantes para o mundo exterior. Por exemplo,
não há razão para expor nossos Casos de Uso se usarmos Limites de Entrada. Nós apenas expomos o último.
Para visualizar isso, vamos usar Java. Por padrão, as classes dentro do pacote não são visíveis externamente.
Para publicá-los, usamos modificador de acesso público. Porém, não deve ser adicionado de forma
imprudente a todas as classes. Expomos apenas o mínimo possível. É um pouco mais difícil conseguir
um efeito semelhante em Python, mas pode-se sugerir aos clientes de um pacote o que pode e o que não
deve ser importado dele. Cada um dos pacotes Python que mostramos antes tem "_init"_.py de nível
superior. Dentro, importamos tudo o que queremos expor e acrescentamos nomes a uma lista especial dentro -
"_all"_:

"_todos"_ = [#
módulo
"Leilões", #
repositórios
"LeilõesRepositório", # tipos

"Id do Leilão",
# casos de uso
"PlacingBid", # sem limites de entrada
"ColocandoBidInputDto",
"PlacingBidOutputBoundary",
"ColocandoBidOutputDto",
]

Se alguém tentar importar de leilões algo que não esteja dentro de "_all"_, será avisado pelo linter e IDE.

Agora que os módulos Injetores estão definidos, eles podem ser montados dentro do main:

def setup_dependency_injection() "- injetor.Injetor:


injetor de retorno. Injetor (
[Leilões(), AuctionsInfraestrutura()], auto_bind=False,

A instância do Injector retornada pode ser usada para construir qualquer objeto configurado. Por exemplo, se
alguém solicitar o PlacingBid, o contêiner IoC usaria o método Auctions.placing_bid_uc para vê-lo
Machine Translated by Google

também precisa de AuctionsRepository, então ele chamaria AuctionsInfrastructure.auctions_repo


para construí-lo primeiro. O contêiner IoC facilita a criação de gráficos de objetos.

ANEXANDO INTERFACE WEB

Neste ponto, estamos prontos para expor nossa funcionalidade ao mundo exterior por meio da API
HTTP. Minha arma preferida é a microestrutura Flask porque é leve e se integra perfeitamente ao
injetor. Para começar, precisamos de um pacote separado para a web:

/
ÿÿÿ leilões
ÿ ÿÿÿ "".
ÿÿÿ leilões_infraestrutura
ÿ ÿÿÿ "".
ÿÿÿ principal
ÿ ÿÿÿ "".
ÿÿÿ
aplicativo web

ÿÿÿ requisitos-dev.txt
ÿÿÿ requisitos-dev.txt
ÿÿÿ setup.py
ÿÿÿ "".

A estrutura básica do pacote web_app não se destaca, embora a maior parte tenha sido omitida.
Uma árvore de diretórios seria típica para o framework web escolhido. Para o Flask, veríamos projetos,
arquivo de fábrica de aplicativos, etc. O objetivo deste livro não é ensinar a você, caro leitor, como
usar frameworks web, então, por favor, feche os olhos para isso. Este exemplo demonstra como
chamar o Caso de Uso a partir de qualquer mecanismo de entrega, não apenas da web.

web_app também define seu módulo injetor para fornecer todos os limites de saída específicos para a
web. Comecemos pelo princípio, implementação de PlacingBidOutputBoundary - um Presenter:
Machine Translated by Google

classe ColocaçãoBidWebPresenter(
ColocandoBidOutputBoundary
):
resposta: Resposta

definitivamente presente (

self, output_dto: PlacingBidOutputDto ) "- Nenhum:


mensagem =
( f"Viva! Você é
um vencedor"
if output_dto.is_winning else f"Seu
lance é muito baixo. O preço atual é {output_dto.current_price}"

) self.response = make_response(
jsonify({"mensagem": mensagem})
)

Módulo injetor:

classe LeilõesWeb(injector.Module):
@injector.provider
@flask_injector.request # escopo da solicitação
def putting_bid_output_boundary( self, ) "-

PlacecingBidOutputBoundary:
retornar ColocaçãoBidWebPresenter()

Observe que há um novo decorador aplicado - @flask_injector.request. Ele instrui o injetor a usar a
mesma instância durante uma solicitação. Você verá o exemplo abaixo. Ele injetará a mesma instância
no Caso de Uso e também no Controlador, para que possamos compartilhar dados sem quebrar o
encapsulamento de um Caso de Uso.

Aí vem a fábrica de aplicativos Flask, aproveitando o principal:

def create_app() "- Frasco: app =


Frasco("_nome"_)

FlaskInjector(
aplicativo,

módulos=[LeilõesWeb()],
injetor=main.setup_dependency_injection(),
)

aplicativo de devolução
Machine Translated by Google

e uma visualização baseada em Flask:

@auctions_blueprint.route( "/
<int:auction_id>/bids", métodos=["POST"]

) def
place_bid( leilão_id:
AuctionId, colocando_bid_uc:
PlacingBid, apresentador:

PlacingBidOutputBoundary, ) "- Resposta: se não


for current_user.is_authenticated: abort(403)

colocando_bid_uc.execute( get_input_dto( #1
ColocandoBidSchema,

context={ "auction_id": leilão_id, # 2


"bidder_id": current_user.id, # 3
},
)

) retornar apresentador.response # 4

Linhas de código interessantes:

1. há uma função auxiliar get_input_dto em uso que retorna PlacingBidInputDto após


analisando dados de solicitação

2. O ID do leilão é retirado do URL

3. bidder_id é um id do usuário autenticado

4. temos que retornar uma resposta de uma visão devido a certas suposições de design

Observe que a mesma instância do Presenter foi injetada no Controller e no Use Case. Isso aconteceu porque
definimos um escopo de solicitação para o método do provedor
AuctionsWeb.placing_bid_output_boundary . Caso contrário, o injetor criaria uma nova instância a cada injeção
e isso não funcionaria conforme o esperado.

Para obter mais detalhes sobre a camada da web, especialmente aqueles relacionados à desserialização JSON e
30 a maior parte é
nuances de construção de PlacingBidInputDto , consulte o código no GitHub. De qualquer forma,
fortemente específica do Python, portanto, haveria pouco valor agregado em explicar tudo aqui.

30 https://github.com/Enforcer/clean-architecture
Machine Translated by Google

Por outro lado, não há mágica nisso - é apenas um código cola que aproveita a popular biblioteca Python de
serialização/desserialização - marshmallow.

IMPLEMENTANDO CASO DE USO DE ENDINGAUCTION

Os leilões são entidades típicas – criaturas de vida longa que possuem uma identidade. A vida útil de um leilão
começa quando ele é criado. Várias configurações podem ser ajustadas, por exemplo, preço inicial. Um leilão
pode ser publicado imediatamente ou tornar-se uma minuta a ser publicada posteriormente.
Quando um leilão se torna disponível publicamente, os licitantes são incentivados a fazer seus lances.
Tal estado só dura até um determinado momento em que consideramos o leilão encerrado. O encerramento de
um leilão ocorre em um horário especificado quando o leilão foi criado.

O que há de tão especial no caso de uso EndingAuction que decidi dedicar as próximas páginas a ele?
Existem vários motivos:

1. encerrar um leilão envolve finalizar uma transação, incluindo pagamento,

2. não será invocado manualmente a partir de qualquer interface do usuário; deverá acontecer automaticamente
assim que o leilão terminar ou logo após o horário de término chegar,

3. como não há UI envolvida, também não haverá ninguém esperando pelo resultado de
Caso de uso de leilão final . Portanto, não há necessidade de um DTO de saída ou limite de saída,

4. Um leilão pode ser encerrado exatamente uma vez.

Em relação ao ponto 1, nas plataformas existentes que conheço, o vencedor paga em uma etapa separada. O
equivalente na vida real ao nosso EndingAuction limita-se a alterar o status do leilão e notificar todos os licitantes se
eles ganharam ou perderam. Depois disso, o vencedor deve voltar à plataforma, selecionar como deseja que o item
seja entregue, etc. No entanto, para aumentar o valor educacional do exemplo, combinei o pagamento e o
encerramento do leilão em um caso de uso.

Em um caso de uso anterior que implementamos, o banco de dados era nossa única dependência. Desta vez,
precisaremos de um provedor de pagamento. Esse é um tipo de dependência um pouco diferente, pois tal
integração envolverá comunicação pela Internet com um serviço de terceiros não hospedado em nossa infraestrutura
(como foi o caso do banco de dados). Esta parte de um exemplo ponta a ponta tem como objetivo ensinar qual é a
função da Interface/ Porta e do Adaptador/ Adaptador de Interface
na Arquitetura Limpa.
Machine Translated by Google

ESBOÇO DO CASO DE USO COM ENTRADA DTO

Pode-se começar rapidamente a implementar o Caso de Uso com o corpo quase vazio:

@dataclass(frozen=True)
classe EndingAuctionInputDto:
leilão_id: AuctionId

class EndingAuction: def


_init_(
self, leilões_repo: AuctionsRepository) "- Nenhum:

self._auctions_repo = leilões_repo

def executar(
self, input_dto: EndingAuctionInputDto) "- Nenhum:
leilão =
self._auctions_repo.get( input_dto.auction_id

) leilão.end() # "?
pagamento "?
self._auctions_repo.save(leilão)

Observe que neste caso não há absolutamente nenhuma necessidade de um DTO de saída e um limite de saída.
Um administrador poderá ver os resultados do Caso de Uso usando diferentes meios.

ESTENDENDO A ENTIDADE DE LEILÃO PARA CUMPRIR NOVOS REQUISITOS

Com um parágrafo que apresenta o caso de uso EndingAuction, você aprendeu que os lances não podem ser
feitos em um leilão que já terminou e um leilão não pode ser encerrado duas vezes. A entidade leiloeira deve
certificar-se disso. São necessárias algumas etapas: O leilão deve receber um novo campo para a data de
término e o horário de criação de novos lances deve ser verificado em relação a ele. Além disso, um campo
booleano será útil para garantir que um leilão foi encerrado exatamente uma vez.
Machine Translated by Google

As mudanças podem ser introduzidas gradualmente com TDD. Primeiro, um teste com falha para o caso de uso existente:

classe ColocandoBidTests(unittest.TestCase):
def test_bid_on_ended_auction_raises_exception(
self, )
"- Nenhum:
ontem = datetime.now() - timedelta( dias=1

) self.create_auction(ends_at=ontem) preço =
Dinheiro(USD, "10,00") input_dto
= PlacingBidInputDto( bidder_id=1,

leilão_id=self.AUCTION_ID,
quantidade=preço,
)

com self.assertRaises(BidOnEndedAuction):
self.use_case.execute(input_dto)

Este teste está fadado ao fracasso por vários motivos. Em primeiro lugar, o leilão não aceita ends_at

argumento. Em segundo lugar, BidOnEndedAuction ainda não existe. Antes de corrigir isso, você deve ter notado
um pequeno método auxiliar create_auction para criar Leilão:

classe ColocandoBidTests(unittest.TestCase):
def create_auction( self,

ends_at: Opcional[datahora] = Nenhum,


finalizado: Opcional[bool] = Nenhum, )
"- Nenhum:
se ends_at for Nenhum :
ends_at = datetime.now() + timedelta( dias=7

leilão =

Leilão( self.AUCTION_ID,

"socks", Money(USD, "1.99"), [], ends_at,

) self.repo.save(leilão)
Machine Translated by Google

Agora, vamos criar uma exceção de domínio com classe base:

classe DomainException (Exceção):


passar

classe BidOnEndedAuction(DomainException):
passar

Não uso sufixos de erro ou exceção em nomes de classes porque são redundantes. O próprio nome da exceção

(especialmente aquela proveniente do domínio) deve transmitir informações suficientes para identificar

exatamente qual era o problema.

Para finalmente fazer este teste passar, alteramos o código do leilão :

Leilão de classe :
def _init_(
"".

termina_at: datahora,
) "- Nenhum:
"".

self.ends_at = ends_at

def place_bid( self,


bidder_id: BidderId, amount: Money ) "- Nenhum: if
datetime.now()
> self.ends_at:
aumentar BidOnEndedAuction
"".

Observe que mostro apenas o novo código que foi adicionado à entidade.

SE AS ENTIDADES NÃO DEVEM TER DEPENDÊNCIAS, PODEM USAR FUNÇÕES DE TEMPO?

De modo geral, as Entidades devem estar livres de dependências, incluindo o relógio do sistema.

Os puristas certamente gritariam de fúria antes de destruir este livro. No entanto, sejamos pragmáticos. Não é

grande coisa em Python, onde podemos controlar o relógio mais facilmente do que Gaunter O'Dim em Witcher 3.

Tudo o que precisamos é de uma biblioteca freezegun. 31

Se por algum motivo não quisermos/não pudermos usá-lo, sempre podemos passar a data e hora atuais para o método

place_bid , então o Leilão só precisa comparar os carimbos de data e hora. Uma data e hora atual pode ser obtida no

Caso de Uso usando Porta / Adaptador para relógio do sistema. Falando em porta e adaptador…

31 arma congelada https://github.com/spulec/freezegun


Machine Translated by Google

APRESENTANDO A PORTA PARA PAGAMENTOS

Porta é para serviços externos, o mesmo que Interface de Acesso a Dados é para Entidades e sua persistência. Abstrai detalhes,

por exemplo, protocolo de comunicação. O provedor de pagamento está falando JSON e REST? Ou talvez ele entenda apenas XML

enviado por SOAP? Não importa da perspectiva do caso de uso EndingAuction. Necessita apenas de uma interface para efetuar o

pagamento.

Primeiro, um monte de suposições e notas importantes sobre provedores de pagamento populares - especialmente para

aqueles leitores que não trabalham com esses serviços diariamente. Vamos supor que o pagamento seja feito com cartão de

crédito ou débito. O licitante deve inserir os dados do cartão antes de poder fazer lances. Não queremos armazenar estes dados

porque a) é arriscado b) você se tornaria um alvo de ataque c) envolve sérias obrigações legais. O importante é que os detalhes

do cartão de pagamento nem sequer fluam pelo nosso back-end. Esses dados são enviados do frontend para um provedor de

pagamento selecionado. Em troca, recebemos um token que armazenamos.

Figura 6.1 Fluxo de dados durante o salvamento dos detalhes do cartão de pagamento

Sempre que quisermos cobrar o cartão de pagamento, enviamos uma solicitação autorizada ao provedor de pagamento com

esse token.

Figura 6.2 O back-end usa um token sempre que precisa cobrar de um


licitante
Machine Translated by Google

O token está associado a um licitante que ganhou o leilão. Talvez pudéssemos armazenar o token junto
com o licitante. Para fazer isso, temos que escrever Bidder Entity e depois BiddersRepository.
Em seguida, use ambos dentro do caso de uso EndingAuction e passe o token para a porta , abstraindo um
provedor de pagamento. Esta é definitivamente uma opção, mas o token é algo muito específico para
um provedor de pagamento concreto. Não podemos reutilizar o mesmo token em diferentes provedores de
pagamento. Se algum dia mudarmos de provedor de pagamento (isso acontece, acredite) ou adicionarmos um
procedimento de pagamento alternativo, tal acoplamento será prejudicial.

Essas deliberações têm como objetivo descobrir onde deveria estar o limite entre o Caso de Uso e a Porta .
O que proponho é passar a identificação do licitante e o dinheiro a ser pago para o método do Porto .
Então o Adapter (implementação do Port) é responsável por encontrar o token associado. No final, o caso de
uso EndingAuction será mais simples e todas as coisas específicas de um determinado provedor de pagamento
serão bem separadas. Além disso, o Licitante não será poluído com nada que não seja específico dos leilões.

Finalmente, lá vai uma porta PaymentProvider :

classe PaymentProvider(abc.ABC):
@abc.abstractmethod
def pay_for_won_auction( self,

leilão_id: AuctionId,
bidder_id: BidderId,
charge: Money, )
"- Nenhum:
passar

Observe um conforto que temos aqui, graças ao uso do Money Value Object. Podemos ter certeza de que o
argumento do valor é válido e depois de um pouco de amor (quero dizer, conversão) será aceito por
praticamente qualquer API.

TRATAMENTO DE ERROS VS REGRA DE DEPENDÊNCIA

Falhas acontecerão. Seria ingênuo acreditar que nossos Adaptadores sempre teriam sucesso.
Uma classe de exceção real que será lançada depende do Adaptador. Há casos em que gostaríamos de
capturar uma exceção dentro do Caso de Uso e fazer algo com ela. Naturalmente, não podemos nos referir a
classes de exceções específicas do adaptador dentro de um Caso de Uso porque isso violaria a Regra de
Dependência!
Machine Translated by Google

O que podemos fazer em vez disso é definir uma classe (ou hierarquia) de exceções junto com o Port:

classe PagamentoFailed (Exceção):


passar

classe NotEnoughFunds(PagamentoFailed):
passar

Os Casos de Uso podem utilizá-los, pois residirão na mesma camada (Aplicativo). Agora Adaptador
pode gerar exceções que herdam desta classe ou aumentá-la diretamente. Eu recomendo ter pelo menos uma
classe de exceção genérica para cada porta.

Cuidado com o tratamento excessivo de exceções dentro do Caso de Uso! Se você não for capaz de recuperar ou tomar
medidas justificadas pelos requisitos de negócios em um Caso de Uso depois que a exceção foi lançada, nem se
preocupe em capturá-la. Deixe as camadas superiores lidarem com isso ou simplesmente deixe a solicitação falhar.

IMPLEMENTANDO ADAPTADOR

O Adaptador para PaymentProvider será algo muito, muito concreto. Vamos supor que temos um token armazenado
em algum lugar no banco de dados e podemos obtê-lo usando bidder_id passado para o método
pay_for_won_auction . A cobrança real envolve uma solicitação HTTP para a API do provedor de pagamento - endpoint /

api/ v1/ charge com autenticação básica e aplicativo/ json


Cabeçalhos Content-Type e Accept . Os dados a serem passados no corpo da solicitação devem ser JSON.

Exemplo:

{
"cartão_token": "123456",
"moeda": "USD",
"valor": "14,99"
}

Se tudo correr bem, esperamos obter 200 OK junto com o seguinte JSON:

{
"charge_uuid": "67f0e9db-015d-407f-a58f-9c76551dc771",
"sucesso": verdadeiro
}
Machine Translated by Google

Sem mais delongas, vamos ver um código que faz o que precisamos:

classe CaPaymentsPaymentProvider(PaymentProvider):

BASE_URL = "http:"/ca-payments.com/api/v1/"
CHARGE_SUFFIX = "cobrança"

def _init_( self,


login: str, senha: str
) "- Nenhum: # 1
self.auth = (login, senha)

def pay_for_won_auction( self,

leilão_id: AuctionId,
bidder_id: BidderId,
charge: Money, )
"- Nenhum:
card_details_model = BidderCardDetails.objects.filter( # 2
bidder_id=bidder_id .first()

resposta = solicitações.post( #3
self.BASE_URL + self.CHARGE_SUFFIX,
auth=self.auth,

json={ "card_token": card_details_model.card_token, "currency":


charge.currency.iso_code, "amount":
str(charge.amount),
},
)

se não, response.ok:
raise PaymentFailedError # 4
outro:
# registra charge_uuid de pagamento no banco de dados, etc.
"".

Linhas interessantes:

1. "_init"_ é onde o objeto de autenticação é preparado para um formato conveniente para


Biblioteca de solicitações do Python . Tanto o login quanto a senha são alguns tipos de segredos que não
devem ser codificados e variam dependendo do ambiente. main.py deve cuidar de passar os valores
adequados para lá

2. Para recuperar card_token associado a um determinado bidder_id, é necessário acessar o banco de


dados. Você pode se perguntar por que não existe entidade nem repositório para
Machine Translated by Google

Detalhes do BidderCard? Porque não há necessidade. O Adaptador foi concebido para ser concreto.

De qualquer forma, não podemos testá-lo sem simular chamadas HTTP. Tais testes dificilmente teriam qualquer

valor. De qualquer forma, as opções de armazenamento, bem como a estrutura das solicitações e respostas

HTTP, estão fortemente acopladas a essa classe.

3. Uma chamada HTTP é feita usando a excelente biblioteca de solicitações .

Caso algo dê errado, geramos uma exceção definida junto com PaymentProvider
Porta (desde que faça sentido tratá-la em vez de apenas deixar a solicitação falhar).

COMO VIVER QUANDO O ADAPTADOR CRESCE?

Você provavelmente notou que pay_for_won_auction não é a melhor função do mundo. É bastante longo e faz várias

coisas. Dependendo do que é realmente abstraído por um Port, um Adapter correspondente pode crescer com o tempo

e eventualmente se tornar uma dessas classes monstruosas que possuem mais de mil linhas de código e ninguém
quer tocá-las. Na verdade, não existe uma regra que diga para manter tudo dentro da classe Adapter ! Este último foi

feito para ser fino.

Inicialmente, quando um Adaptador implementa um ou dois métodos, um desenvolvedor pode relutar em refatorá-lo
para não cair na armadilha da otimização prematura. Uma vez um adaptador

cresce, será aconselhável recorrer ao padrão de design Fachada .


Machine Translated by Google

Veja esta refatoração na prática:

classe CaPaymentsPaymentProvider(PaymentProvider):
"".

def pay_for_won_auction( self,

leilão_id: AuctionId, bidder_id:


BidderId, charge: Money, )
"- Nenhum:
request =
ChargeRequest( # 1
card_token=dao.get_bidders_card_token(
self._session,
bidder_id ),
#2
moeda=carga.moeda.iso_code,
quantidade=str(carga.quantia),

) resposta = self._execute_request(
solicitação, ChargeResponse)
#3

dao.record_successful_payment(self._session,
leilão_id, bidder_id,
cobrança,

charge_uuid=response.charge_uuid,
)

def _execute_request( self,

request: Request,
response_cls: Type[ResponseCls], ) "-
ResponseCls:
resposta = requests.post( request.url,
auth=self.auth,
json=asdict(request),

) se não, response.ok:
raise PaymentFailedError else:

retornar resposta_cls("*response.json())

Partimos do visual final do Adaptador. Agora parece muito mais genérico.


Machine Translated by Google

Linhas interessantes:

1. Introduzimos classes de dados para solicitações e respostas HTTP. Eles impõem a presença de
parâmetros. Veremos a implementação em breve. A segunda refatoração significou puxar todo
o código que interage com o banco de dados para um módulo separado -
dao.

O que sobrou aqui é a lógica responsável por fazer chamadas HTTP. É genérico e facilmente
extensível com classes de dados de Solicitações e Respostas personalizadas. Poderíamos também ter
separado função/módulo/classe.

Agora vamos ver como essas solicitações e respostas são claras:

Solicitação de classe

@dataclass(frozen=True): url = "http:"/ca-payments.com/api/v1/"


método = "OBTER"

@dataclass(frozen=True) classe
ChargeRequest(Solicitação):
card_token: str
moeda: st
quantidade: str

url = Request.url + "cobrança"


método = "POSTAR"

Essa estrutura permite uma lista declarativa de extensão de endpoints manipulados com um pouco de esforço.
As respostas são muito semelhantes, exceto que não há classe base.

Por fim, dao não é muito interessante, mas mostro-o para completar:

def get_bidders_card_token( sessão:


Sessão, bidder_id: BidderId, ) "- str: card_details_model
=

( session.query(BidderCardDetails) .filter_by(bidder_id=bidder_id) .one()

) retornar card_details_model.card_token
Machine Translated by Google

def record_successful_payment( sessão:


Sessão, leilão_id:
AuctionId, bidder_id:
BidderId, cobrança:
Money,
charge_uuid: str, ) "-
Nenhum:
entrada =

PaymentHistoryEntry( leilão_id=auction_id,
bidder_id=bidder_id, amount=charge.amount, moeda=cobrança. moeda.iso_code, charge_uuid=charge_uuid

) sessão.add(entrada)

Após essa sessão de refatoração, o Adapter se torna apenas uma fachada fina sobre um subsistema
bastante complexo.

LEIA SOMENTE OPERAÇÕES

CASO DE USO - ABORDAGEM BASEADA

Já é hora de abordarmos um assunto delicado - a implementação de operações somente leitura com a


Arquitetura Limpa. É algo que definitivamente poderíamos implementar agora usando os blocos de
construção que vimos até agora - Caso de Uso, DTO de Entrada, Saída e Repositório. Vamos supor que
permitiremos que potenciais licitantes vejam os detalhes do leilão - seu título, preços iniciais e atuais. Para
tornar todo o exemplo mais interessante, podemos adicionar uma lista dos três principais lances com nome
de usuário de licitante anônimo. O anonimato, neste caso, significa que exibiremos a primeira letra de cada
nome de usuário seguida de reticências.

Não seremos preconceituosos. Vamos ver como funciona a primeira ideia. Obtendo detalhes do leilão
O Caso de Uso deve, com certeza, aceitar um leilão_id em seu DTO de entrada. Quanto ao Output
DTO, já sabemos o que se espera.

@dataclass(frozen=True)
classe GettingAuctionDetailsInputDto: leilão_id:
AuctionId

@dataclass(frozen=True)
classe TopBidder:
anonymized_name: str
bid_amount: dinheiro
Machine Translated by Google

@dataclass(frozen=True)
classe GettingAuctionDetailsOutputDto:
leilão_id: AuctionId título:
str
preço_atual: Preço_inicial
em dinheiro: Top_bidders
em dinheiro: Lista[TopBidder]

A tarefa de GettingAuctionDetails é extrair os dados necessários dos repositórios e reempacotá-los para


DTO de saída:

class GettingAuctionDetails: def


_init_( self,

output_boundary: PlacingBidOutputBoundary,
leilões_repo: AuctionsRepository,
bidders_repo: BiddersRepository,
) "- Nenhum:
self._output_boundary = output_boundary
self._auctions_repo = leilões_repo
self._bidders_repo = bidders_repo
Machine Translated by Google

def

execute( self, input_dto: GettingAuctionDetailsInputDto, )


"- Nenhum:
leilão = self._auctions_repo.get( input_dto.auction_id

) top_bids = leilão.bids[-3:]

top_bidders = []
para lance em top_bids:
bidder = self._bidders_repo.get( bid.bidder_id

) nome_anônimo =
( f"{bidder.username[0]}""."

top_bidders.append( TopBidder(nome_anônimo, lance.valor


)
)

output_dto = GettingAuctionDetailsOutputDto(
leilão_id=auction.id,
title=auction.title,
current_price=auction.current_price,
start_price=auction.starting_price,
top_bidders=top_bidders,

) self._output_boundary.present(output_dto)

Acredito que você NÃO gostou desta solução. Espero também que você ainda não queira desistir da
Arquitetura Limpa. O que quero dizer aqui é que, embora a abordagem de Caso de Uso funcione muito bem
para cenários que envolvem dados mutantes, ela se torna um fardo quando tudo o que se deseja é
recuperar algumas informações de uma maneira possivelmente eficiente . O código acima não é apenas
detalhado, mas também ineficiente. Ele sofre de uma falha clássica das consultas ORM n + 1. Felizmente,
ainda temos alguns coelhos na cartola.

CQRS PARA O RESGATE

O padrão CQRS apresentado em um dos capítulos anteriores demonstrou como pode ser benéfico
separar o código responsável pelas gravações das leituras. É o momento certo para aproveitarmos
esse conhecimento e conversarmos um pouco sobre como implementar o lado da leitura no projeto.
Machine Translated by Google

Lembrete rápido: três variantes de como implementar o lado de leitura do CQRS foram descritas neste
livro. Para este exemplo, será utilizado o último (Read Model Facade). Para simplificar as coisas,
suponha que haja um único banco de dados para gravações e leituras.

ABORDAGEM LIBERAL COM LEIA A FACHADA DO MODELO

A interface Read Model Facade será um monte de métodos para cada modelo/entidade. Cada um deles
retornará um objeto de consulta preparado que o cliente pode personalizar adicionando filtros ou
(dependendo da implementação) juntando mais modelos. A implementação real será naturalmente algo
muito específico para o banco de dados subjacente. Aqui está como poderia ser para o Django e o
exemplo acima mencionado com uma lista dos três principais licitantes:

classe AuctionsReadFacade:
def leilões(self) "- models.Manager: return
Auction.objects

def lances(self) "- models.Manager:


retornar Bid.objects

Exemplo de uso:

detalhes de definição (

solicitação: HttpRequest, leilão_id: int


) "- HttpResponse: try:
leilão
=

( AuctionsReadFacade() .auctions() .get(pk=auction_id) ) # 1


exceto ObjectDoesNotExist:
raise
Http404( f"Leilão #auction_id} não existe!"

) lances
=

( AuctionsReadFacade() .bids() .filter(auction_id=auction_id) # 2

.select_relacionado("licitante") .order_by("-quantidade")[:3]
)
Machine Translated by Google

ctx =
Context( {"leilão": leilão, "lances": lances} # 3

) tpl = Modelo ( # 4
"""{% carregar filtros de aplicativos %}"""
"""Leilão: {{ leilão.title }}<br>"""
"""Preço alterado de {{ leilão.starting_price|dollars }}"""
"""para {{ leilão.current_price|dollars }}<br>"""
"""Lances principais":br>"""
"""{% para lance em lances %}"""
"""{{ bid.amount|dollars }} por {{ bid.bidder.username|anonymize }}<br>"""
"""{% endfor %}"""

) retornar HttpResponse(tpl.render(ctx))

AuctionsReadFacade é muito limitado neste caso. Pode fazer pouco sentido com o Django (assim como
a Arquitetura Limpa não funciona bem com esse framework), mas estamos apenas para entender a ideia.
Linhas interessantes:

1. Buscamos o leilão primeiro para poder descobrir rapidamente se ele não está lá e reagir com
HTTP404

2. Observe como esse Queryset é altamente personalizado. Nós filtramos, ingressamos no modelo Bidder, solicitamos
e limitar o conjunto de resultados

3. Os dados inalterados obtidos do Read Model Facade são passados para processamento posterior

4. O modelo no Django é responsável por exibir e formatar a saída. É mostrado


aqui para enfatizar que Read Model Facade não é responsável pela conversão de dados para o formato
desejado.

ABORDAGEM UM POUCO MAIS ESTRUTURADA COM CONSULTA


AULAS

Uma abordagem alternativa que aliviaria a visualização (ou controlador em outras linguagens) da necessidade
de personalizar objetos de consulta brutos do mecanismo de persistência subjacente é usar classes Query.
O último deve conter detalhes e ocultá-los atrás de algum nome descritivo e bonito.
Machine Translated by Google

No exemplo acima, poderia ficar assim:

classe GetAuctionDetails:
@dataclass(frozen=True)
classe Dto: # 1
leilão: Lances de
leilão: Lista[Lance]

def
query( self, leilão_id: AuctionId ) "-
"Dto": leilão =
Auction.objects.get( pk=auction_id ) # 2

lances =

( Bid.objects.filter( leilão_id=auction_id

) .select_relacionado("licitante") .order_by("-quantidade")[:3]

) return self.Dto(leilão, lances)

Linhas interessantes:

1. Os dados são retornados na forma de um DTO para reforçar a estrutura. Uma alternativa é retornar

um ditado simples que será aceito com alegria pelo Contexto do Django. Pode ser benéfico por motivos
de desempenho - significa menos reembalagem de dados.

2. A biblioteca de persistência subjacente é usada diretamente pela classe.


Machine Translated by Google

O uso comparado ao Read Model Facade é significativamente mais simples:

detalhes de definição (

solicitação: HttpRequest, leilão_id: int


) "- HttpResponse: tente:
dto
= GetAuctionDetails().query( leilão_id

)
exceto ObjectDoesNotExist: raise
Http404( f"Leilão
#auction_id} não existe!"
)

ctx = Context(asdict(dto)) tpl =


Template("".) return
HttpResponse(tpl.render(ctx))

Tenha em mente que as classes Query e Read Model Facade fazem parte da interface da nossa aplicação junto
aos Use Cases. Isso significa que se acharmos valioso ter uma classe abstrata (ou interface) para cada caso de uso

- limite de entrada, também pode ajudar abstrair a consulta. Fazer isso com Read Model Facade seria na verdade
muito, muito mais difícil - seria necessário abstrair todo o ORM subjacente, o que não faz absolutamente nenhum
sentido.

Exemplo para GetAuctionDetails:

classe GetAuctionDetails(abc.ABC):
@dataclass(frozen=True) classe
Dto: # 1
leilão: Lances de
leilão: Lista[Lance]

@abstractmethod
consulta def
(self, leilão_id: AuctionId
) "- "Dto": # 2
passar

Linhas interessantes:

1. Dto é um valor de retorno, portanto deve fazer parte da classe de abstração

2. a consulta é deixada como abstrata. A implementação concreta agora pode ser conectada usando
Injeção de dependência. Dessa forma, separa-se a persistência do mecanismo de entrega (se isso
for desejável, é claro).
Machine Translated by Google

As classes Read Model Facade e Query são dois exemplos de abordagens para implementar o lado de leitura.
Escolha o que melhor se adapta às suas necessidades. Se não tiver certeza, comece com classes Query sem
superclasses abstratas.

INVERTENDO O CONTROLE COM EVENTOS

Os casos de uso mostrados até agora controlavam o fluxo apenas diretamente. Simplesmente dizendo, um
Caso de Uso sempre se comportou como se fosse o condutor mais rigoroso e observador – nada poderia acontecer
sem o seu conhecimento.

EXEMPLO - ENVIO DE E-MAILS

Esta abordagem é desejável se você precisa coordenar uma dança de Repositórios, Entidades e Portas em que tudo
tem que acontecer na ordem específica. No entanto, em casos de uso da vida real
terão efeitos secundários que podem ser cruciais do ponto de vista das partes interessadas, mas simplesmente
não se enquadram no nosso actual conjunto de blocos de construção. Por exemplo, enviar e-mails para licitantes
que acabaram de receber lances exagerados. Para realmente enviar um e-mail, deve haver alguma comunicação de
rede com um servidor SMTP. Se você está ansioso para escrever um par Porta/Adaptador, segure seus cavalos
por um momento. Existem algumas perguntas difíceis de responder. Onde pertence o conteúdo do e-mail, sua
aparência, modelo, etc.? Para aplicação? Não, muitos detalhes são concretos. Caso contrário, o Port deveria ser
algo mais genérico, como CommunicationGateway, que também poderia enviar notificações
push ou mensagens SMS para dispositivos móveis? Torna-se óbvio que a abordagem Porta/Adaptador seria
desajeitada. Outra ideia é necessária.

TÉCNICAS DE CONTROLE DE INVERSÃO

O envio de e-mails deve ser dissociado dos Casos de Uso. Nem a comunicação de rede nem o conteúdo dos e-
mails pertencem à camada de Aplicação. A técnica de controle de inversão usada principalmente (porta/
adaptador + injeção de dependência) não se encaixa aqui. Se ao menos houvesse uma maneira de informar todas as
partes interessadas sobre algum evento significativo que aconteceu dentro do leilão... Bem, existe. E é
chamado apenas de Evento. Antes de nos aprofundarmos na implementação, vamos considerar o que Event
realmente é. Simplificando, os Eventos representam fatos – sua única ocorrência significa que algo aconteceu.
Eles não podem ser negados ou rejeitados. Os eventos destinam-se a desacoplar um remetente de qualquer outro
objeto que esteja interessado nas mudanças de estado do remetente.
Este último nem sequer sabe se alguém está ouvindo os acontecimentos. Esta é a principal diferença entre
Porta/Adaptador e Evento/Listener. Na imagem abaixo você pode ver que há uma parte adicional envolvida - Event
Bus, atuando como Mediador. Seu objetivo é dissociar Evento
remetente do ouvinte.
Machine Translated by Google

IMPLEMENTAÇÃO DO EVENTO

Figura 6.3 A emissão de eventos via EventBus desacopla o remetente de um ouvinte. O remetente não sabe quantos
ouvintes estão inscritos ou mesmo se há um

No contexto do exemplo, com o envio de e-mails após um licitante ter sido superado, poderíamos expressar essa

situação com o Evento BidderHasBeenOverbid. Observe o pretérito usado - ele enfatiza o que é Evento - uma informação,

que algo aconteceu. Mais exemplos relacionados a leilões que poderíamos imaginar são BidPlaced, AuctionStarted ou
AuctionEnded. Em termos de implementação, um Evento é simplesmente um Objeto de Transferência de Dados:

@dataclass(frozen=True) classe
BidderHasBeenOverbid:
leilão_id: AuctionId bidder_id:
BidderId new_price: Dinheiro

Agora conhecemos eventos de exemplo; agora a questão é qual bloco de construção é realmente responsável

por enviá-los? Na abordagem Porto/Adaptador, era o Caso de Uso o responsável pela coordenação. No entanto, os

casos de uso podem simplesmente não saber o suficiente quando se trata de emitir eventos. O conhecimento

necessário para a criação de Eventos está disponível em Entidades. Os Casos de Uso precisariam de chamadas extras de

método da Entidade e maneiras potencialmente complexas de redescobrir o que aconteceu para construir um Evento. Ao

fazer isso, correríamos o risco de diminuir a qualidade do encapsulamento das Entidades. Conseqüentemente, os

Eventos pertencem ao círculo mais interno da Arquitetura Limpa - eles fazem parte do Domínio. Conseqüentemente, eles

podem ser (e geralmente são) chamados apenas de Eventos de Domínio.

Os eventos como técnica de dissociação têm um enorme potencial. Por serem estruturas de dados simples, podem

ser serializadas e depois enviadas pela rede para uma aplicação diferente.

Não vamos pular neste pântano neste momento. Por enquanto, suponha que tanto o envio quanto o
Machine Translated by Google

objetos de escuta residem em um processo do sistema operacional. Voltaremos a cenários mais


complexos, prometo.

ONDE POSSO COMPRAR UM DESSES ÔNIBUS PARA EVENTOS?

Que tal ônibus de eventos? No mínimo, esperamos que isso nos permita assinar um determinado tipo de evento
(um único evento pode ter 0 ou mais assinantes) e emitir eventos:

class EventBus:
def emit(self, evento: Evento) "- Nenhum:
"".

def subscribe( self,

event_cls: Type[Event],
listener: Callable,
) "- Nenhum:
"".

Normalmente, o Event Bus deve ser uma dependência do projeto. Soluções de terceiros são mais que
suficientes. Se nós mesmos escrevêssemos um, teríamos que recorrer a uma certa solução alternativa.
EventBus e a classe base Event seriam colocadas em outro pacote, chamado, por exemplo, fundação.
Cuidado, a base não é um lugar para as chamadas classes utilitárias (um conjunto incoerente de funções
onde uma é responsável por eliminar caracteres não-ascii e outra por verificar se uma determinada data
pertence a um determinado intervalo)! Esse “truque” é fazer com que o Event Bus
parte de nossa biblioteca padrão, por assim dizer.
Machine Translated by Google

/raiz
ÿÿÿ leilões
ÿ ÿÿÿ leilões
ÿ ÿ ÿÿÿ aplicativo
ÿÿÿ "".
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

ÿÿÿ testes
ÿÿÿ domínio
ÿÿÿ eventos
ÿ ÿÿÿ bidder_has_been_overbid.py
ÿÿÿ "".
ÿ

ÿÿÿ "".

ÿÿÿ "".
ÿ

ÿÿÿ fundação
ÿÿÿ fundação
ÿ ÿÿÿ "_init"_.py
ÿ ÿÿÿ evento.py
ÿÿÿ
ÿ ÿÿÿ event_bus.py
requisitos.txt
ÿÿÿ setup.py

Não deve ser colocado dentro da estrutura de diretórios da Arquitetura Limpa, pois será, alerta de spoiler,
reutilizado entre diferentes módulos do projeto.

COMO TIRAR EVENTOS DAS ENTIDADES?

Os próprios eventos fazem parte do Domínio, sem dúvida. Event Bus se assemelha a uma porta. Portanto,
esperaríamos que fizesse parte do Aplicativo. No entanto, isso levaria a uma situação paradoxal.
A todo-poderosa Regra de Dependência afirma claramente - A entidade NÃO DEVE usar o Barramento de
Eventos, pois reside acima da camada de Domínio. Apesar de Entity ter que emitir Event e este último deve
ser passado para EventBus.emit.

Entidade mantém Eventos a serem emitidos pelo Repositório

Figura 6.4 Repositório emite eventos obtidos da Entidade via EventBus


Machine Translated by Google

Nesta abordagem a Entidade cria instâncias de eventos, mas como não é permitido usar Event Bus
(ou qualquer outra coisa fora do Domínio) ele os armazena em um campo privado. Mais tarde, quando o Repositório
salva uma Entidade, ela deve coletar todos os eventos pendentes e depois passá-los para o Event Bus.

Leilão de classe:
def _init_("".) "- Nenhum:
"".

self._pending_domain_events: Lista[
Evento
] = [] # 1

def _record_event(self,
evento: Evento) "-
Nenhum: # 2
self._pending_domain_events.append(evento)

@propriedade
def domain_events(self) "- Lista[Evento]: # 3
retornar self._pending_domain_events[:]

def place_bid( self,


bidder_id: BidderId, quantidade: Dinheiro
) "- Nenhum:
"".

self._record_event(#4
BidderHasBeenOverbid(self.id,
old_winner,
quantidade,
self.title,

)
)

Lugares interessantes:

1. Durante a criação do objeto, inicializamos uma lista privada de eventos pendentes

2. Um método privado, útil para anexar eventos à lista

3. O repositório usará este método para obter eventos pendentes. Ele retorna uma cópia de uma lista

4. Sempre que ocorre um Evento de Domínio, nós o registramos.


Machine Translated by Google

Aí vai um exemplo de uso do EventBus em um repositório concreto:

classe SqlAlchemyAuctionsRepo(AuctionsRepository):
def _init_( self,

conexão: Conexão, event_bus:


EventBus, ) "- Nenhum: # 1

self._conn = conexão
self._event_bus = event_bus

def save(self, leilão: Leilão) "- Nenhum:


"".

para evento em leilão.domain_events: # 2


self._event_bus.emit(evento)

Lugares interessantes:

1. Event Bus torna-se uma dependência de um Repositório

2. Ao final do salvamento, emitimos todos os Eventos de Domínio pendentes

Este design é bastante limpo, embora exija colocar a lógica de emissão em um repositório concreto.
Espera-se que a mesma lógica apareça em todos os repositórios e pode ser considerada uma grande
desvantagem desta abordagem. Naturalmente, poderíamos refatorá-lo e aprimorar ainda mais o código. No
entanto, o objetivo desses trechos é apresentar uma ideia: eventos de domínio pendentes são passados para o
barramento de eventos durante o salvamento de uma entidade.

Entidade retorna eventos de métodos que mudam de estado

Vamos lembrar o que é um design de classe CQS (Command-Query Separation). Simplesmente dizendo,
os métodos podem ser categorizados em uma de duas categorias – comandos ou consultas. Os comandos
alteram o estado do objeto e não retornam nada, enquanto as consultas retornam qualquer coisa, mas
são proibidas de alterar o estado da instância. Este é um design que foi escolhido deliberadamente para a nossa
Entidade Leilã. A segunda abordagem se resume a retornar eventos de métodos que são comandos. Embora seja
uma troca, transfere a responsabilidade de colaboração com o Event Bus do Repositório para o Caso de Uso.
Aposto que você admite que isso se adapta muito bem ao caso de uso.
Machine Translated by Google

Em termos de implementação, nossa Entidade deve retornar Eventos no final da execução do comando:

Leilão de classe:
"".

def place_bid( self,


bidder_id: BidderId, amount: Money ) "- Lista[Evento]:

events = [] # uma lista de eventos criados


if self._should_end:
aumentar BidOnEndedAuction

old_winner = self.winners[0] if self.bids else Nenhum


if valor > self.current_price:

self.bids.append( Bid( id=None,


bidder_id=bidder_id, amount=amount,
)

) # "".um primeiro evento é anexado

events.append(WinningBidPlaced(self.id, bidder_id, quantidade, self.title,


)

) if old_winner e old_winner "! bidder_id: # segundo


evento anexado

events.append( BidderHasBeenOverbid( self.id, old_winner, quantidade, self.title,


)
)

# finalmente, a lista de eventos é retornada


eventos de retorno
Machine Translated by Google

Então, a entidade que chama o caso de uso deve coletar eventos e emití-los usando o Event Bus:

classe ColocaçãoBid:
"".

def execute("".) "- Nenhum:


"".

eventos = leilão.place_bid("".)
para evento em eventos:
self._event_bus.emit(evento)

Como este é, pelo menos parcialmente, um livro sobre Python, também podemos tornar tudo um pouco mais
fácil eliminando a lista de eventos em um método. Como? Primeiro, transformamos nosso método de comando
em um gerador:

Leilão de classe:
"".

def place_bid( self,


bidder_id: BidderId, quantidade: Dinheiro
) "- Gerador[Evento, Nenhum, Nenhum]:
"".

se valor> self.current_price:
"".

rendimento WinningBidPlaced(self.id,
bidder_id, quantidade, self.title

) se old_winner e old_winner "! bidder_id: rendimento


BidderHasBeenOverbid( self.id,
old_winner, quantidade, self.title
)

Ainda não podemos parar por aqui - uma palavra-chave yield interrompe a execução de um método. Com uma

combinação de um trecho acima do Caso de Uso ou Repositório, significa que o primeiro Evento pode ser despachado

para os ouvintes, mesmo que ainda não tenhamos finalizado o processamento do comando! Além disso, se nos encontrarmos

numa situação indesejada e quisermos levantar uma exceção entre Eventos, podemos acabar com parte dos

eventos já emitidos. Provavelmente não é isso que queremos que aconteça.


Machine Translated by Google

Para compensar, poderíamos sempre nivelar os eventos gerados usando list ou escrever um decorador que fará isso por
nós:

def command_returning_events(método:
Callable[""., Gerador[Evento, Nenhum, Nenhum]]
) "- Chamável[""., Lista[Evento]]:
@functools.wraps(método) def
wrap(*args: Any, "*kwargs: Any) "- Lista[Evento]:
lista de retorno(método(*args, "*kwargs))

retorno embrulhado

Leilão de classe:
"".

@command_returning_events
def place_bid( self,
bidder_id: BidderId, quantidade: Dinheiro
) "- Gerador[Evento, Nenhum, Nenhum]:
"".

INSCRIÇÃO EM EVENTOS

Sem assinantes, o Evento simplesmente se perderia no vazio. Por enquanto, o melhor local para assinantes se

inscreverem em Eventos futuros já é conhecido por você – principal. O próximo capítulo mostra outro método, mas
por enquanto vamos reutilizar o principal:

def setup() "-Nenhum: # 1


event_bus = EventBus()
setup_dependency_injection()
setup_event_subscribe()

def setup_dependency_injection( event_bus:


EventBus, ) "- Nenhum: def

di_config(binder: inject.Binder) "- Nenhum: binder.bind(EventBus,


event_bus) # 2
"".

injetar.configure(di_config)
Machine Translated by Google

def setup_event_subscriptions( event_bus:


EventBus, ) "- Nenhum:

event_bus.subscribe( # 3
BidderHasBeenOverbid, evento
lambda: send_email.delay(evento.auction_id,
evento.bidder_id,
evento.money.amount,

),
)

Linhas interessantes:

1. O procedimento de configuração foi estendido com a configuração de assinaturas de eventos

2. EventBus deve ser configurado para contêiner de injeção de dependência

3. Neste exemplo, conectamos BidderHasBeenOverbid com uma tarefa implementada usando a tarefa Celery (fila de
32
tarefas distribuídas) que enviará um e-mail informativo

Os eventos são uma técnica de dissociação extremamente poderosa. Quando o controle direto com Portas/

Os adaptadores não parecem adequados ou levam à criação de interfaces de portas com aparência estranha. Os eventos são

sua melhor aposta.

ENTIDADES DE TESTE QUE EMITE EVENTOS

Independentemente da implementação que escolhermos, o que é maravilhoso em fazer Entidades

produzir eventos é que os primeiros permaneçam facilmente testáveis. Por natureza, os Eventos são Objetos de

Transferência de Dados e podem ser vistos como Objetos de Valor - indistinguíveis desde que seus campos tenham
valores iguais.

Se fôssemos testar a entidade que mantém eventos dentro, tudo o que precisamos verificar é comparar o valor da propriedade

domain_events com uma lista de eventos esperados:

32
Aipo: fila de tarefas distribuídas http://www.celeryproject.org/
Machine Translated by Google

Um exemplo um pouco aproximado que dá uma ideia:

def test_auction_upon_overbid_emits_bidder_has_been_overbid_event() "- Nenhum:


leilão = create_auction()
valor_vencedor =
(leilão.preço_atual +
get_dollars("1,00")
)

leilão.place_bid(bidder_id=1,
valor=valor_vencedor
)

afirmar leilão.domain_events "=


[WinningBidPlaced(leilão.id,
1,

valor_vencedor,
leilão.título,
)
]

EVENTOS VS TRANSAÇÕES VS EFEITOS COLATERAIS

Contanto que tudo aconteça dentro de uma transação de banco de dados e a comunicação com serviços
externos pela rede não esteja envolvida, pode-se dormir bem. Lamentavelmente, isso quase nunca acontece.
Mesmo os exemplos simplificados acima mencionados podem falhar de várias maneiras interessantes. Embora
“interessante” provavelmente não seja uma palavra que você usaria ao ser acordado no meio da noite durante
seu turno de plantão. A confiabilidade não é algo que se possa considerar levianamente.

Vamos considerar um cenário em que alguém supera o lance de outro licitante em um determinado leilão:

1. A solicitação HTTP é recebida

2. Uma transação de banco de dados é iniciada

3. O leilão é retirado de um armazenamento usando AuctionsRepository

4. Uma nova licitação é feita

5. O leilão emite um evento

O assinante invoca trabalho em segundo plano para enviar um e-mail

6. O leilão é salvo de volta no banco de dados


Machine Translated by Google

7. A resposta HTTP é construída

8. A transação está comprometida

9. A resposta HTTP é enviada ao cliente.

O que poderia dar errado aqui…?

1. Se reagirmos ao evento logo após a entidade/repositório emiti-lo, estaremos enviando um e-mail antes que
uma transação seja confirmada, o que significa que o overbid ainda poderá falhar. O leilão pareceria
como se um destinatário de e-mail não tivesse recebido um lance exagerado

2. se o trabalho em segundo plano for acionado antes do commit da transação original E for
acessar o banco de dados em busca de dados, pode acontecer que ele não os encontre simplesmente
porque a primeira transação ainda está em andamento e as alterações feitas são invisíveis para outras conexões.
Uma condição de corrida clássica.

Embora estes problemas pareçam ser muito difíceis de resolver à primeira vista, a sua causa é trivial – uso indevido
de Eventos! No contexto do RDBMS transacional, nada realmente aconteceu enquanto a transação ainda estava
em andamento. Isso significa que quaisquer efeitos colaterais causados por eventos não podem ser acionados até
que a transação do banco de dados seja confirmada. Antes disso, os Eventos emitidos são infundados.
A situação fica rapidamente complicada se houver mais bancos de dados ou corretores de mensagens envolvidos.
Bem-vindo ao mundo dos sistemas distribuídos.

Não vamos cair na paranóia, no entanto. Sem uma escala considerável, é pouco provável que problemas como o
primeiro aconteçam. Este não é o caso da 2. situação. Essa condição de corrida pode nos afetar inesperadamente,
mesmo se houver apenas um usuário.

Existe uma saída fácil para ambos os problemas? Há. Muitas bibliotecas de acesso ao banco de dados
implementam a funcionalidade de retorno de chamada para chamar a lógica definida pelo usuário após
a transação ser confirmada. Se combinarmos isso com filas de tarefas (cada assinante de evento agenda uma tarefa
para ser executada após o commit da transação), obteremos o comportamento desejado. Uma abordagem
mais formal é usar o padrão Unidade de Trabalho. No entanto, a solução não é 100% fiável, embora seja
suficientemente boa em certos casos e especialmente numa escala inferior.

APRESENTANDO A UNIDADE DE TRABALHO

Unidade de Trabalho é uma abstração sobre uma chamada transação comercial. Por favor, não confunda isso
com uma transação de banco de dados que possui um escopo mais restrito. Há muitas semelhanças,
Machine Translated by Google

ENVIO DE MENSAGENS CONFIÁVEL: O PADRÃO DE SAÍDA

Os sistemas distribuídos que se comunicam por meio de mensagens enviadas pela rede são feras
complicadas. Mesmo que desconsideremos a natureza falível das redes, as coisas ainda podem
não funcionar como desejamos.

Vamos considerar um cenário em que o microsserviço A envia uma mensagem que deveria
chegar ao microsserviço B. Entre eles existe algum tipo de corretor. Agora, no mundo perfeito, a
mensagem chegará ao microsserviço B e chegará exatamente uma vez. Infelizmente, isso não é algo
que possamos considerar garantido. Depende muito da configuração do nosso corretor e de
como o usamos. Podemos facilmente obter uma de duas garantias de entrega - no máximo uma vez
ou pelo menos uma vez. No primeiro caso, aceitamos o facto de, por vezes, podermos não receber
a mensagem, enquanto no segundo cenário, podemos esperar que a mesma mensagem chegue
esporadicamente várias vezes. Um Santo Graal é uma entrega exatamente única, mas isso
não é trivial. O mundo dos sistemas distribuídos é um lugar fascinante, mas, ao mesmo tempo, está
além do escopo deste livro. O autor recomenda a leitura sobre as garantias Kafka e RabbitMQ e
suas abordagens para confiabilidade de mensagens. Kafka Streams são uma funcionalidade
especialmente interessante.

Agora, em casos mais simples que envolvem apenas o envio de uma mensagem ou o agendamento
de um trabalho em segundo plano APÓS a transação ser confirmada, o Outbox Pattern vem em
socorro. A ideia é muito simples - salvamos mensagens no mesmo banco de dados dentro de uma
transação que as causou. Acabamos com alterações persistentes e mensagens salvas temporariamente.
Em seguida, outro thread, corrotina ou processo designado abre outra transação, pega as
mensagens pendentes e as envia. Em seguida, as mensagens são removidas do banco de
dados e a segunda

porém - a Unidade de Trabalho também agrupa algumas operações para ter sucesso ou falhar como uma só. É apenas
não restrito às consultas ao banco de dados.

33
Originalmente, a Unidade de Trabalho servia apenas para rastrear as alterações feitas nos modelos para
minimizar o número de consultas emitidas. Na verdade, ORMs avançados (como SQLAlchemy) implementam tal
mecanismo. Nossa Unidade de Trabalho será um pouco mais especializada e exporá quatro métodos úteis - começar,
confirmar, reverter e registrar_callback_after_commit. Sob o capô,

33 Martin Fowler, Padrões de Arquitetura de Aplicativos Corporativos, Capítulo 11, Unidade de Trabalho
Machine Translated by Google

Figura 6.5 Diagrama simplificado mostrando o fluxo de controle. send_email é um retorno de chamada agendado devido a um
evento editado durante a vida útil de uma unidade de trabalho atual

chamará métodos apropriados de uma transação de banco de dados e manterá uma lista de retornos de chamada
agendados:

classe UnidadeDeTrabalho(abc.ABC):
@abc.abstractmethod def
start(self) "- Nenhum:
passar

@abc.abstractmethod def
rollback(self) "- Nenhum:
passar

@abc.abstractmethod def
commit(self) "- Nenhum:
passar

@abc.abstractmethod def
register_callback_after_commit( self, retorno de
chamada: digitação.Callable[[], Nenhum]
) "- Nenhum:
passar
Machine Translated by Google

Um exemplo de uso poderia ser assim:

def setup_event_subscriptions( event_bus:


EventBus, ) "- Nenhum:

event_bus.subscribe( BidderHasBeenOverbid,
evento lambda: ( # 1
injetar.instance(#2
UnidadeDeTrabalho

.register_callback_after_commit(#3
lambda: send_email.delay(#4
evento.auction_id,
evento.bidder_id,
evento.money.amount,
)
)
),
)

Lugares interessantes:

1. Após o evento BidderHasBeenOverbid, uma função anônima é chamada (lambda)

2. Durante a execução (ainda na transação), uma instância do UnitOfWork atual é


obtido…

3. …e instruído a executar outra função anônima logo após a transação atual ser
empenhado

4. Callback não aceita nenhum argumento, mas podemos criar um encerramento usando outro
lambda, então ainda temos os dados que precisamos.

Dependendo da biblioteca de acesso ao banco de dados subjacente, uma unidade de trabalho pode ser
apenas um invólucro fino ou fornecer recursos mais avançados, como maior confiabilidade e garantias
sobre a execução de retornos de chamada pós-confirmação.

UNIDADE DE VIDA DE TRABALHO

Juntando tudo - o tempo de vida da Unidade de Trabalho é insignificantemente maior que o tempo de vida de uma
transação de banco de dados no cenário explicado em Eventos versus transações versus efeitos colaterais. No
contexto da web, a Unidade de Trabalho é criada assim que recebemos uma nova solicitação e confirmada
após a conclusão do tratamento da solicitação. Se houver uma exceção não tratada lançada no processo,
o método de reversão da Unidade de Trabalho será chamado. Seu papel é descartar
Machine Translated by Google

quaisquer alterações feitas. Na maioria dos casos, será apenas reverter a transação do banco de dados e eliminar
eventos pendentes.

RELAÇÃO ENTRE UNIDADE DE TRABALHO E ÔNIBUS DE EVENTO

O Event Bus deve estar ciente da existência da Unidade de Trabalho. Mais notavelmente, deve operar no contexto
de uma determinada instância de Unidade de Trabalho. Embora o mapeamento entre Eventos e seus assinantes
deva ser considerado parte da configuração e normalmente não seja alterado quando o aplicativo for iniciado, o
Event Bus agendando retornos de chamada após a transação ser confirmada indica que há necessidade de
estado. A Unidade de Trabalho fornece isso.

Cabe ao desenvolvedor de software decidir se determinado assinante deve ser executado de forma
síncrona (dentro da mesma unidade de trabalho) ou de forma assíncrona, por exemplo, em uma fila de
tarefas em segundo plano (fora da unidade de trabalho atual, após um commit). Uma execução síncrona é
mais simples para Event Bus - ela apenas chama o assinante e é executada logo após a emissão do evento. Para
lidar com comportamento assíncrono, é necessária cooperação. O Event Bus usará o
register_callback_after_commit da Unidade de Trabalho para garantir que o assinante receba o Evento.
Se a introdução da Unidade de Trabalho for indesejada por qualquer motivo, será suficiente habilitarmos o
Event Bus para agendar retornos de chamada após a confirmação da transação de outra maneira.

A parte complicada é garantir que o Event Bus sempre obtenha a instância correta da Unidade de Trabalho.
O IoC Container deve ser capaz de fornecê-lo - um recurso que procuramos é chamado de escopos. Se estamos
falando sobre como lidar com solicitações HTTP, queremos o chamado escopo de solicitação, que existe
apenas enquanto manipula uma única solicitação HTTP. É claro que cada solicitação recebe seu próprio
escopo para ser totalmente isolada das demais. Quando processamos tarefas em um trabalhador, geralmente
queremos ter um escopo por tarefa.

Normalmente, não deve haver problema em usar contêineres IoC maduros da maneira mencionada acima. Deve
haver um plugin para estruturas populares ou a possibilidade de escrever uma quantidade mínima de código
cola. A documentação de um contêiner DI exemplar, Autofac, é muito elaborada sobre sua integração.

LIDAR COM OUTRAS PREOCUPAÇÕES TRANSVERSAIS

Muito espaço foi dedicado à discussão do tratamento de transações. Você pode ter pensado que isso tinha pouco
a ver com a Arquitetura Limpa, mas tinha que ser mencionado, uma vez que as transações são uma das chamadas

preocupações transversais. Simplesmente dizendo, é algo que está presente em todos os lugares do aplicativo e
afeta muitas coisas, ao negligenciar o tratamento das transações, corre-se o risco de inconsistência de dados. Em
certos projectos, isto pode não ser um problema significativo, e é
Machine Translated by Google

seria mais viável apenas ligar para o cliente e pedir desculpas. Em outras empresas, a inconsistência de dados é
um problema sério. Pode exigir a interrupção de todo o sistema, a correção de inconsistências e a
reversão de todos os efeitos colaterais desde que ocorreram. É claro que um bug que causou o desastre ainda precisa
ser corrigido. É como parar um trem escaldante puxado por uma locomotiva a vapor, cheio de mercadorias caras e
com prazo de validade curto e ter que consertar os trilhos onde o trem já está. Ah, esqueci dos passageiros chateados
e das partes interessadas perguntando a cada poucos minutos quanto tempo durará o atraso. Você entendeu.

Nem todas as preocupações transversais são capazes de incomodá-lo tanto, mas menosprezá-las pode resultar em
código incorreto que será difícil de manter posteriormente. Como este é o livro Arquitetura Limpa, vamos
prestar atenção neles.

CONFIGURAÇÃO

Várias configurações, como tokens de acesso a serviços externos ou credenciais ao banco de dados, devem ser
carregadas assim que um aplicativo é iniciado. Há também uma segunda classe de configurações - que são usadas
para parametrizar várias partes do aplicativo, como frequência de tarefas periódicas ou tamanhos de pool.
O terceiro tipo de configurações são as chamadas alternâncias de recursos. Basicamente, esses são
sinalizadores booleanos responsáveis por ativar/desativar os recursos aos quais dizem respeito. Por exemplo, eles
podem ser usados para desabilitar recursos que ainda não estão completos ou que fazem sentido apenas em
determinados ambientes, como desenvolvimento ou produção. Um exemplo é usar um gateway de pagamento de teste
fictício em vez de um gateway real.

Como podemos ver, existem muitas ocasiões para chegar à configuração em todas as aplicações. Uma solução
rápida e suja seria criar uma classe que representasse a configuração como:

classe Configuração (dict):


passar

Em seguida, conectamos isso à injeção de dependência inicializando a instância com configurações.


Sempre que precisarmos de alguma configuração, injetamos Config e usamos como se fosse um dicionário
bruto:

def setup_dependency_injection( configurações:


dict, ) "- Nenhum:
def
di_config(binder: inject.Binder) "- Nenhum: binder.bind(Config,
Config(settings))

injetar.configure(di_config)
Machine Translated by Google

# em algum lugar do código

classe CaPaymentsPaymentProvider(PaymentProvider):
@inject.autoparams() def
_init_(self, config: Config) "- Nenhum: self.auth =

( config["payments.login"],
config["payments.password"],
)

Pelo lado positivo, esta solução é muito rápida de implementar. No lado negativo, não é especialmente
limpo. De repente, dezenas de classes e funções passam a depender do Config. Isso faz com que o Config seja
um código praticamente imutável, já que alterá-lo quebraria demais. Naturalmente, há pouco que possamos fazer
em relação à implementação mostrada acima. Fazer com que muitas classes dependam do Config não é
uma boa ideia. Além disso, não há restrições sobre quais dados estão disponíveis para cada classe. Resumindo,
esse design não é ideal.

O ideal seria conter a consciência da configuração em um só lugar – principal, onde está tudo montado:

def

setup_dependency_injection( configurações: dict, ) "- Nenhum: def di_config(binder: inject.Binder) "- Nenhum:

binder.bind( PagamentoProvider, CaPaymentsPaymentProvider(


configurações["pagamentos.login"],
configurações["pagamentos.senha"],
),
)

injetar.configure(di_config)

No próximo capítulo você também verá uma abordagem mais orientada a objetos para a passagem de
configuração quando nos aprofundarmos na modularidade.

VALIDAÇÃO

A validação é uma das partes mais complicadas da Arquitetura Limpa. Por um lado, existem muitas bibliotecas e
componentes de frameworks web que fazem exatamente isso – validação. Eles funcionam perfeitamente com
aplicativos CRUD. Por outro lado, temos DTOs de entrada sendo passados para Casos de Uso. Os DTOs
de entrada podem não ser o lugar certo para realizar a validação. No entanto, quando chegar ao Caso de
Uso, esperamos que seja correto e seguro para uso.
Machine Translated by Google

Resumindo, o Input DTO fornece:

• No caso de uso, devo poder confiar na correção semântica (por exemplo, tipo) dos atributos

ÿ Recebi um bidder_id igual a 3. Tudo bem; Eu sei que os IDs dos usuários são
inteiros positivos. As instâncias de DTO de entrada não devem existir com atributos semanticamente
incorretos, por exemplo, bidder_id igual a "incorreto".

• Em casos de uso, geralmente ainda não sei se um DTO de entrada semanticamente correto será suficiente para
realizando uma operação comercial

ÿ Normalmente não se tentará verificar se um licitante com determinado bidder_id existe antes de
chamar o caso de uso

Para garantir a correção semântica, a solução definitiva seria aproveitar objetos de valor
e soluções de validação existentes. Em Python, há uma infinidade de bibliotecas excelentes que permitem
34 35
serializar dicionários em objetos, por exemplo marshmallow ou Pydantic, apenas para citar alguns. Para um exemplo
36
completo, consulte o pacote web_app do projeto exemplar.

Em um mundo ideal, as camadas internas (Domínio e Aplicação) dependeriam de Objetos de Valor que, por
design, são imutáveis e protegem sua correção.

SINCRONIZAÇÃO

Se você já participou de algum leilão online, provavelmente sabe como é quando alguém faz um lance maior que
o seu no último segundo. Talvez o vencedor não tenha sido o único que tentou a sorte na esperança de oferecer
um preço melhor no último momento. Esta tarefa é, no entanto, mais adequada para bots. Imagino que
inúmeras vezes os bots competiram entre si no último segundo de um leilão. Tal competição provoca as condições
de corrida mais comuns. A menos que alguém se proteja.

34
marshmallow - serialização simplificada de objetos https://marshmallow.readthedocs.io/en/ stable/

https://pydantic-docs.helpmanual.io/ 35

https://github.com/Enforcer/clean-architecture/blob/master/auctioning_platform/ 36
web_app/web_app/blueprints/auctions.py#L82
Machine Translated by Google

Eu comecei a rastrear condições de corrida sofisticadas, então não pude resistir a colocar um parágrafo sobre como lidar
com elas neste livro. Este é um tema amplo, então decidi mostrar uma técnica que se mostrou extremamente útil na

aplicação da Arquitetura Limpa.

É chamado de bloqueio otimista. Resumindo, atribuímos uma versão a cada Entidade. Quando buscamos um em um
armazenamento de dados, também lembramos qual versão ele tinha. Quando chega a hora de persistir a Entidade,

fazemos isso condicionalmente. A condição verifica se a versão no armazenamento de dados é igual à que
obtivemos quando buscamos a Entidade no início. Se isso acontecer, atualizamos a Entidade e aumentamos a
versão em um. Caso a versão tenha sido alterada (já tenha sido alterada por outra pessoa nesse meio tempo),
falharemos ou tentaremos novamente toda a operação. O problema é que o armazenamento de dados que usamos
precisa suportar essa atualização condicional.

Os detalhes da implementação variam de acordo com o banco de dados usado. Vou apresentar como fazer isso usando
SQL simples:

COMEÇAR; #1

SELECT id, preço_atual, versão do leilão; #2

#faça algumas coisas fora do SQL

ATUALIZAÇÃO #3

leilão
DEFINIR

preço_atual = {novo preço atual},


versão = {versão original} + 1
ONDE
id = {id}
AND versão = {versão original};

COMPROMETER-SE; # 4

Linhas interessantes:

1. Tudo está envolvido em uma transação, embora não nos proteja completamente

2. O leilão é obtido com um número de versão atual

3. Esta declaração mantém nossas alterações, desde que ninguém altere a versão em
enquanto isso

4. Cabe a nós detectar se a instrução UPDATE atualizou alguma linha (#3).


Caso contrário, a transação seria confirmada normalmente, independentemente do resultado de UPDATE.
Machine Translated by Google

Muitos ORMs maduros (incluindo SQLAlchemy37 para Python, Hibernate38 para Java ou Doctrine39
para PHP) tem suporte para esse recurso, então não é preciso pensar tão baixo nível.

A sincronização é mencionada como uma preocupação transversal porque, em muitos casos, a aplicação
não podemos ignorar isso, especialmente quando coordenamos uma dança complexa de Repositórios
e Portos. Afinal, é a implementação da Interface de Acesso a Dados (por exemplo, Repositório) que terá
que gerar a exceção ConcurrentSave.

Em muitos casos, porém, o bloqueio é indesejável, pois afeta o desempenho. Se dois licitantes tentarem
a sorte ao mesmo tempo, um deles terá que tentar novamente e bloqueará os recursos do
servidor por pelo menos o dobro. Uma alternativa é retrabalhar um projeto, para que ele não precise de
nenhum travamento. Se pensarmos em leilões, poderíamos inserir cada novo lance na tabela do banco
de dados. No entanto, não seria mais viável manter o preço atual em um leilão. Tornar-se-ia uma
propriedade virtual que só poderia ser calculada obtendo-se o lance mais alto. Também é muito difícil
detectar quem fez uma oferta exagerada e quando. Escolher uma estratégia de sincronização
é um jogo de compensações. Além disso, depende fortemente do armazenamento de dados
subjacente.

RESUMO DO CAPÍTULO

Este capítulo foi um passeio selvagem com muitas informações novas. Foi apresentada a implementação
de dois casos de uso (PlacingBid e EndingAuction). A Entidade Leiloeira passou por uma evolução
gradual para atender cada vez mais exigências. Todo o código comercial escrito foi coberto com
testes. Foi apresentado o conceito de acesso a dados na forma de repositório abstrato orientado
à persistência. Foi criada a porta para efetuar pagamentos e sua contraparte concreta, o Adaptador. Várias
abordagens para implementar o Read Side foram discutidas. Finalmente, foi demonstrada a
inversão do controle com eventos. No final, foram dadas algumas dicas sobre como lidar com
preocupações transversais.

Leva tempo e é preciso escrever algum código para entender completamente todos esses blocos de
construção. Também ajuda observá-los de uma perspectiva mais ampla de um aplicativo inteiro:

O que é crucial descobrir é onde estão os limites da aplicação. O estado do sistema sofre mutação
usando Casos de Uso que aceitam DTOs de Entrada. Durante a mutação, os eventos podem ser

37
SQLAlchemy https://www.sqlalchemy.org/
38
Hibernar https://hibernate.org/
39
Doutrina https://www.doctrine-project.org/
Machine Translated by Google

Figura 6.6 Dependências entre a maioria dos blocos de construção demonstrados neste capítulo

emitido para informar ao mundo exterior o que aconteceu. Há também uma Read Facade que permite
consultando o sistema sobre seu estado.
Machine Translated by Google

MODULARIDADE

"O desenvolvimento de software é um processo de aprendizagem; o código funcional é um efeito


colateral." -Alberto Brandolini

O ÔNUS DO SUCESSO - CRESCIMENTO E CONTÍNUO


MUDANÇAS

Digamos que lançamos com sucesso nosso aplicativo para produção. Embora a poeira ainda não
tenha baixado, as partes interessadas já estão cheias de novas ideias. Além disso, os primeiros
clientes deram feedback. É inevitável - novas solicitações de recursos estão chegando. Prepare-se
porque este será o teste final para o design que você produziu. É interessante observar quanto de um
código cuidadosamente elaborado ainda estará lá quando iniciarmos um desenvolvimento sério sob
pressão.

COESÃO E MÓDULOS

De onde vêm os requisitos de negócios? A resposta varia dependendo da estrutura da equipe. Se


você está desenvolvendo um projeto favorito, você é seu próprio patrão e a única fonte de visão do
projeto. Uma única equipe Scrum com proprietário do produto proxy está em uma situação um pouco
mais complicada - o último cuida de destilar os requisitos e apresenta uma visão clara aos
desenvolvedores. Mesmo assim, o proprietário do produto muitas vezes tem que lidar com um
recurso nem sempre compatível ou com solicitações de mudança vindas de pessoas diferentes ou,
em maior escala, de departamentos. Sem falar nos usuários que exigem novos recursos que são
cruciais do seu ponto de vista. Isso já deveria soar um sinal - esta é uma situação sobre a qual o
Princípio da Responsabilidade Única alerta - o mesmo código pode ter mais de um motivo para mudar.
Felizmente, já conhecemos a solução: refatorar o código, para que partes individuais não precisem
ser alteradas por diferentes motivos.

Os humanos (em particular os desenvolvedores de software) são ruins quando se trata de nomear
coisas. Mesmo dentro de uma organização, o mesmo termo pode significar algo completamente
diferente para várias pessoas. Considere o item no contexto de uma plataforma de leilões que
também gerencia estoque. Quando um leilão é configurado, um item nada mais é do que um preço
inicial, um monte de imagens, nome, talvez alguns atributos. Do ponto de vista da licitação, o item é irrelevante.
Porém, o item é tudo para o licitante – ele participa do leilão apenas para obtê-lo.
Quando vencem um leilão e o item está prestes a ser enviado, o que mais importa é o tamanho, o
peso ou a localização no armazém. É quase impossível conciliar todos esses diferentes significados
e conjuntos de recursos se tentarmos criar uma única classe. Estaria repleto de muitos atributos
ou métodos que são relevantes apenas em contextos específicos.
Machine Translated by Google

Se é tão difícil e impraticável satisfazer as necessidades de todos ao mesmo tempo, o que mais podemos fazer?
Lembre-se da boa e velha regra de dividir e impera - atender às necessidades separadamente quando faz sentido,
então, em vez de um problema gigante, temos vários problemas muito menores.

CÓDIGO DE EMBALAGEM POR RECURSO

Imagine que as partes interessadas solicitaram uma nova funcionalidade: cada usuário pode optar por algumas áreas de

interesse, por exemplo, tecnologia, livros de fantasia, alimentação saudável ou parentalidade. Os usuários podem ativar e

desativar a qualquer momento.

Esta é a aparência da estrutura atual do projeto:

/raiz
ÿÿÿ leilões
ÿ ÿÿÿ…
ÿÿÿ leilões_infraestrutura
ÿ ÿÿÿ…
ÿÿÿ
aplicativo web

Onde devemos colocar o novo código? Afinal, as áreas de interesse não têm nada em comum com os leilões.

Queremos que nosso módulo principal permaneça coeso, o que significa que ninguém se pergunta “o que diabos

essa coisa está fazendo aqui? O pacote se chama leilões, mas aquela coisa de áreas de interesse
definitivamente não são sobre leilões. Devemos evitar colocar tudo em um pacote, a menos que queiramos acabar
40
com uma assustadora Grande Bola de Lama.

Um dos capítulos de Arquitetura Limpa, O capítulo que falta, é justamente sobre esse assunto – empacotamento
de código. Manter nossos módulos coesos compensa rapidamente, pois eles são muito mais fáceis de entender e
manter. Portanto, é lógico criar um novo pacote para esse recurso – vamos chamá-lo de preferências. Essa abordagem
de empacotamento seria uma materialização da ideia do capítulo que falta – pacote por recurso.

MÓDULOS E FLEXIBILIDADE DE DESIGN DE INTERIORES

Como deve ser o pacote de preferências? Escrevendo casos de uso, entidades, interface de acesso a dados
e sua implementação para algo tão simples é um pouco exagerada. O recurso real não é muito complicado - nada
mais é do que salvar vários valores booleanos provenientes de um único formulário com várias caixas de seleção.

40
Brian Foote, Joseph Yoder, Grande Bola de Lama http://www.laputan.org/mud/
lama.html#BigBallOfMud
Machine Translated by Google

Embora a Arquitetura Limpa seja uma combinação perfeita para projetos que possuem um domínio rico, NÃO É
uma solução única para todos. Sejamos realistas: não faz sentido desenvolver um projeto inteiro utilizando
apenas a Arquitetura Limpa. Às vezes, o bom e velho CRUD serviria. Utilizar a Arquitetura Limpa, assim como
outras técnicas sofisticadas, é um investimento. Faz sentido para as peças mais complexas e valiosas.

Portanto, nada nos impede de implementar certas partes da aplicação utilizando abordagens diferentes e
mais simples. Mesmo sem nenhuma abstração no caminho para o banco de dados.

MÓDULOS VERSUS MICROSERVIÇOS

Todos esses recursos da abordagem modular mencionada acima podem se assemelhar a microsserviços.
Eles têm, na verdade, muito em comum. Muitas qualidades de um bom microsserviço também podem ser usadas
para descrever um módulo bem escrito. Por exemplo, um microsserviço é considerado devidamente desacoplado
do resto do sistema se a introdução de alterações nele não exigir a reconstrução de metade do
aplicativo. Ser capaz de desenvolver um microsserviço sem se preocupar em quebrar outras partes móveis
do sistema indica uma boa dose de desacoplamento e encapsulamento.

Formar limites de microsserviços é algo importante. É virtualmente impossível se a equipe não tiver um forte
entendimento do projeto e de seu domínio. Um erro potencial pode custar muito. No caso de subpacotes, que
residem em uma única base de código, consertar um design errado é mais acessível - geralmente equivale
a mover o código de um pacote para outro. É muito mais difícil com microsserviços, especialmente se eles foram
desenvolvidos em total separação por vários meses ou mais.

A diferença mais óbvia é que não há sobrecarga de comunicação de rede quando um

módulo conversa com outro - diferentemente dos microsserviços. Os microsserviços também exigem muito mais
esforço do (Dev)Ops para mantê-los em funcionamento e em execução do que um único aplicativo monolítico.
Sem mencionar o aumento do custo de implantação e manutenção.

Um bom design modular não impede a divisão em microsserviços. Além disso, permite adiar esta decisão com
consequências de longo alcance até que a consideremos necessária. Por exemplo, um determinado módulo
responsável pela comunicação com o Provedor de Pagamento pode ser movido para um microsserviço para
implementar medidas extras de segurança. Colocá-lo em uma máquina dedicada, limitar o número de pessoas
com acesso a literalmente poucas, manter as credenciais do Provedor de Pagamento somente ali reduziria
definitivamente o número de possíveis vetores de ataque. Outro exemplo pode ser o crescimento da equipe e
os desafios de gerenciamento - por que
Machine Translated by Google

não organizar uma equipe em torno de recursos e permitir que cada um deles possua bases de código
separadamente?

Resumindo, não há absolutamente nenhuma necessidade de migrar para microsserviços se tudo o que é necessário
é ter uma base de código bem organizada e de fácil manutenção. É perfeitamente normal ficar com um
monólito, desde que seja modular. Além disso, existe uma palavra chique para monólito modular - é modulito.
O monólito modular é um meio termo razoável entre uma base de código desorganizada e microsserviços.

MÓDULOS VERSUS USUÁRIO

Você deve ter notado que antes deste capítulo não havia uma única ocorrência do termo usuário. Omiti
propositalmente, pois este é um dos nomes mais vagos e abusados em projetos de software. Definitivamente
não ajuda organizar o código em módulos. No contexto dos leilões, Licitante é o nome certo a usar, pelo
menos quando falamos de uma pessoa que licita. É muito fácil criar nomes para cada módulo. Haverá Assunto
(autenticação), Destinatário (mailing) ou Repórter (suporte). Porém, ao final, será necessário associar o
Assunto à Entidade correspondente de outro módulo. A maneira mais fácil é compartilhar um ID comum,
enquanto o restante dos dados da Entidade depende do módulo ao qual pertence.

Lembre-se de como o PlacingBidInputDto foi criado no pacote da web. bidder_id é um argumento obrigatório
que pode ser obtido no mecanismo de autenticação que está em uso na estrutura da web.

MÓDULOS VS CONTEXTOS LIMITADOS

Toda essa conversa sobre módulos pode (e espero que sim!) soar como uma campainha para leitores
familiarizados com design estratégico orientado a domínio. Para aqueles que não estão familiarizados com
DDD, existe um bloco de construção de modelagem chamado Bounded Context. Delimita uma área de
aplicabilidade de um determinado modelo, garantindo a sua coesão. Significa que deveríamos, em vez
disso, produzir modelos mais especializados. Não há necessidade de se beneficiar de colocar tudo em uma
classe User. Também não precisamos nos preocupar se nosso proponente coeso e especializado não puder
ser usado em outro contexto limitado. É melhor quando não é. A relação entre Contexto Delimitado e módulo é

esse contexto limitado pode abranger um ou mais módulos, mas nunca deve haver vários contextos limitados
em um único módulo. O DDD estratégico está fora do escopo deste livro.
Eu recomendo fortemente que você pelo menos leia sobre isso.
Machine Translated by Google

DESIGN ESTRATÉGICO ORIENTADO POR DOMÍNIO

Se as estratégias mencionadas anteriormente lhe parecem uma dúvida e você prefere usar algo
mais formalizado, tente aplicar padrões estratégicos de Design Orientado a Domínio. Em
particular, leia sobre uma técnica chamada Mapeamento de Contexto.
O artefato resultante é denominado Mapa de Contexto. Este último mostra os contextos limitados
descobertos e as relações entre eles. A abordagem prática para obtê-lo é conduzir sessões de Event
Storming. Porém, há um requisito: as partes interessadas devem ser incluídas no workshop. Os
desenvolvedores de software não serão capazes de apresentar uma visão precisa por conta própria,
ponto final. O último conselho: não trate o Mapa de Contexto como algo imutável. Os requisitos
e o conhecimento de todas as pessoas envolvidas evoluirão com o tempo e, como todo documento
escrito, pode simplesmente se tornar obsoleto em algum momento.

IMPLEMENTAÇÃO DE MÓDULOS

Resumindo, um módulo é um pacote coeso de código com seu próprio vocabulário (em particular, tendo um
nome específico para um usuário). Ele agrupa funcionalidades relacionadas e as expõe usando uma API de
módulo. Um módulo bem projetado se assemelha a um microsserviço, embora não seja necessária comunicação
de rede para usar o módulo.

Os módulos que seguem a Arquitetura Limpa expõem um subconjunto de blocos de construção padrão para
outros usarem:

• Casos de uso

• Consultas (ou outra forma de modelo de leitura)

• Eventos (outros módulos podem assiná-los)

O pacote de leilões já pode ser tratado como um módulo.

Os módulos que não implementam a Arquitetura Limpa ficam livres para utilizar a estrutura que for mais
conveniente. Como se espera que um módulo tenha uma API, pode-se aproveitar o Façade
padrão de design. Uma fachada deve ser usada da mesma maneira que casos de uso ou consultas -
chamada síncrona externa. A fachada do módulo expõe métodos para alterar e consultar o estado do
sistema dentro do módulo. Vamos pegar um módulo que contém
Machine Translated by Google

código responsável pela gestão de relacionamento com o cliente. Mais precisamente, envia
e-mails informativos.

A fachada pode ficar assim:

class CustomerRelationshipFacade: def


create_customer( self,
customer_id: int, email: str
) "- Nenhum:
"".

def update_customer( self,


customer_id: int, email: str
) "- Nenhum:
"".

def send_email_about_overbid( self,

customer_id: int, new_price:


Dinheiro, leilão_title: str,

) "- Nenhum:
"".

def send_email_about_winning( self,

customer_id: int, bid_amount:


Dinheiro, leilão_title: str,

) "- Nenhum:
"".

MÓDULOS DEPENDENDO UNS DOS OUTROS

A parte mais complicada dos módulos é que eles precisam cooperar. É uma situação rara que o
módulo possa existir em completo isolamento sem ter conhecimento de quaisquer outros módulos. No
entanto, dividir a base de código em módulos e descobrir como eles dependem uns dos outros é uma
solução muito mais sustentável do que adicionar mais e mais código a uma única Big Ball of Mud. O
agrupamento de códigos compensa rapidamente.

Desde que existam dois módulos (respectivamente A e B), existem três relações possíveis entre
eles:

1. A e B não se conhecem
Machine Translated by Google

2. A depende de B

3. B depende de A

CAMINHOS SEPARADOS

A situação nº 1 não requer explicações adicionais. A e B existem inconscientes um do outro.

As situações 2 e 3 são muito mais interessantes.

DEPENDÊNCIA DIRETA - AMBOS OS MÓDULOS IMPLEMENTAM A LIMPEZA


ARQUITETURA

Supondo que A e B implementem a Arquitetura Limpa, como A pode chamar o código de B? Quando se
trata de um controle direto e síncrono, normalmente gostaríamos de chamar o Caso de Uso de um
módulo de outro. No entanto, não queremos chamar o Caso de Uso de B diretamente - isso acoplaria A
com B e tornaria quase impossível testar um sem o outro. Felizmente, na Arquitetura Limpa, existe um
bloco de construção que resolve o problema - Limite de Entrada, sendo apenas uma abstração sobre o
Caso de Uso.

Figura 7.1 O caso de uso de um módulo chama o caso de uso de outro módulo via InputBoundary

Esta solução é boa o suficiente se for aceitável para um módulo chamador adotar a nomenclatura do
módulo chamado. Afinal, uma entrada Dto pertencente a B deve ser montada em A. O fator decisivo
aqui é qual módulo é mais significativo do ponto de vista empresarial. Se o módulo chamado (B) for,
então só podemos conciliar o fato de que A precisa saber algo
Machine Translated by Google

sobre a linguagem de B. No caso oposto, quando um vazamento do vocabulário de B (inferior) para A


(superior) é indesejável, ainda há algo que se pode fazer. Em vez disso, o que podemos fazer é escrever uma
porta em A enquanto B fornece um adaptador. O primeiro pertence a A onde será utilizado, portanto não há
risco de vazamento de conhecimento de B para A. Nomes de métodos do Porto/
O adaptador pertencerá a A. Ainda assim, a implementação reembalará os dados apropriadamente para
eventualmente chamar o Caso de Uso de B. Portanto, o par Porta/Adaptador não será complexo - apenas
separa dois mundos.

Figura 7.2 Caso de uso chama porta dentro do mesmo módulo. A porta abstrai o adaptador de outro módulo

No entanto, tenha em mente que a dependência direta é uma forma de acoplamento muito rígida e
deve ser usada com moderação. Faz todo o sentido quando ambos os módulos fazem parte do mesmo
Contexto Delimitado e compartilham um vocabulário.

DEPENDÊNCIA INDIRETA - DUAS INSTÂNCIAS DO LIMPO


ARQUITETURA

No caso de dependência indireta, contamos com eventos. Introduzimos um novo bloco de construção -
Handler - que reagirá ao evento de um módulo e chamará o Caso de Uso apropriado do módulo ao qual
pertence.

Uma dependência indireta separa o módulo assinante daquele que emite eventos. O módulo de assinatura
ainda poderá ser acoplado a outro caso importe algum código (por exemplo, classe de evento para
configuração de assinatura). Para desacoplar totalmente dois módulos, precisamos configurar
Machine Translated by Google

subscriptions no módulo principal , assim como foi mostrado no exemplo anterior. Na maior parte do
tempo, não precisaremos disso.

Figura 7.3 O Handler de um módulo assina eventos de outro módulo. Quando é notificado, ele chama um
caso de uso apropriado

DEPENDÊNCIA QUANDO UM DOS MÓDULOS NÃO ESTÁ


IMPLEMENTANDO A ARQUITETURA LIMPA

No caso de dependência direta, quando o módulo CRUD tiver que utilizar outro que implemente a
Arquitetura Limpa, chamamos o Caso de Uso (opcionalmente via Limite de Entrada correspondente ).

Quando lidamos com uma situação oposta, adicionamos um Port ao módulo Clean Architecture e
implementamos Adapter no módulo CRUD.

Novamente, o uso direto introduz um forte acoplamento entre os módulos. Use-o com cautela. No caso
de dependência indireta, recorre-se a eventos.

SABORES DE INTEGRAÇÃO BASEADA EM EVENTOS

Um dos parágrafos acima menciona que um módulo se inscreve em eventos emitidos por outro. Isso
sugere que o módulo de assinatura ainda precisa contar diretamente com o código do segundo
módulo. Naturalmente, esta é uma forma de acoplamento. Nem sempre é um problema, mas
Machine Translated by Google

quando é, há uma saída. Em ainda outro módulo, são colocados manipuladores que assinam eventos e
chamam a lógica apropriada de outros módulos. Esses manipuladores fazem toda a tradução por conta
própria, fazendo com que os módulos desconheçam uns dos outros. Para um processo de vários estágios
com cascatas de eventos, um padrão chamado Saga ou Process Manager pode ajudar. Gerente de Processos
será discutido e apresentado posteriormente no livro.

DEPENDÊNCIAS ENTRE MÓDULOS - tranqüilidade

Não se preocupe se os exemplos acima parecerem um pouco abstratos neste momento. Desde que
tenhamos algumas opções, podemos hesitar ao escolher qual escolher. Não existe uma abordagem
única e boa - cada uma delas possui características únicas que podem ou não ser adequadas aos nossos módulos.
Aprenderemos isso em breve. Tudo será ilustrado com exemplos nas próximas páginas.

ESTUDO DE CASO - PLATAFORMA DE LEILÃO

DESCUBRA DIFERENTES MÓDULOS

Até agora, os exemplos de código concentraram-se apenas no leilão em si. No entanto, não basta gerir com
sucesso uma plataforma de leilões. Afinal, precisamos receber pagamentos, notificar os licitantes
sobre diversas coisas ou enviar o item ganho para eles. Normalmente, as partes interessadas são a
melhor fonte para este tipo de informação. O conhecimento do domínio pode ser adquirido durante
reuniões ou workshops, como Event Storming.

Se a cooperação direta for limitada ou impossível, pode-se sempre tentar um exercício mental de
mapear diferentes características em diferentes departamentos da empresa em funcionamento. Um
funcionário realiza leilões, enquanto outro aceita pagamentos e faz relatórios financeiros. Ainda outro
funcionário se preocupa em garantir que o item ganho chegue ao vencedor.
Essa forma de modelagem funciona melhor quando sabemos como funciona o negócio.

Também pode ocorrer que um novo módulo surja organicamente - os próprios desenvolvedores os
descobrirão. No nosso caso, descobrimos um novo módulo duas vezes, quando nossas portas e adaptadores
estavam crescendo e crescendo. Em ambos os casos, a princípio Port e Adapter eram bastante simples,
com no máximo três métodos. Quando adicionamos o 8º método a uma porta e o adaptador era então uma
fachada em um subsistema bastante complicado, ficou claro que é hora de refatorar e encontrar um novo lar
para esse código.
Machine Translated by Google

MÓDULOS DA PLATAFORMA DE LEILÃO

Fundação - local para aulas compartilhadas entre diferentes módulos. Foundation serve como uma
extensão lib padrão. Também conhecido como Kernel Compartilhado em DDD. Por exemplo, a classe
Money pertence aqui.

Leilões - tudo relacionado aos leilões em si. Em particular, todas as nuances do leilão são modeladas aqui.

Relacionamento com o Cliente - hospeda detalhes sobre a comunicação com o cliente, como
conteúdos de e-mails.

Pagamentos - aqui reside o código responsável pelo processamento de pagamentos e integração com
fornecedor externo.

Envio - processo de envio de modelos - endereços, status etc.

Processos - casa dos Sagas e Gerentes de Processos. Ele contém processos de negócios complexos que
abrangem vários módulos.

Principal - módulo especial que é o único responsável pela montagem de todo o resto.

Aplicativo Web - uma interface HTTP para a plataforma. Fortemente dependente da estrutura da web
escolhida.

Módulo Arquitetura Termo para usuário

Leilões a Arquitetura Limpa Licitante

Relacionamento com o cliente Baseado em fachada Cliente

Pagamentos Baseado em fachada Cliente

Envio a Arquitetura Limpa Destinatário

Aplicativo Web dependente da estrutura Imposto por uma estrutura

Guia 1 Visão geral dos módulos escolhidos para plataforma de leilões

Isso não é tudo, é claro. No entanto, estes são módulos principais, independentes de linguagem.
Precisaremos de vários auxiliares específicos para a linguagem de programação e bibliotecas de terceiros
que utilizamos. Também poderíamos adicionar mais, por exemplo, gerenciamento de estoque, mas
vamos manter as coisas um pouco mais simples para maximizar o valor educacional.

Você certamente notou que Cliente é utilizado no módulo Relacionamento com Clientes e Pagamentos.
Escusado será dizer que Cliente significa algo totalmente diferente em cada um desses módulos.
Machine Translated by Google

ANATOMIA DE UM MÓDULO - PARTE COMUM

Cada módulo do projeto expõe classes públicas ao mundo exterior.

Os baseados em Clean Architecture irão expor em particular:

• Eventos de Domínio

• Tipos (por exemplo, AuctionId)

• Repositórios (classe base abstrata)

• Limites de entrada (ou casos de uso se o módulo não usar o primeiro)

• DTOs de entrada

• Limites de saída (se os usarmos)

• DTOs de saída (se os usarmos)

• Consultas (se implementarmos a pilha de leitura desta forma)

• Portos

• Módulo de injeção de dependência (ai, a palavra “módulo” está muito sobrecarregada)


Machine Translated by Google

Para o módulo Leilões em Python, será parecido com isto:

"_todos"_ =
[ # módulo
"Leilões",
# eventos
"Leilão encerrado",
"WinningBidPlaced",
"BidderHasBeenOverbid", #
repositórios
“LeilõesRepositório", # tipos

"AuctionId", #
casos de uso
"Colocando lance",
"PlacingBidOutputBoundary",
"RetirandoBids", # input
dtos
"BeginningAuctionInputDto",
"EndingAuctionInputDto",
"ColocandoBidInputDto",
"WithdrawingBidsInputDto", #
consultas
"GetActiveAuctions",
"GetSingleAuction",
]

leilões de classe (injector.Module):


@injector.provider def
colocando_bid_uc( self,
limite:
PlacingBidOutputBoundary, repo:
AuctionsRepository, ) "-
PlacingBid: return
PlacingBid(limite, repo)

@injector.provider def
retirando_bids_uc( self, repo:
AuctionsRepository
) "- WithdrawingBids:
retornar WithdrawingBids(repo)
Machine Translated by Google

Um módulo equivalente com implementações específicas de infraestrutura, chamado


Infraestrutura de Leilões, tem a seguinte aparência:

"_todos"_ = [#
módulo
"AuctionsInfrastructure", # modelos,
necessários para SQLALchemy ORM descobri-los
"leilões",
"licitações",
]

classe LeilõesInfraestrutura(injector.Module):
@injector.provider def
get_active_auctions(
self, conexão: Conexão
) "- GetActiveAuctions: retorna
SqlGetActiveAuctions(conn)

@injector.provider def
get_single_auction(
self, conexão: Conexão
) "- GetSingleAuction: retorna
SqlGetSingleAuction(conn)

@injector.provider def
leilões_repo( self, conn:

Conexão, event_bus:
EventBus, ) "-
AuctionsRepository: return
SqlAlchemyAuctionsRepo( conn, event_bus

Observe que estamos expondo muitas coisas lá fora. Se implementássemos uma pilha de gravação CQRS, poderíamos
expor muito menos. Em vez de exibir casos de uso (ou limites de entrada) e DTOs de entrada, estaríamos expondo
apenas classes de comando (aproximadamente equivalentes a DTOs de entrada).

Um módulo baseado em Façade, Pagamentos, expõe:


• Eventos
• Fachada
• Configuração
DTO • Módulo de injeção de dependência
Machine Translated by Google

"_todos"_ = [#
módulo
"Pagamentos",
"Configuração de Pagamentos",
# fachada
"PagamentosFacade",
# eventos
"Pagamento iniciado",
"Pagamento cobrado",
"Pagamento Capturado",
"Pagamento falhou",
]

classe Pagamentos (injector.Module):


@injector.provider def
fachada( self,
config:
PaymentsConfig, connection:
Connection, event_bus:
EventBus, ) "-
PaymentsFacade: return
PaymentsFacade( config,
connection, event_bus
)

O segundo módulo baseado no Facade, Relacionamento com o Cliente, não deveria nos surpreender. Também
expõe apenas uma fachada:

"_todos"_ = [#
módulo
"Relacionamento com o cliente",
"CustomerRelationshipConfig", #fachada

"CustomerRelationshipFacade",
]

classe CustomerRelationship(injetor.Module):
@injector.provider def
fachada( self,
config:
CustomerRelationshipConfig, connection:
Connection, ) "-
CustomerRelationshipFacade:
return CustomerRelationshipFacade(config,
conexão
)
Machine Translated by Google

Processos é um dos módulos mais interessantes, embora a maior parte do seu código será ignorada por
enquanto. Isso será discutido em detalhes posteriormente neste capítulo. Também exporá o módulo de
injeção de dependência:

"_todos"_ = [#
módulo
"Processos"
]

Processos de classe (injector.Module):


"".

Principal e Fundação são criaturas um pouco diferentes - o papel do primeiro é reunir todos

outros módulos na aplicação, enquanto o último é simplesmente um local para classes ou funções comumente
usadas. Portanto, nenhum deles expõe um módulo de injeção de dependência denominado Main ou Foundation.
Por outro lado, no projeto de exemplo escrito para o livro, Main define diversas classes de módulos auxiliares. A
maioria deles será específica da tecnologia, mas há uma exceção - classe de módulo Configs para fornecer
todas as configurações necessárias para outros módulos.
Tanto o Payments quanto o CustomerRelationship exigem que tal DTO funcione:

configurações de classe (injector.Module):


def _init_(self, configurações: dict) "- Nenhum: self._settings
= configurações

@injector.singleton
@injector.provider def
customer_relationship_config( self, ) "-

CustomerRelationshipConfig:
return

CustomerRelationshipConfig( self._settings["email.host"], int(self._settings["email.port"]), self._settings["emai


self._settings["email.de.nome"],

self._settings[ "email.de.endereço"
],
),
)
Machine Translated by Google

@injector.singleton
@injector.provider def
pagamentos_config(self) "- PaymentsConfig:
return PagamentosConfig(
self._settings["pagamentos.login"],
self._settings["pagamentos.senha"],
)

Além disso, Main pegará as classes do módulo de injeção de dependência e as usará para construir um injetor - nosso
contêiner IoC:

# em algum lugar no Main


def setup_dependency_injection(configurações:
dict,
connection_provider: ConnectionProvider,
) "- injector.Injector: retorno
injector.Injector(
[
Banco de dados (provedor_de_conexão),
RedisMod(),
Rq(),
EventBusMod(),
Configurações(configurações),
Leilões(),
LeilõesInfraestrutura(),
Relacionamento com o cliente(),
Pagamentos(),
Processos(),
],
auto_bind=Falso,
)

Um contêiner IoC resultante é capaz de construir para nós uma árvore de objetos para acessar a
funcionalidade de nossos aplicativos. Por enquanto, permanece ignorante do mecanismo de entrega…

Por último, mas não menos importante, existe um aplicativo Web. A estrutura deste depende, em última análise, da
estrutura da web e da tecnologia escolhida. A única questão importante é escolher o contêiner IoC e a estrutura da
web que funcionem bem juntos, como é o caso do Flask e do injetor. O resultado final que queremos alcançar é
livrar-nos do uso explícito do IoC Container e deixá-lo para as ferramentas. Usamos uma biblioteca Flask-
Injector para fornecer integração:

# durante a construção do aplicativo"".


app = Flask("_nome"_)
# "". registramos o plugin…
main_injector = main.setup_dependency_injection()
FlaskInjector(app,injetor=main_injector)
Machine Translated by Google

# mais tarde no código de visualização

@auctions_blueprint.route("/") def
leilões_list( query:
GetActiveAuctions, ) "- Resposta:
# argumento de
consulta é injetado pelo Flask-Injector
# Não usamos injetor diretamente, uau!
retornar make_response(jsonify(query.query()))

Este exemplo é reduzido ao mínimo porque depende fortemente da escolha das bibliotecas. Um
contêiner IoC maduro deve fornecer dicas sobre como integrá-lo a uma variedade de estruturas da web,
como é o caso do Autofac para projetos C#.

A regra deve ser visível a olho nu. Em essência, um módulo não é apenas um monte de código colocado
em um diretório com nome exclusivo, mas também um cidadão de primeira classe que define um
módulo de injeção de dependência. Posteriormente, o IoC Container (classe Injector em exemplos de
código) usará classes de módulos para resolver classes e manipuladores solicitados. No entanto,
observe que as classes do módulo Injector são algo muito específico para Python e Injector. Um Santo
Graal é tornar o código ignorante sobre DI tanto quanto possível - especialmente no módulo Leilões .

DIFERENÇAS DE ARQUITETURA DOS MÓDULOS - PODEM SER UNIFICADOS?

Pode ser preocupante que módulos diferentes não sigam o mesmo padrão arquitetônico.
Embora a uniformidade seja altamente desejada, deixá-la para trás é uma troca entre módulos
complexos que exigem a Arquitetura Limpa e outros mais simples. Em outras palavras, esta decisão
significa desistir da uniformidade, mas elimina o excesso de engenharia em módulos menos complexos.

É relativamente fácil aceitar que os módulos terão estruturas internas diferentes. Primeiro de tudo, eles
precisam ser especializados para um problema específico que modelam. Em segundo lugar, a
estrutura interna de um módulo é como atributos privados de uma classe – eles não devem ser usados diretamente.
Eles são detalhes de implementação ocultos atrás da API pública de uma classe. Dito isto, se unificar a
estrutura interna não é uma boa ideia, que tal API de módulos? Existe algum meio-termo entre a abordagem
derivada da Arquitetura Limpa com Limites de Entrada + DTOs de Entrada e Fachadas simples?

Na verdade, existe, e já foi mencionado no livro - é uma combinação dos padrões Command,
Command Handler e Command Bus do CQRS. Refatorar a partir da Arquitetura Limpa é bastante fácil -
DTOs de entrada tornam-se Comandos, Limites de Entrada são removidos e Casos de Uso são
registrados como Manipuladores de Comando para que o Barramento de Comando possa despachar
Machine Translated by Google

Comandos. Para Facades também não é complicado - criamos classes Command e as conectamos
com os métodos do Facade ou dividimos o Facade em vários manipuladores de comando.

# PlacingBidInputDto torna-se um comando independente


classe PlaceBidCommand:
bidder_id: BidderId leilão_id:
Valor do AuctionId: Dinheiro

# Caso de uso PlaceBid torna-se PlaceBidHandler


classe PlaceBidHandler: def
execute( self,
comando: PlaceBidCommand
) "- Nenhum:
"".

# módulo registra o manipulador de comandos


leilões de classe (injector.Module):
@injector.provider def
place_bid_handler( self,
limite:
PlacingBidOutputBoundary, repo:
AuctionsRepository, ) "-
Handler[PlaceBidCommand]:
retornar PlaceBidHandler(limite, repositório)

A partir de agora, para utilizar os serviços do módulo Leilões , basta conhecer o


PlaceBidCommand. Depois de instanciar um Comando, usa-se um padrão estilo Barramento de
Comando - Mediador que despachará PlaceBidCommand para o Handler apropriado.

TCommand = TypeVar("TCommand")

# criamos uma classe genérica fictícia


# para encadernações bonitas
manipulador de classe (Genérico[TCommand]):
def _call_(self, comando: TCommand) "- Nenhum:
passar
Machine Translated by Google

class CommandBus:
def _init_( self,
injector: Injector ) "- Nenhum:
self._injector
= injector

def expedição(self, comando: Qualquer) "- Nenhum:


# CommandBus usa injetor para encontrar um manipulador
manipulador =
self._injector.get( Handler[tipo(comando)]

) # apenas para chamá-lo com uma instância de comando


manipulador (comando)

# O comando será uma classe de dados simples


@dataclass(frozen=True)
classe WithdrawBid:
bid_id: BidId

classe WithdrawBidHandler:
def _chamar_(
self, comando: WithdrawBid ) "-
Nenhum: #
manipulador fictício apenas imprime o que obtém
print(f"Manipulando {comando}!")

Leilões de classe (Módulo):


def retirar_bid_handler( self,

) "- Manipulador[RetirarBid]:
retornar WithdrawBidHandler()

# IoC está configurado com Módulo


injetor = Injetor([Leilões()])
# CommandBus é iniciado com instância do injetor
command_bus = CommandBus(injetor)

# e pronto!
command_bus.dispatch(WithdrawBid(bid_id="123")) # imprime
"Tratando WithdrawBid(bid_id='123')!"

Observe uma consequência profunda: o módulo não precisa mais expor Casos de Uso ou seus
Limites de Entrada , como acontecia com PlaceBidHandler. Em outras palavras, encapsulamos
mais, minimizando o acoplamento com outros módulos.
Machine Translated by Google

Essa refatoração é totalmente opcional – ela não será apresentada mais adiante neste livro. Foi
mencionado para esclarecer dúvidas sobre a falta de uniformidade arquitetônica entre os módulos.

DEPENDÊNCIAS ENTRE MÓDULOS

Graças à natureza explícita da Injeção de Dependência, torna-se óbvio como os módulos da plataforma
de leilões dependem uns dos outros.

Figura 7.4 Dependências entre pacotes no projeto de exemplo

Começando de baixo, o Foundation não depende de nenhum outro módulo. Idealmente, usaria apenas
uma biblioteca padrão da linguagem de programação e certas bibliotecas de terceiros que não a
vinculam a um banco de dados, estrutura, etc.

Os Leilões, assim como o Remessa, sendo ambos módulos baseados na Arquitetura Limpa, dependem
apenas da Fundação.

Implementações específicas de infraestrutura Infraestrutura de Leilões e Infraestrutura de


Remessa dependem respectivamente dos módulos Leilões e Remessa. Eles também podem
Machine Translated by Google

dependem da Fundação, principalmente devido a Leilões ou Frete dependendo dela. Além disso, os módulos
de infraestrutura estão vinculados a bibliotecas de terceiros, fornecendo acesso ao banco de dados de sua escolha
etc.

O Relacionamento com o Cliente, em nosso caso, depende do módulo Leilões porque ele reagirá a determinados
eventos de domínio de Leilões. Por exemplo, quando BidderHasBeenOverbid ou WinningBidPlaced
ocorre, o módulo envia e-mails apropriados. O Customer Relationship também usa bibliotecas de terceiros para
acessar o banco de dados.

Os pagamentos não dependem de nenhum outro módulo, mas estão vinculados a bibliotecas de terceiros
para acessar o banco de dados. Ele não assina diretamente nenhum evento. Portanto não depende de Leilões ou
Fretes. Os métodos de sua fachada serão chamados de uma maneira um pouco diferente, que será descrita
posteriormente neste capítulo.

O módulo Processos une todos os outros módulos que participam de cenários de negócios mais
complexos. Dependerá de Leilões, Frete, Relacionamento com o Cliente e Pagamentos para reagir aos seus
eventos ou chamar os métodos/ Casos de Uso do Facade de acordo. Os detalhes da implementação serão
explicados detalhadamente, apenas alguns parágrafos adiante neste capítulo.

Main dependerá de todos os outros módulos, exceto do aplicativo da Web. Embora o módulo principal saiba como
montar o aplicativo, ele permanece ignorante de um mecanismo de entrega, ou seja, web, CLI, fila de tarefas em
segundo plano, etc.

O aplicativo Web dependerá do Main e de qualquer outro módulo que estará conectado à API Web. Neste caso,
são Leilões e Pagamentos.

Lembrete: embora alguns módulos tenham acesso ao banco de dados, eles não compartilham tabelas do banco de
dados! Os modelos definidos em um módulo permanecem privados deste módulo.

NOÇÕES BÁSICAS DE EMITÊNCIA E MANUSEIO DE EVENTOS

Embora a emissão de eventos já tenha sido discutida no capítulo anterior, os fatos mais importantes
devem ser lembrados porque os eventos são a base da integração entre módulos. Portanto, temos que ser muito
mais sérios sobre eles.

Para começar, no módulo baseado em Clean Architecture, os eventos podem ser emitidos do Repositório
(Entidade mantém eventos em campo privado até o momento em que está sendo salvo) ou de Entidade
(então violamos conscientemente a Regra da Dependência). Também não deveria ser um grande problema emitir
eventos dentro do Caso de Uso se isso não fizer sentido dentro de uma Entidade.
Machine Translated by Google

Em módulos implementados usando a abordagem baseada em Facade , emitimos eventos dentro do Facade
métodos:

class PaymentsFacade: def


_init_( self, config:

PaymentsConfig, conexão:
Conexão, event_bus: EventBus,

) "- Nenhum:
"".

captura de definição (
self, payment_uuid: UUID, customer_id: int
) "- Nenhum:
"".

self._event_bus.post( PaymentCaptured( pagamento_uuid, customer_id


)
)

De Eventos versus Transações versus Efeitos Colaterais, já sabemos que até que a transação seja confirmada, os
Eventos são infundados. Uma maneira direta de contornar o problema é executar a lógica de manipulação de
eventos em uma tarefa em segundo plano que será acionada somente após a transação ser confirmada. Este é um
design bastante útil, mas uma versão um pouco generalizada será usada - de agora em diante, tal comportamento será
conhecido como tratamento assíncrono de eventos.

Vamos começar com algumas regras fundamentais:

• Uma classe que emite Eventos permanece totalmente ignorante sobre a forma como eles são tratados

• O assinante decide como lidar com os eventos

ÿ de forma assíncrona - em segundo plano, fora da transação/unidade de trabalho atual


escopo

ÿ de forma síncrona - dentro da transação/unidade de trabalho atual

• Um Event Listener também ignora se é chamado de forma síncrona ou assíncrona

• A assinatura e o envio de eventos são alimentados por injeção de dependência

• O módulo principal fornece classes/funções auxiliares para garantir uma integração suave com um
mecanismo de trabalho em segundo plano e gerenciamento de transações/unidade de trabalho.
Machine Translated by Google

Aproveitar a injeção de dependência para lidar com o envio de eventos pode ser de grande ajuda. Uma abordagem

concreta para implementar isso requer um conhecimento profundo do contêiner IoC escolhido. Temos que ser capazes

de vincular vários manipuladores ao mesmo evento.

Com o Python Injector, isso pode ser alcançado usando uma combinação de multibind e genéricos.

Primeiramente, precisamos de alguns genéricos para manipulação de eventos síncronos e assíncronos:

manipulador de classe (Genérico [T]):


"""Genérico simples usado para associar manipuladores a eventos usando DI. Por exemplo,
Handler[AuctionEnded].
"""

passar

classe AsyncHandler(Genérico[T]):
"""Uma contraparte assíncrona de Handler[Event]."""

passar

Sempre que quisermos vincular um novo manipulador para um determinado Evento, podemos fazer isso de uma forma

elegante dentro da classe do módulo Injeção de Dependência:

binder.multibind( # um
manipulador síncrono para AuctionEnded
Manipulador[AuctionEnded],
para=SYNCHRONOUS_HANDLER,

) binder.multibind( # um
manipulador assíncrono para AuctionEnded
AsyncHandler[AuctionEnded],
to=ASYNCHRONOUS_HANDLER,
)

Um manipulador em si não sabe se é chamado de forma síncrona ou assíncrona. Cabe à ligação determinar quando o

manipulador será acionado. No projeto de exemplo, os manipuladores são classes muito simples para gerenciar

dependências facilmente, por exemplo:


Machine Translated by Google

classe BidderHasBeenOverbidHandler:
@injetor.inject def
_init_(
self, fachada: CustomerRelationshipFacade) "-
Nenhum:
self._facade = fachada

def _call_( self,


evento: BidderHasBeenOverbid) "-
Nenhum:
self._facade.do_something(…)

Devido a certos detalhes de implementação do Injector do Python, os manipuladores devem ser agrupados
com provedores simples. Em palavras mais simples, provedores são a nomenclatura da Injector para
fábricas de manipuladores. A razão pela qual precisamos ter nossa lógica customizada é que os
manipuladores síncronos devem ser construídos usando o Injector e chamados, enquanto os assíncronos não -
isso acontecerá mais tarde, especificamente na fila de tarefas em segundo plano. Os
provedores são respectivamente EventHandlerProvider e AsyncEventHandlerProvider. Exemplo de ligação:

binder.multibind( Handler[AuctionEnded], to=EventHandlerProvider[SomeEventHandler],

)
binder.multibind(AsyncHandler[AuctionEnded],
to=AsyncEventHandlerProvider[
AlgunsEventHandler
],
)

A forma como a vinculação é feita é totalmente transparente para eventos e manipuladores. Pelo lado positivo,
este design é muito flexível e permite modelar tudo da maneira que precisamos.
Machine Translated by Google

Como sempre, os detalhes de implementação são muito específicos de como esse Event Bus específico foi escrito
usando o Injector:

classe InjectorEventBus(EventBus):
"""Um Event Bus simples que aproveita o injetor."""

def _init_( self,


injetor:
Injetor, run_async_handler:
RunAsyncHandler, # 1
) "- Nenhum:
self._injector = injetor
self._run_async_handler =
( run_async_handler
)

def post(self, evento: Evento) "- Nenhum:

tente: manipuladores =

self._injector.get( Handler[type(event)] ) # 2
exceto Requisito Insatisfeito: # 3
passar
outro:
assert isinstance(handlers, list) para
manipulador em manipuladores:
manipulador (evento) # 4

tente: async_handlers =
self._injector.get( AsyncHandler[type(event)]
)
exceto Requisito Insatisfeito:
passar
outro:
afirmar
isinstance( async_handlers, lista

) para async_handler em async_handlers:

self._run_async_handler(async_handler, event) # 5

Linhas interessantes:

1. InjectorEventBus depende de run_async_handler - uma função que irá agendar um


trabalho em segundo plano que chamará determinado manipulador com o evento passado. Isto é fornecido por
Machine Translated by Google

Módulo principal . A implementação depende de um mecanismo de trabalhos em segundo plano e de


gerenciamento de transações. run_async_handler aciona o trabalho somente após a transação ser
confirmada, portanto, é seguro.

2. Graças ao multibind, é possível recuperar uma lista de manipuladores síncronos

3. Pode acontecer que ninguém esteja inscrito em determinado evento, por isso silenciamos a exceção aqui

4. Os manipuladores síncronos são chamados como se fossem funções. Observe que agora as classes
manipuladoras são instanciadas com dependências injetadas. A classe manipuladora deve definir o
método especial "_call"_ para que possa ser chamada como uma função.

Uma solução em uma linguagem de programação diferente deve funcionar de forma análoga. Sempre verifique
a documentação do seu contêiner IoC para encontrar a solução ideal.

TRATAMENTO DE EVENTOS NO MÓDULO

Numa gama limitada de casos, pode ser útil tratar eventos emitidos dentro do mesmo módulo. Veja o módulo de
Pagamentos , por exemplo - uma vez cobrado o cartão de pagamento de um cliente (os fundos são reservados),
precisamos capturá-lo (confirmar o pagamento), o que eventualmente resultará na transferência de fundos para
nossa conta bancária.

Existe a possibilidade de realizar as duas etapas ao mesmo tempo, mas dependendo do gateway de pagamento
pode nem sempre ser confiável. Além disso, existem cenários (embora não na plataforma de leilões) em que
queremos especificamente dividir o pagamento em duas fases. Por exemplo, reservamos fundos, depois cuidamos
das mercadorias usando API de algum fornecedor externo e somente se este tiver sucesso, captamos fundos.

No entanto, a título de exemplo, vamos supor que, uma vez cobrado o cartão de pagamento, se queira capturar os
fundos em segundo plano. A cobrança deve ser online, se possível, porque, em caso de dados incorretos do
cartão, o cliente poderá ver imediatamente se algo está errado. É improvável que a captura falhe, portanto pode ser
executada com segurança em segundo plano.
Machine Translated by Google

A fachada de Pagamentos cobra e emite o evento:

class PaymentsFacade:
def

charge( self,
payment_uuid: UUID,
customer_id:
int, token:
str, ) "- Nenhum: payment =
self._dao.get_payment( payment_uuid, customer_id
)

tente: charge_id =
self._api_consumer.charge( pagamento.valor, token
)
exceto PaymentFailedError:

self._event_bus.post( PaymentFailed( payment_uuid, customer_id


)

) senão:
"".
# código ignorado

self._event_bus.post( PaymentCharged( pagamento_uuid, customer_id


)
)

O manipulador é fino, apenas aciona outro método do Façade - captura:

classe PaymentChargedHandler:
@injector.inject def
_init_(
self, fachada: PaymentsFacade) "-
Nenhum:
self._facade = fachada

def _call_( self,


evento: PaymentCharged) "-
Nenhum:
self._facade.capture(
evento.pagamento_uuid, evento.customer_id
)
Machine Translated by Google

A classe do módulo de injeção de dependência trata da assinatura do evento:

classe Pagamentos (injector.Module):


def configure( self,
binder: injector.Binder ) "- Nenhum:

binder.multibind( AsyncHandler[PaymentCharged],

to=AsyncEventHandlerProvider( PaymentChargedHandler
),
)

Et voilá! Observe que não há nada incomum no manipulador e no Façade que indique que há algum
processo assíncrono em funcionamento. Isto é importante apenas quando um desenvolvedor escreve
a ligação.

TRATAMENTO DE EVENTOS DE MÓDULOS CRUZADOS - CASOS SIMPLES

O tratamento de eventos provenientes de módulos diferentes não é realmente diferente do tratamento


deles dentro do mesmo módulo. Um exemplo simples é enviar um e-mail quando um licitante tiver
uma oferta exagerada. A lógica de lances está no módulo Leilões , especialmente dentro da Entidade
Leilão . É daí que um evento é emitido:

Leilão de classe :
def place_bid( self,
bidder_id: BidderId, amount: Money ) "- Nenhum:
old_winner
= ( self.winners[0]
if self.bids else Nenhum

) se valor > self.current_price:


"".

if old_winner:

self._record_event( BidderHasBeenOverbid( self.id, old_winner, quantidade, self.title,


)
)
Machine Translated by Google

Toda a comunicação com o cliente pertence ao módulo Relacionamento com o Cliente onde é definido um
manipulador apropriado:

classe BidderHasBeenOverbidHandler:
@injector.inject
def _init_( self,
fachada: CustomerRelationshipFacade ) "- Nenhum:
self._facade
= fachada

def _call_( self,


evento: BidderHasBeenOverbid ) "-
Nenhum:
self._facade.send_email_about_overbid( event.bidder_id,
event.new_price,
event.auction_title,

Tudo o que faz é chamar um método de Façade . Os manipuladores dentro dos módulos não serão nada
mais complexos do que um simples código cola.

Quanto à assinatura, não há realmente nada de novo:

classe CustomerRelationship(injetor.Module):
def configure( self,
binder: injector.Binder ) "- Nenhum:

binder.multibind( AsyncHandler[BidderHasBeenOverbid],
to=AsyncEventHandlerProvider(
LicitanteHasBeenOverbidHandler
),
)

Embora esta forma de integração de módulos seja simples, não é uma solução ideal para todos os casos.
Para assinar um evento proveniente do módulo Leilões é necessário importá-lo para o Relacionamento
com o Cliente. Isso faz com que esses dois estejam acoplados. Isto não é muito problemático
aqui, porque isto é apenas uma ilustração da natureza do Relacionamento com o Cliente –
destina-se a informar os clientes sobre muitas coisas diferentes que aconteceram dentro do sistema.
Inevitavelmente, este módulo terá que conhecer muitos outros.

Porém, tome cuidado ao modelar fluxos de negócios abrangendo vários módulos dessa maneira.
Quando o módulo A se inscreve em um evento do módulo B, e este último se inscreve em um evento
do módulo C, imagine como é agradável ler um código tão disperso.
Machine Translated by Google

É extremamente difícil entender tal processo quando um desenvolvedor tem que passar por diferentes módulos
e recriar todo o fluxo em suas cabeças. Felizmente, existe um padrão apropriado para esta situação – Process
Manager.

TRATAMENTO DE EVENTOS DE MÓDULOS CRUZADOS - CASOS COMPLEXOS

A divisão da base de código em módulos aumenta a distância entre os fragmentos de código responsáveis por
cenários de negócios complexos. Isoladamente, nenhum dos métodos de Casos de Uso ou Fachadas é
difícil. A única peça que falta aqui é algum padrão que torne os processos que abrangem vários
módulos explícitos e fáceis de compreender.

Existem duas abordagens possíveis - Saga ou Process Manager.

Em primeiro lugar, tanto o Saga quanto o Process Manager assinam eventos provenientes de módulos diferentes.
Eles assumem a coordenação e eliminam a necessidade de dependências entre módulos.

A principal diferença entre esses padrões é que o Saga não tem estado, enquanto o Process Manager
é estatal. Assim, o Process Manager mantém o estado interno para saber como reagir. Esse recurso tem muito
41 . Por outro lado, a Saga não guarda nenhum dado. Pode exigir módulos
em comum com o padrão de design State
para fornecer recursos de consulta adicionais. No livro, veremos um estudo de caso de um Gestor de Processos.

Vamos considerar o seguinte cenário de negócios:

• assim que o leilão terminar:

ÿ um e-mail é enviado ao vencedor

ÿ um novo pagamento é exigido do vencedor

• assim que o pagamento for cobrado:

ÿ outro e-mail é enviado ao vencedor

ÿ o item está preparado para envio

• assim que o item for enviado:

ÿ mais um e-mail é enviado ao vencedor

41
Bert Bates et al, Head First Design Patterns, Capítulo 10. O padrão de estado: o estado das coisas
Machine Translated by Google

Primeiro, vamos ver os eventos envolvidos no processo:

• Leilão encerrado

• Pagamento Capturado

• Pacote enviado

Observe que cada um deles é emitido por um módulo diferente. Um trecho de código que dá uma ideia sobre a implementação:

classe PayingForWonItem:
@method_dispatch
def handle(self,
evento:
Qualquer,
dados: PayingForWonItemData,
) "- Nenhum:
raise Exception( f"Evento
não tratado {event}"
)

@handle.register(AuctionEnded) # 1
def handle_auction_ended( self,
evento:
AuctionEnded, dados:
PayingForWonItemData, ) "- Nenhum:

afirmar que data.state é Nenhum


self._payments.start_new_payment("".)
self._customer_relationship.send_email_about_winning(
"".

@handle.register(PaymentCaptured) # 2
def handle_payment_captured(self, evento:

PaymentCaptured, dados:
PayingForWonItemData,) "- Nenhum:
assert
(data.state

"= Estado.PAYMENT_STARTED

) self._customer_relationship.send_email_after_successful_payment(
"".

) self._shipping.register_new_package("".)
Machine Translated by Google

@handle.register(PackageShipped) # 3
def handle_package_shipped(self,
evento:
PackageShipped, dados:
PayingForWonItemData,) "- Nenhum:
assert
(data.state

"= Estado.SHIPPING_STARTED

) self._customer_relationship.send_email_after_shipping(
"".

Linhas interessantes:

1. Ao final do Leilão chamamos Fachadas de Pagamentos e Relacionamento com o Cliente

2. Após Pagamento Capturado chamamos Fachadas de Relacionamento com o Cliente e Envio

3. Após o Pacote Enviado chamamos de Fachada de Relacionamento com o Cliente

Cada manipulador inspeciona o estado interno do Process Manager antes de executar qualquer lógica. Dessa forma,
um Gerenciador de Processos impõe a correção exatamente como a Máquina de Estado faz. Essas verificações
também trazem idempotência – estamos seguros de reagir aos mesmos eventos mais de uma vez. Isso pode
não parecer grande coisa em um aplicativo modular, mas ainda monolítico, mas é um recurso altamente desejado
em sistemas distribuídos.

Embora PayingForWonItem seja um exemplo trivial, seu estado não é uma variável única (por exemplo
Estado.PAYMENT_STARTED ou Estado.SHIPPING_STARTED). Um Gerente de Processo deve manter
informações suficientes para tomar decisões de forma autônoma usando dados limitados incluídos em novos Eventos.
Na prática, o Process Manager copia todos os dados necessários dos Eventos. Em outras palavras, os
manipuladores alteram o estado do Process Manager, que pode ser uma estrutura de dados bastante complexa:

@dataclass
classe PayingForWonItemData:
process_uuid: uuid.UUID estado:
Opcional[Estado] = Nenhum
vencedor_bid: Opcional[Dinheiro] = Nenhum
leilão_title: Opcional[str] = Nenhum
leilão_id: Opcional[int] = Nenhum
vencedor_id: Opcional[int] = Nenhum

Para obter um exemplo de mutação de estado, consulte a segunda metade do manipulador de AuctionEnded:
Machine Translated by Google

class PayingForWonItem:
@handle.register(AuctionEnded) def
handle_auction_ended( self, evento:

AuctionEnded, dados:
PayingForWonItemData, ) "- Nenhum:

"".

data.state = State.PAYMENT_STARTED
data.auction_title = event.auction_title data.winning_bid =
event.winning_bid data.auction_id = event.auction_id
data.winner_id = event.winner_id

Posteriormente, passaremos esses dados para Fachadas ou Casos de Uso apropriados, por exemplo:

class PayingForWonItem:
@handle.register(PaymentCaptured) def
handle_payment_captured( self, event:

PaymentCaptured, data:
PayingForWonItemData, ) "- Nenhum:

"".

self._customer_relationship.send_email_after_successful_payment(
dados.winner_id,
dados.winner_bid,
dados.auction_title,
)

Uma alternativa para manter todos os dados no estado é usar consultas apropriadas para buscar
informações extras. Então, o estado será mantido no mínimo. Obviamente, isso é impossível quando nosso
sistema é eventualmente consistente, porque as consultas não retornarão necessariamente as
informações mais recentes.

A statefulness dos Gestores de Processos permite implementar cenários mais complexos, como repetir
pagamentos até N vezes - é necessário registar as informações necessárias no estado interno. Em segundo
lugar, uma aplicação muito comum do Process Manager é o tratamento de tempos limite. Digamos que um
vencedor tenha um dia para pagar. Caso contrário, teremos que enviar-lhes um e-mail lembrando. As
possibilidades são infinitas. Do ponto de vista da implementação, é necessário adicionar um campo timeout_at ao estad
estrutura:

@dataclass
classe PayingForWonItemData:
"".

timeout_at: Opcional[datetime] = Nenhum


Machine Translated by Google

Outra etapa é implementar um método de timeout no próprio Process Manager :

classe PayingForWonItem:
def
timeout( self, data: PayingForWonItemData )
"- Nenhum:
self._customer_relationship.send_payment_reminder( self._state.winner_id,
self._state.auction_title,

) self._data.state = (
Estado.PAYMENT_OVERDUE_A_DAY
)

Com esta implementação ainda é necessário invocar o Process Manager de alguma forma de fora.

Uma abordagem seria consultar periodicamente o banco de dados em busca de Gerenciadores de Processos que ficam

sem tempo e depois chamar seu método de tempo limite . Uma solução mais sofisticada envolve o uso de algum

serviço de agendamento externo que invocará nosso código em um momento específico no futuro.

GERENTE DE PROCESSO VERSUS PERSISTÊNCIA

Naturalmente, um Process Manager não fica na memória do programa o tempo todo. Assim como uma Entidade, ela é

buscada no banco de dados quando necessário e salva posteriormente. Portanto, requer um mecanismo de persistência.

Existem dois problemas a serem resolvidos:

• como manter PayingForWonItemData em nosso banco de dados?

• como encontrar os dados desejados se houver vários gerenciadores de processos do mesmo tipo

em andamento?

Uma solução suficientemente boa para o primeiro dilema é manter a estrutura de dados do estado como JSON. Essa

abordagem é enxuta, mas tem uma desvantagem: ela funcionará somente se a estrutura de dados do Process Manager

for serializável e desserializável. Isso deve ser deixado para bibliotecas de terceiros, se possível. Para Java, existe um

excelente Jackson ObjectMapper que é capaz de lidar com POJOs. Afinal, é possível escrever um repositório único e

genérico para todos os Gerenciadores de Processos. Terá que ser capaz de traduzir DTOs em JSON para posteriormente

serem armazenados no banco de dados. Por exemplo, o seguinte objeto:


Machine Translated by Google

ExemploDados(process_uuid=UUID( "9fc15305-2a0f-41ed-8c1c-eafa2416ee75"
),
name="Exemplo",
contador=0,
timeout_at=datetime.datetime(
2019, 9, 29, 20, 20, 11, 426930
),
)

...pode ser serializado no seguinte JSON:

{
"nome": "Exemplo",
"contador": 0,
"timeout_at": "2019-09-29T20:20:11.426930"
}

...para ser posteriormente inserido na tabela PostgreSQL criada com:

CRIAR TABELA process_manager_data (


uuid CHAVE PRIMÁRIA UUID,
dados jsonb NÃO NULOS
);

Agora, existem duas abordagens possíveis para manter timeout_at. Pode-se mantê-lo dentro dos dados, mas será
necessário um índice JSON parcial para um desempenho aceitável ou criar outra coluna apenas para o tempo
limite. Então, usá-lo deve ser muito mais simples. A propósito, o Gerenciador de Processos
os dados pertencem claramente a uma ampla categoria de documentos, o que torna os bancos de dados de
documentos (por exemplo, MongoDB) realmente úteis aqui. Se for aquele que você já possui em seu projeto,
manter os dados dos gerentes de processos dentro dele não deverá causar muita dor de cabeça.

Um segundo dilema (como encontrar os dados desejados se houver vários Gerenciadores de Processos
do mesmo tipo em andamento?) é um pouco mais complexo. O que ainda não foi mencionado é que entre Eventos
e um Gerenciador de Processos existe um Process Manager Handler. Esta última é uma classe que será
responsável por criar uma instância do Process Manager se ele apenas manipular um Evento inicial ou buscá-lo
usando o repositório.
Machine Translated by Google

class PayingForWonItemHandler:
@injector.inject def
_init_( self,

process_manager: PayingForWonItem, repo:


ProcessManagerDataRepo, ) "- Nenhum:

self._process_manager = process_manager self._repo


= repositório

@method_dispatch
def _call_(self, evento: Evento) "- Nenhum: # 1
aumentar NotImplementedError

@_call_.register(AuctionEnded) def
handle_beginning( self,
event: AuctionEnded ) "- Nenhum:
#2
dados =
PayingForWonItemData(process_uuid=uuid.uuid4()

) self._run_process_manager(dados, evento)

@_call_.register(PaymentCaptured) def
handle_payment_captured(
self, evento: PaymentCaptured ) "-
Nenhum: # 3
dados =

self._repo.get(evento.payment_uuid, PayingForWonItemData,

) self._run_process_manager(dados, evento)

def _run_process_manager(dados:
PayingForWonItemData, evento:
Evento,) "-
Nenhum: # 4
self._process_manager.handle(evento, dados)
self._repo.save(data.process_uuid, dados)

Linhas interessantes:

1. Método manipulador abrangente a ser invocado em caso de evento não tratado.

2. AuctionEnded é o primeiro evento na vida de cada PayingForWonItem. Daí, sempre


cria uma nova instância de PayingForWonItemData
Machine Translated by Google

3. PaymentCaptured sempre acontece após a criação de um Process Manager, portanto

PayingForWonItem é carregado do repositório

4. _run_process_manager cuida das partes repetíveis do tratamento de eventos

Agora podemos resolver o segundo dilema.

Existem (pelo menos) duas saídas:

• garantir que todos os eventos retornarão com o mesmo UUID que será usado apenas para a chave primária - uuid

• escrever manipuladores e repositórios de forma que seja possível usar diferentes UUIDs e consultas

process_manager_data por vários campos aninhados.

A primeira solução é simples de implementar e resulta em um código mais simples, mas afetará os módulos
envolvidos. Na prática:

# Manipulador do Gerenciador de Processos


@_call_.register(AuctionEnded) def
handle_beginning( self,
event:
AuctionEnded, data:
PayingForWonItemData, ) "- Nenhum:
data =

PayingForWonItemData( process_uuid=uuid.uuid4() ) # 1

# Gerente de Processos
@handle.register(AuctionEnded) def
handle_auction_ended( self,
evento:
AuctionEnded, dados:
PayingForWonItemData, ) "- Nenhum:

self._payments.start_new_payment( self._data.process_uuid,
"".
)#2
Machine Translated by Google

# Manipulador do Gerenciador de Processos


@_call_.register(Pagamento
Capturado

) def handle_payment_captured( self,

evento: PaymentCaptured,
dados: PayingForWonItemData, ) "-
Nenhum:
data =

self._repo.get( event.payment_uuid, PayingForWonItemData, ) # 3

Linhas interessantes:

1. O primeiro evento é tratado - AuctionEnded. Basta gerar um novo UUID

2. Um gerente de processo passa seu UUID para ser usado como UUID de pagamento recém-criado

3. Quando PaymentCaptured é emitido, pode-se ter certeza de que é o mesmo UUID que Process
Gerente tem

Um efeito colateral dessa abordagem é que será obtido um ID de correlação - o mesmo UUID será passado e
reutilizado em módulos diferentes, facilitando o rastreamento do que realmente está acontecendo.
Por outro lado, esta solução nem sempre é possível de implementar, especialmente quando é necessário
rastrear vários objetos do mesmo tipo. Não é possível reutilizar o mesmo UUID.

A segunda abordagem não possui requisitos específicos, mas é um pouco mais complexa.
No entanto, esta complexidade extra está contida no Process Manager Handler:

classe PayingForWonItemHandler:
@_call_.register(AuctionEnded) def
handle_beginning( self,
event: AuctionEnded ) "- Nenhum:
#1
dados =
PayingForWonItemData(process_uuid=uuid.uuid4()

) self._run_process_manager(dados, evento)
Machine Translated by Google

@_call_.register(PaymentCaptured) def
handle_payment_captured(
self, evento: PaymentCaptured) "-
Nenhum:
data = self._repo.get_by_field( #2
PayingForWonItemData,
pagamento_uuid=event.payment_uuid,

) self._run_process_manager(dados, evento)

Linhas interessantes:

1. Não há alterações no primeiro método

2. Um método handle_payment_captured é onde as coisas ficam interessantes. O repositório agora tem um


novo método - get_by_field - que aceita um argumento de palavra-chave arbitrária para usá-lo em uma
consulta executada em um banco de dados

Observe que essas soluções funcionam apenas se houver exatamente um evento que possa iniciar o Process
Manager. Se houver mais, é preciso encontrar outra forma de garantir a continuidade de um Gestor de Processos.

GERENTE DE PROCESSO VERSUS CONDIÇÕES DE CORRIDA

Os gerentes de processos são criaturas com estado. Eles têm seu estado persistido entre as invocações.
Os eventos que eles tratam podem ser emitidos ao mesmo tempo. Pense em uma pessoa pagando no último
momento, quando passa uma data e hora especificada em timeout_at. Em outras palavras, Gerentes de Processo
são vulneráveis às condições de corrida e é preciso protegê-los.

No capítulo anterior, foi utilizada uma técnica chamada bloqueio otimista. O método consiste em verificar se alguém
alterou a Entidade desde a última vez que a buscamos. Nesse caso, será necessário repetir toda a operação.
Funciona muito bem com uma entidade sem efeitos colaterais, mas pode não ser a melhor opção para gerentes
de processos. O bloqueio pessimista seria melhor.

Nos seus princípios, é ainda mais simples do que a solução anterior. Antes de chamar qualquer lógica de
manipulação de um Gerenciador de Processos, um bloqueio deve ser adquirido explicitamente. Ao finalizar
o processamento, o bloqueio é liberado. Se não conseguirmos adquiri-lo, abortaremos.
Às vezes, também podemos querer esperar algum tempo até que o bloqueio seja liberado ou simplesmente tentar
novamente mais tarde. Isso é uma teoria, mas para fins de exemplo, vamos supor que se o bloqueio não puder
ser adquirido, podemos abortar. Considere o exemplo exagerado mencionado no início da seção - um
vencedor pagando no último momento quando um Gerente de Processo
o tempo acabou. Vamos supor que quando o tempo limite ocorrer, não queremos mais o dinheiro do vencedor.
Eles estão atrasados, fim da história. Em outras palavras, quando dois Eventos que podem encerrar o Processo
Machine Translated by Google

Manager estão competindo, não faz sentido tentar novamente ou esperar. Em outros casos, como ao contar
as conquistas de alguém, fará sentido tentar novamente ou esperar.

Em termos de implementação, o Process Manager Handler pode ser sobrecarregado com a lógica de aquisição de
bloqueio:

classe PayingForWonItemHandler:
LOCK_TIMEOUT = 30

@injector.inject def
_init_( self,

*omitido_args,
lock_factory: LockFactory ) "-
Nenhum: # 1
"".

self._lock_factory=lock_factory

def _run_process_manager (self,

lock_name: str,
dados: PayingForWonItemData,
evento: Evento,)
"- Nenhum:
lock = self._lock_factory (lock_name,
self.LOCK_TIMEOUT) # 2

com fechadura: #3
self._process_manager.handle(evento, dados)
self._repo.save(data.process_uuid, dados)

Linhas interessantes:

1. Uma implementação concreta de Lock é injetada no Process Manager Handler

2. É uma boa ideia sempre adquirir bloqueios com algum tempo limite, após o qual eles serão liberados
automaticamente se algo der errado. Isso melhora a resiliência do sistema

3. O gerenciador de contexto Pythonic abstrai de nós a aquisição e liberação do bloqueio

Para completar o exemplo, Redis será usado para bloqueio. Um exemplo de implementação de um gerenciador
de contexto de bloqueio pode ser o seguinte:
Machine Translated by Google

classe RedisLock (Bloquear):


LOCK_VALUE = "BLOQUEADO" #1

def _init_( self,


redis:
StrictRedis, nome: str,
timeout: int
= 30, ) "- Nenhum:
self._redis =
redis self._lock_name =
nome self._timeout = timeout

def _enter_(self) "- Nenhum: # 2


se não for

self._redis.set(self._lock_name, self.LOCK_VALUE, nx=True, ex=self._timeout,


):
aumentar já bloqueado

def _sair_(#3
self,
exc_type: Opcional[Type[BaseException]], exc_val:
Opcional[BaseException], exc_tb:
Opcional[TracebackType], ) "- bool: if
exc_type "!
Já bloqueado:
self._redis.delete(self._lock_name)
retorna falso

Linhas interessantes:

1. Para fazer o bloqueio no Redis usando o tipo String, é necessário colocar algo dentro. É bastante
irrelevante. Para alguns fins de depuração, pode-se querer colocar o carimbo de data/hora lá

2. O método _enter_ é executado antes de tratar a lógica de um Gerenciador de Processos. É


equivalente a executar SET {lock_name} “LOCKED” NX EX 30 em um console Redis

_exit_ é executado após o bloco abaixo com extremidades. Seu objetivo é liberar o bloqueio somente se o adquirirmos.

RESUMO DO CAPÍTULO

Este capítulo explicou uma parte vital da arquitetura de um sistema: empacotar o código por recurso ou
fatiamento vertical. Aprendemos que nem todo módulo precisa seguir a Arquitetura Limpa se
Machine Translated by Google

apenas um se beneficiaria com isso. Em casos mais simples, podemos recorrer a módulos baseados em Facade
que interagem com bases de dados ou serviços externos de forma mais direta, sem camadas adicionais de
abstrações.

Em seguida, foram discutidos diferentes tipos de relações entre módulos para mostrar formas de
integração. Dentre as técnicas apresentadas, estavam:

• chamadas diretas de um módulo de outro,

• definir uma porta em um módulo e permitir que outro módulo forneça um adaptador,

• integrações usando eventos de domínio

ÿ simples, quando um módulo assina os eventos de outro módulo

ÿ complexo, quando o padrão Process Manager foi introduzido para orquestrar processos
abrangendo vários módulos

A forma preferida de integrar módulos deve ser usando Eventos de Domínio , a menos que seja necessária uma
chamada síncrona.
Machine Translated by Google

TESTE

ESTRATÉGIA DE TESTE E SABORES DE RECURSOS

No início de qualquer projeto, entregar valor ao negócio sem causar regressão é algo trivial. No
entanto, com a adição de mais e mais recursos, a complexidade aumenta. O mesmo acontece com o
risco de romper alguma coisa. A adesão a práticas de bom design de OOP, como Princípio Aberto-
Fechado ou acoplamento fraco diminui o perigo no caso de adição de novos recursos, por isso ainda não
dá confiança suficiente. É preciso mais do que uma coincidência para entregar regularmente. Felizmente,
a solução já é conhecida: testes automatizados. Aqui está o que Adrian Sutton diz sobre confiar no
42
conjunto de testes da LMAX:

Obviamente investimos muito tempo na construção de todos esses testes, mas eles são
inestimáveis na forma como nos libertam para tentar coisas ousadas e fazer mudanças
radicais, confiantes de que se algo quebrar, será detectado. Estamos felizes em reestruturar
radicalmente os componentes (…).

Para que nosso conjunto de testes forneça uma rede de segurança completa, a arquitetura deve facilitar os testes.
Caso contrário, nos encontraremos hackeando o monstro não testável. A Arquitetura Limpa é
definitivamente uma aliada poderosa quando se trata de escrever código testável. Até o momento, o foco
estava em testar peças menores, como Entidades ou Casos de Uso. Já é hora de vermos o panorama
geral e pensarmos em testar módulos inteiros, bem como um aplicativo inteiro.

A PIRÂMIDE DE TESTE – UM MITO OU A ÚNICA COISA CERTA A FAZER?

Provavelmente você já ouviu falar sobre o conceito de distribuição de testes supostamente perfeita - A
Pirâmide de Automação de Testes. Este conceito é atribuído a Mike Cohn e foi publicado pela primeira
vez de forma escrita em 43
2009:

A Pirâmide de Automação de Testes original se esforça para maximizar o retorno do investimento usando
diferentes tipos de testes. Testes unitários rápidos e baratos não são suficientes para ter certeza de que

42
Adrian Sutton, Fazendo testes ponta a ponta funcionarem https://www.symphonious.net/2015/04/30/
making-end-to-end-tests-work/

43 Mike Cohn, Sucesso com Agile: Desenvolvimento de Software Usando Scrum


Machine Translated by Google

A Figura 8.1 A Pirâmide de Automação de Testes assume que os testes unitários são os
mais numerosos de todos. Eles são complementados com menos testes de serviço e significativamente
menos testes de UI. A premissa fundamental da Pirâmide de Automação de Testes é que os testes
unitários são consideravelmente mais rápidos e baratos que outros.

programa funciona quando um usuário interage com ele, então eles são complementados com uma quantidade menor

de testes de serviço de nível superior e ainda menos testes executados na interface do usuário. Observe que este modelo não leva em

consideração os testes manuais - afinal, é a Pirâmide de Automação de Testes. No entanto, os testes manuais ainda são necessários – especialmente

os testes exploratórios. Um modelo de Pirâmide de Teste que leva isso em consideração é assim:

Figura 8.2 Pirâmide de Teste com teste manual incluído


Machine Translated by Google

Por outro lado, um antipadrão quando a maior parte do esforço de teste é feito manualmente é chamado
de casquinha de sorvete:

Figura 8.3 Antipadrão de casquinha de sorvete

TAXONOMIA DE TIPOS DE TESTES

A Pirâmide de Automação de Testes de Mike Cohn consiste em três tipos de testes - unidade, serviço e
UI.

Os testes de unidade e a IU devem parecer familiares, mas o que são testes de serviço misteriosos?
De acordo com 44
o autor :

Da forma como estou usando, um serviço é algo que o aplicativo faz em resposta a alguma entrada ou
conjunto de entradas. Nosso exemplo de calculadora envolve dois serviços: multiplicar e dividir.
O teste de nível de serviço consiste em testar os serviços de um aplicativo separadamente de sua interface
de usuário. Portanto, em vez de executar cerca de uma dúzia de casos de teste de multiplicação
por meio da interface de usuário da calculadora, realizamos esses testes no nível de serviço.

44 Mike Cohn, A camada esquecida da pirâmide de automação de


testes , https://www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid
Machine Translated by Google

Se aplicássemos isso à Arquitetura Limpa, nossos Casos de Uso (e Consultas) seriam nossos Serviços. A
Pirâmide de Automação de Testes pode ser definitivamente elogiada por sua consistência - todos os tipos
de testes envolvidos referem-se a determinados níveis em um aplicativo testado, seja uma interface de
usuário (UI), casos de uso / consultas (serviço) ou classes/funções (unidade).

Outros nomes comumente usados para tipos de testes, dependendo do seu nível, são testes de sistema,
testes ponta a ponta ou testes de integração. Embora seja geralmente aceito quais testes de sistema
e testes ponta a ponta significam (basta passar por todas as camadas), os testes de integração são
definidos de maneiras diferentes, muitas vezes contraditórias. Por exemplo, o45glossário do ISTQB diz que
é um nível de teste que se concentra nas interações entre componentes ou sistemas. Outra definição
assume que é uma forma de testar algumas unidades juntas como um todo maior – um componente. Por
que não chamá-lo de teste de componentes, então? Oh, espere - esse termo já existe. De
qualquer forma - a última interpretação conhecida é escrever testes com escopo igual aos testes unitários,
mas não fazer stub do banco de dados e ainda chamá-los de testes de integração. Vamos supor que
neste livro a partir de agora os testes de integração signifiquem verificar a exatidão do código
responsável pela comunicação com um sistema ou provedor externo. Em termos de Arquitetura Limpa
pense em um par de Porta e Adaptador para um provedor de pagamento. Os testes do Adaptador que
requerem comunicação com o sistema externo seriam chamados de testes de integração. Eles não se
destinam a testar o próprio provedor externo - apenas o adaptador.

Vou usar ainda outro tipo - testes de API. Eles são colocados um pouco acima dos testes de serviço e da
estrutura da web de exercícios usada. Uma característica de tais testes

estaria chamando um URL específico e fazendo afirmações sobre a resposta.

Há ainda outra tentativa de classificar testes que usa sua função e não como deveriam ser executados.
Esses tipos são testes funcionais e testes de aceitação. A diferença entre testes funcionais e
de aceitação é sutil e algumas pessoas tratam abertamente como se fossem idênticos. O Glossário ISTQB
afirma que o teste funcional visa avaliar se um sistema satisfaz os requisitos funcionais, enquanto o
teste de aceitação visa determinar se o sistema deve ser aceito. De uma certa perspectiva, são realmente
iguais - ambos os tipos de teste verificam se o sistema funciona do ponto de vista comercial. A única diferença
de significado que consegui encontrar é que os testes de aceitação se concentram em cenários da vida real,
enquanto os testes funcionais
também pode exercitar o sistema de uma forma mais completa.

De agora em diante, vou me ater ao nome de teste de aceitação quando me referir à verificação se o sistema
faz o que deveria fazer do ponto de vista das partes interessadas e dos usuários.

45 Glossário ISTQB https://glossary.istqb.org/


Machine Translated by Google

Distribuições de teste como na Pirâmide de Automação de Teste original são muito desejáveis, mas
raramente vistas. A razão é dupla: uma: o código não é testável, portanto não permite testes unitários de
qualidade e dois: os benefícios do teste unitário do código para resolver um problema específico são
insignificantes. Quanto à testabilidade do código, já sabemos a resposta - refatore seu código para que ele
possa aproveitar a Arquitetura Limpa ou pelo menos algumas técnicas de aumento de testabilidade, como
inversão de controle. A segunda parte é muito mais interessante. Você leu certo - em algumas situações, os
testes unitários não valeriam o esforço. Um sintoma típico é que todos os casos de teste podem ser cobertos
por um conjunto muito limitado de testes de nível superior. Na melhor das hipóteses, seríamos capazes
de duplicar apenas algumas verificações com testes unitários, mas não trazer nada de novo para a mesa,
mesmo com código hipertestável.

Para ilustrar melhor o problema, vamos ver através das lentes de uma interface de usuário e três
recursos com sabores diferentes:

1. um navegador de banco de dados - fazendo alterações diretas nos dados armazenados em um banco de dados, repetíveis

resultados,

2. proxy para outros sistemas – fazendo muitas chamadas externas e fazendo muito pouco com os resultados
além de armazená-los ou apenas apresentá-los,

3. um sistema “profundo” – muitas pré-condições envolvidas, numerosos cenários alternativos para


lidar.

Cada um desses exemplos é uma extremidade, mas essas são todas as características possíveis dos
sabores. Uma aplicação da vida real será uma combinação de tudo. No entanto, o sabor predominante
determinará a estratégia de teste mais apropriada.

Para concluir, a Pirâmide de Automação de Testes original não é uma solução mágica. Pode ser necessário
adotar uma abordagem diferente, mais adequada ao seu projeto. Observe que com uma aplicação
modularizada, podemos aplicar uma estratégia de teste personalizada para cada módulo. Isso permanecerá
verdadeiro se, eventualmente, decidirmos nos dividir em microsserviços. Não há razão para impor a única
abordagem correta ao longo de todo o projeto, especialmente tendo em conta que não existe uma
solução única para todos.

Agora vamos examinar detalhadamente cada tipo de recurso.


Machine Translated by Google

COMO TESTAR UM NAVEGADOR DE BANCO DE DADOS?

Esses sistemas são às vezes chamados de CRUDs. Todos os requisitos de negócios são interpretados como

operações de banco de dados - Criar (INSERT), Ler (SELECT), Atualizar (UPDATE) e Excluir (DELETE). Vamos

considerar um exemplo.

Um gerente de projeto diz:

Vamos construir um sistema para nossas Pizza Fridays. A cada semana, todos os participantes pedirão uma pizza específica

inserindo seu nome no cardápio em um formulário. Haverá uma pessoa chamada coordenador que será responsável por fazer uma ligação

para um restaurante ou fazer um pedido online. Assim que o pedido for feito no restaurante, o coordenador limpará a lista

interna de pedidos.

O desenvolvedor de software interpreta isso como:

Participante adiciona pedido à lista interna - INSERT uma linha no banco de dados com o nome da pizza

Coordenador exibe conteúdo da lista interna - SELECIONE todas as linhas do banco de dados

O coordenador esvazia a lista após o pedido - DELETE linhas, como alternativa, use UPDATE para exclusão reversível

Observe que é a abordagem mais simples possível. Para que tal aplicativo fosse usado além de um único escritório, seria necessário

considerar muitos outros cenários, como atualização do pedido do participante, retirada da Pizza Friday, etc. O problema é que o

CRUD deixaria de funcionar neste exemplo.

CRUD “puro” é um caso extremo quando existe exatamente um caminho possível para uma determinada interação. Não há

cenários alternativos. Fazer a mesma ação repetidamente não tem consequências (ou é limitada). Se alguém aplicasse a Arquitetura

Limpa para tal problema e esta fosse sua primeira experiência com ela, eles pensariam que é uma completa perda de tempo e uma

bobagem exagerada.

Pense em um fluxo padrão:

1. reempacotar os dados da solicitação no DTO de entrada

2. passe para o caso de uso

1. Caso de uso na primeira linha obtém Entidade

2. no segundo, o Caso de Uso chama o método da Entidade


Machine Translated by Google

3. A entidade é salva na terceira linha.

E o que o método de uma Entidade faz? Define alguns campos. Sem instruções if, sem ramificações lógicas, sem

casos especiais. Ah, e é tentador nomear Caso de Uso como Atualização[Nome da Entidade]

já que se pensa através das lentes das linhas do banco de dados. Claro, é possível escrever um teste unitário para Entidade

ou Caso de Uso em tal situação. A forma singular foi usada propositalmente - afinal, haveria apenas um cenário para verificar.

Não há valor agregado em tais testes - se também testarmos em um nível superior (por exemplo, via API), os testes unitários

verificariam exatamente os mesmos caminhos de código e nos dariam menos confiança, uma vez que exercitam menos

código de uma só vez. Portanto, quando enfrentamos esse problema, podemos pular com segurança os testes unitários

na maior parte do tempo e nossa “pirâmide” de automação de testes pode ser assim:

Figura 8.4 Test Automation Kite,


adaptado para aplicação CRUD

Em tal projeto, os testes unitários provam ser valiosos apenas quando lidamos com alguma calculadora ou validador.

Todas as funcionalidades são verificadas com testes de API partindo do pressuposto de que uma quantidade mínima de

cenários testados é suficiente. Um teste para um cenário positivo, um para um cenário negativo e potencialmente um para

verificar a segurança (se acertarmos a autenticação) deve nos dar confiança suficiente. Poucos testes extras de UI devem

complementar a estratégia.

Cuidado com o sabor descoberto incorretamente. Se um aplicativo se beneficiar da Arquitetura Limpa, mas for

implementado como se fosse um CRUD, os testes serão muito menos eficazes, pois mais e mais cenários teriam que
ser verificados por meio de testes de API. Tenha paciência

Lembre-se de que a questão não é apenas a velocidade dos testes, mas também sua estabilidade e

facilidade de manutenção. Se sua única dependência for um banco de dados, todo o conjunto de testes terminará em um
Machine Translated by Google

tempo relativamente curto (por exemplo, menos de 10 minutos), e toda a configuração se resume a colocar poucos

objetos no banco de dados, ainda está tudo bem.

COMO TESTAR UM PROXY PARA OUTROS SISTEMAS?

Isenção de responsabilidade: esta parte foi escrita pensando em integrações escassas com provedores terceirizados

que possuem APIs públicas. Tem pouca ou nenhuma aplicação a microsserviços. Para este último, há uma nota no final.

Um projeto (ou parte dele - Adaptador) pode ser apenas um proxy de outro serviço. Mesmo que alguém forneça outra

interface de usuário, a maior parte da lógica de negócios ainda está sendo executada em outro lugar. A função do

nosso sistema é traduzir as solicitações da nossa GUI e lidar com as respostas. Outra forma de saber se temos esse tipo

de problema é perguntar o que aconteceria se o serviço de terceiros desaparecesse. Se a resposta for “nosso sistema não

vai funcionar de jeito nenhum”, então você tem um proxy. Um exemplo típico é uma pesquisa de voos baratos que verifica

alguns fornecedores. Se não conseguir buscar os voos disponíveis em outros lugares, será inútil e não fornecerá

nenhum serviço. É claro que os resultados poderiam ser armazenados em cache por algum tempo para fornecer um serviço

mínimo para as rotas de voo mais populares, mas isso é apenas uma fração do valor original.

Neste caso, mais uma vez, os testes unitários não provam ser a fonte mais confiável de verdade sobre se o nosso

software funciona. Isto depende principalmente do fornecedor externo e não das nossas unidades isoladas.

Eventualmente, temos que recorrer a testes de integração. Uma necessidade absoluta é testar pelo menos uma vez o código

usando cada endpoint (ou um método se falamos de RPC ou SOAP). Há mais duas coisas que precisamos abordar:

dependências entre métodos e validação de dados.

Não é possível usar o ponto final de reembolso se um ponto final de cobrança não tiver sido usado anteriormente.

Obviamente, poderíamos testar como o reembolso se comporta se lhe dermos um ID inexistente, mas isso traz pouco ou

nenhum valor para um cliente de API como nós. Nesse caso, pode-se simplesmente emitir solicitações adicionais conforme

necessário para verificar cada endpoint da API individualmente. A segunda solução seria escrever testes que verificariam

cenários da vida real chamando vários métodos de uma só vez, por exemplo, criando cobrança (1. chamada), depois

verificando seus detalhes (2. chamada) para finalmente reembolsar (3. chamada). Tudo em
um teste.
Machine Translated by Google

@pytest.mark.super_cool_payment_provider def
test_charge_then_capture( api_consumer:
ApiConsumer, fonte: str, api_key:
str, ) "- Nenhum:

# primeiro, chamamos nosso código para cobrar e capturar 15 dólares


charge_id =
api_consumer.charge(get_dollars("15,00"), fonte

) api_consumer.capture(charge_id)

# "".então usamos a API do Provedor de Pagamento para verificar se a cobrança foi bem-sucedida
resposta =
solicitações.get(f"{Request.url}/v1/charges/
{charge_id}", auth=(api_key, ""),

) capture_json = response.json() assert


capture_json["quantia"] "= 1500 # centavos
afirmar capture_json["moeda"] "= "usd"
afirmar capture_json["capturado"]

A validação de dados pode ser tratada principalmente usando objetos de valor como argumentos e valores
de retorno do Facade. As regras de validação podem ser facilmente aplicadas devido à natureza dos
Objetos de Valor. O número de testes de integração deve ser muito limitado e devem ser escritos tendo em
46
mente o Princípio da Robustez (também conhecido como Lei de Postel):

Seja conservador no que você faz, seja liberal no que você aceita dos outros (muitas vezes
reformulado como “Seja conservador no que você envia, seja liberal no que você aceita”)

Simplesmente dizendo, escrevemos testes (e código!) Verificando apenas coisas em que confiamos. Por
exemplo, falhar em um teste se houver um campo inesperado no JSON viola o Princípio da Robustez. Tal
mudança na API é considerada compatível com versões anteriores e, portanto, não deve interromper nossa
integração.

Outra coisa a considerar é quão estáveis seriam esses testes. Se eles falharem aleatoriamente devido a um
provedor falível, seria negligenciado executá-los. Existem pequenas coisas mais irritantes para um
desenvolvedor de software do que um conjunto de testes falhando localmente sem culpa dele. O mesmo
vale para um pipeline de CI. As soluções podem variar desde excluir esses testes de execuções locais até
adicionar novas tentativas aos testes ou implementá-los no próprio código em teste.

46 Princípio da Robustez https://en.wikipedia.org/wiki/Robustness_principle


Machine Translated by Google

Por último, mas não menos importante, muitos provedores externos possuem APIs de teste que
podemos aproveitar. Isso é extremamente útil quando chamar a API real tem implicações sérias, como
transferir dinheiro ou reservar um voo. Seria bom não ter que cancelar reservas manualmente depois
que alguns desenvolvedores executassem testes em suas máquinas. Naturalmente, temos que ter certeza de
que a API de teste é 100% compatível com aquela que usamos na produção.

E se eu tiver dezenas ou centenas de integrações (por exemplo, em microsserviços?)

O conselho dado acima não é realmente aplicável em um sistema que consiste em microsserviços. A própria
ideia de teste de integração é contraditória com a forma como os sistemas de microsserviços bem feitos
são desenvolvidos e implantados. Testar serviços isoladamente também não resolve o problema - é como
confiar apenas em testes unitários. Felizmente, uma solução adequada foi descoberta e redescoberta -
47
Teste de Contrato Orientado ao Consumidor. Uma das ferramentas mais populares para facilitar
48
esta técnica é o Pacto. Este tópico é interessante, embora permaneça fora do escopo deste livro. Um leitor
interessado certamente será capaz de estudar mais detalhadamente os testes de contrato orientados ao
consumidor por conta própria.

COMO TESTAR UM SISTEMA PROFUNDO?

Quando percebemos que estamos lidando com um problema essencialmente complexo, a Arquitetura Limpa
ou técnicas mais sofisticadas (como DDD) vêm a calhar. Complexidade significa muitos cenários
potenciais, inúmeros casos extremos e muitos fatores que afetam o resultado das ações realizadas pelos
usuários.

Em primeiro lugar, apenas a estrutura e arquitetura de código apropriadas podem nos permitir usar uma
estratégia de teste eficaz. Eficaz não significa simples - é necessário combinar pelo menos alguns tipos de
testes automatizados para ganhar seu nome. A lógica de negócios (entidades) de toda a empresa deve
ser testada em unidade. As regras de negócios do aplicativo (casos de uso) se enquadram nos testes de
serviço. Depois temos Repositórios e Adaptadores que serão os mais beneficiados com os testes de
integração tal como foi descrito na seção anterior - Como testar um proxy para outros sistemas?. Depois, há
uma estrutura web e potencialmente uma interface de usuário, então também devemos ter alguns testes de API
e UI.

Então, uma pirâmide de automação de testes será muito semelhante à do exemplo do início deste
capítulo.

47
Ian Robinson, testes de contratos orientados ao consumidor https://martinfowler.com/articles/
consumerDrivenContracts.html

48
Pacto https://docs.pact.io/
Machine Translated by Google

REDESCOBRINDO O TESTE DE UNIDADE

TESTE DE CAIXA PRETA E BRANCA

O teste automatizado pode ser atribuído a outro recurso - o quanto um teste sabe sobre o código que está
sendo testado. Usando a mesma terminologia dos testes manuais, podemos pensar em testes de caixa
preta e caixa branca. O teste de caixa preta significa que um testador não tem ideia sobre os
aspectos internos, enquanto o teste de caixa branca é exatamente o oposto - um testador sabe e vê tudo,
incluindo a implementação. O teste de caixa preta valida os requisitos e especificações, enquanto o teste
de caixa branca valida o código.

Os testes unitários podem ser implementados de ambas as maneiras. Um extremo é um ato ortodoxo de
caixa branca, quando a unidade em teste não tem segredos do teste. No contexto de testes unitários, isso
raramente é uma boa ideia. Tal abordagem duplica a implementação em testes, impossibilitando
evoluir nossos testes de tal forma que à medida que os testes se tornam mais específicos, o código fica
mais genérico. Além disso, os testes estão fortemente acoplados à implementação, o que os faz falhar
cada vez que esta muda. Dessa forma, o teste de unidade se assemelha ao ato de despejar concreto
- garantindo que ninguém possa mover nada. No teste de caixa branca de uma classe, chamamos seus
métodos públicos, mas frequentemente verificamos o resultado examinando campos privados ou usando
simulações de tipos usados internamente.

Exemplo:

# parte da implementação da classe Auction


leilão de classe :
def _init_( self, id:

AuctionId,
*omitido_args,
finalizado: bool
) "- Nenhum:
"".

self._ended = finalizado
Machine Translated by Google

# Má maneira de testar
def test_Auction_Ending_ChangesEndedFlag(
ontem: datahora,
) "- Nenhum:
leilão = AuctionFactory(ends_at=ontem)

leilão.end_auction()

afirmar leilão._ended # 1

def test_Auction_Ending_ChangesEndedFlag(
ontem: datetime, ) "-
Nenhum:
leilão = AuctionFactory(ends_at=ontem) leilão._ended
= True # 2

com pytest.raises (BidOnEndedAuction):

leilão.place_bid(bidder_id=1, quantidade=get_dollars("19,99"),
)

def test_EndedAuction_Ending_RaisesException(
ontem: datetime, ) "-
Nenhum:
leilão = AuctionFactory(ends_at=ontem) leilão._ended
= True # 2

com pytest.raises (AuctionAlreadyEnded):


leilão.end_auction()

49 À primeira vista, o primeiro teste parece muito bom…


Observe quanta intimidade inadequada existe.
Até você notar seu título e afirmação. Sem mencionar o acesso ao campo pseudoprivado em (1).
O segundo e o terceiro testes parecem melhores em termos de afirmação, mas colocar um leilão no estado
esperado (2) manipulando seu campo privado é simplesmente errado.

Agora, imagine que para fins de rastreamento o sinalizador _ended seja substituído por _ended_at que
mantém a instância de data e hora indicando o momento da chamada end_auction ou None se o método
não tiver sido chamado. Todos esses três testes falhariam, embora provavelmente tudo funcionaria em
serviços ou testes de nível superior (e em produção). Os testes unitários são um fardo inútil, não são?
Bem, eles são escritos de uma forma que viola o encapsulamento.

49 https://refactoring.guru/smells/inappropriate-intimacy
Machine Translated by Google

Uma abordagem melhor é respeitar a privacidade da classe e testá-la de forma caixa preta. A regra é
simples - não temos permissão para perguntar à classe seus segredos e podemos tocar apenas em seus
métodos e propriedades públicas.

def test_EndedAuction_PlacingBid_RaisesException( ontem:


datetime, ) "- Nenhum:
leilão =
AuctionFactory(ends_at=ontem) leilão.end_auction() # 1

com pytest.raises (BidOnEndedAuction):


leilão.place_bid(bidder_id=1,

quantidade=get_dollars("19,99"),
)

def test_EndedAuction_Ending_RaisesException(
ontem: datetime, ) "-
Nenhum:
leilão = AuctionFactory(ends_at=yesterday)
leilão.end_auction() # 1

com pytest.raises (AuctionAlreadyEnded):


leilão.end_auction()

A mudança mais notável é que não manipulamos mais um campo privado, mas informamos o fim do
leilão (1). Em seguida testamos o comportamento da turma, verificando se um leilão encerrado nos impede de
fazer novos lances (teste 1) e encerrá-lo novamente. Observe que reduzimos o número de testes em um e
ainda temos a mesma cobertura. Além disso, mantivemos a capacidade de reorganizar os detalhes da
implementação, desde que o comportamento da classe permaneça inalterado.

Escrevo sobre testes de caixa branca e caixa preta, mas sempre há uma área cinzenta entre eles.

“Todos os problemas da ciência da computação podem ser resolvidos por outro nível de indireção”
- David Wheeler

Nos exemplos acima, uma classe AuctionFactory foi usada como auxiliar para criar Auction
instâncias. Este é um exemplo de construtor, um padrão bastante útil para usar em testes. Dito isto, em Python
existe uma excelente biblioteca factory_boy que facilita a escrita de construtores de forma declarativa. Para obter
um construtor, escrevemos uma espécie de especificação com valores padrão para um objeto que está sendo
construído.
Machine Translated by Google

classe AuctionFactory(fábrica.Factory):
classe Meta:
modelo = Leilão

id = fábrica.Sequence(lambda n: n) lances =
fábrica.List([]) título =
fábrica.Faker("nome") preço_inicial =
get_dollars("10,00") ends_at =
fábrica.Faker( "future_datetime",
end_date= "+7d"

) terminou = Falso

Em sua forma básica, todos os campos declarados em AuctionFactory são usados para
preencher os argumentos Auction."_init"_. Um equivalente aproximado, escrito de uma forma mais imperativa,
é o seguinte:

def create_auction( id:


Opcional[AuctionId] = Nenhum, lances:
Opcional[List[Bid]] = Nenhum, título:
Opcional[str] = Nenhum, preço inicial:
Dinheiro = get_dollars("10,00"), ends_at: datetime = datetime.
agora() + timedelta(dias=7), finalizado: bool =
False, ) "- Leilão: se id for
Nenhum:

# objeto que podemos iterar para obter números inteiros como 1, 2, 3"".
id = leilão_sequence.next() se lances
for Nenhum: lances
= [] se título
for Nenhum:
título = faker.nome()

return Leilão(id,

título,
preço_inicial,

lances,
término_em, finalizado,
)
Machine Translated by Google

Agora, se mudássemos o campo _ended para _ended_at e ainda deixássemos nossos testes usá-lo via
create_auction ou AuctionFactory com parâmetro finalizado , simplesmente teríamos que implementar a lógica em
um construtor:

# construtor baseado em factory_boy


classe AuctionFactory(fábrica.Factory):
classe Meta:
modelo = Leilão

parâmetros de classe :
terminou = Falso

id = fábrica.Sequence(lambda n: n)
"".

terminou_at = fábrica.LazyAttribute( lambda o:


datetime.now() - timedelta(dias=1) se
o.ended

senão Nenhum
)

# construtor baseado em função


def
create_auction( *omitido_args, finalizado: bool = False,
) "- Leilão:
"".

se não terminou:
terminou_at = Nenhum
outro:
terminou_at = datetime.now() - timedelta(dias=1

return Leilão( id, título,

preço_inicial, lances,
fins_em,
fim_em,

Contar com construtores nos testes é uma boa ideia, pois eles podem abstrair muitos detalhes de
implementação e limitar significativamente o número de locais onde precisamos de alterações.
Eles também simplificam a criação de objetos, fornecendo valores padrão para todos os campos que decidimos não
Machine Translated by Google

nos especificar. Claro, eles sempre terão que acompanhar as mudanças no Leilão
implementação porque estão fortemente acoplados a ela.

Resumindo, confiar em métodos públicos durante os testes geralmente é uma ideia muito boa. O
vazamento de conhecimento sobre a implementação custará caro no longo prazo. Lembre-se da analogia de
despejar concreto. Os testes acoplados à implementação garantem que ninguém possa mover nada no
futuro.

TESTES ORIENTADOS PARA ESTADO E INTERAÇÃO

INTRODUÇÃO

Outro ângulo de teste que pode ser observado é se eles são mais orientados ao estado ou à interação.
Essas duas abordagens parecem bastante semelhantes durante o exercício do sistema em teste (etapa
Agir ou Quando), mas têm uma abordagem diferente para a fase de verificação (etapa Afirmar ou Então).
O teste baseado em estado inspeciona o estado interno do sistema em teste, enquanto o teste baseado
em interação verifica se o sistema em teste chamado usou dependências simuladas conforme esperado.

# exemplo de teste baseado em interação


# Verifica se um método simulado de EventBus foi chamado com o argumento esperado
@pytest.mark.usefixtures("transação") def
test_AuctionsRepo_UponSavingAuction_PostsPendingEventsViaEventBus(
conexão: Conexão,
leilão_com_evento_pendente: Leilão,
evento_pendente: Evento,
evento_bus_mock: Simulado,
) "- Nenhum:
repo = SqlAlchemyAuctionsRepo( conexão,
event_bus_mock
)

repo.save(auction_with_pending_event)

event_bus_mock.post.assert_called_once_with(evento_pendente

)
Machine Translated by Google

# exemplo de teste baseado em estado


@pytest.mark.usefixtures("transaction") def
test_AuctionsRepo_UponSavingAuction_ClearsPendingEvents( conexão:
Conexão,
leilão_with_pending_event: Leilão,
event_bus_mock: Mock, )
"- Nenhum:
repo = SqlAlchemyAuctionsRepo( conexão,
event_bus_mock
)

repo.save(auction_with_pending_event)

afirmar len(auction.domain_events) "= 0

Tem havido uma disputa sobre uma abordagem ser melhor que outras, mas pessoalmente, considero tal
argumento fora de questão. Acredito que as abordagens de testes baseadas em estado e baseadas em
interação são úteis em situações apropriadas. No entanto, isso não significa que eles possam sempre ser
usados de forma intercambiável. Independentemente de qual você usar, um objetivo geral é evitar a violação
do encapsulamento de um sistema em teste. Infelizmente, nenhuma das duas abordagens impede isso
intencionalmente.

PERIGOS DOS TESTES ORIENTADOS PELO ESTADO

O teste baseado em estado parece ser uma escolha perfeita quando implementamos a etapa Act (ou When) com
uma simples chamada de método público e, em seguida, verificamos seu resultado usando outro método público.
No entanto, é perigoso um teste usar campos ou métodos privados. Quando não há uma maneira óbvia
de ler um estado, pode-se sentir tentado a adicionar um método especial que será usado apenas em
testes.

def test_Auction_Ending_ChangesEndedFlag(
ontem: datahora,
) "- Nenhum:
leilão = AuctionFactory(ends_at=ontem)

leilão.end_auction()

assert ( # Linter reclama de _ended? Vamos adicionar um getter!


leilão.is_ended()
)
Machine Translated by Google

Evite essa tentação. Existem vários bons motivos para não usar tais truques: 50

1. adiciona carga de manutenção e terá que ser mantido uma vez interno

mudanças de implementação (considere trocar _ended por _ended_at)

2. Cresce a complexidade da classe Leilão

3. Os testes não descrevem mais como o Leilão deve ser utilizado. is_ended não deve ser usado fora do
código de produção, mas os testes que são documentação ativa sugerem o contrário

4. A verificação do estado interno não contribui para transmitir conhecimento do domínio. Ok, o leilão
terminou, mas e daí? Quais são as consequências? O que posso/não posso fazer com um leilão
encerrado?

Para concluir, o teste baseado em estado é uma ótima escolha, desde que seja possível inspecionar o estado
sem quebrar o encapsulamento do sistema em teste.

PERIGOS DO TESTE ORIENTADO À INTERAÇÃO

O teste baseado em interação é conveniente quando verificamos o uso de dependências. Esse tipo de teste
é usado em combinação com simulações que são usadas para escrever asserções e potencialmente
falhar nos testes. Os mocks são a nossa salvação quando não podemos/não queremos usar dependências
externas, como provedores de pagamento ou outros terceiros. Em termos de Arquitetura
Limpa, pode-se simular Port e então verificar se o Caso de Uso chamou Port conforme esperado:

def test_EndingAuction_WonEndedAuction_CallsPaymentProviderWithAuctionCurrentPrice( ending_auction_u


EndingAuction, leilão: Leilão,
payment_provider:
Mock,) "- Nenhum:

ending_auction_uc.execute( EndingAuctionInputDto(auction.id)
)

pagamento_provider.begin_payment.assert_called_once_with( leilão.preço_atual

50Nat Pryce, exemplo de teste baseado em estado versus interação http://natpryce.com/articles/


000356.html
Machine Translated by Google

Zombamos da dependência externa, que pode ser lenta, instável ou não deve ser usada por qualquer outro
motivo. Zombar é bom, desde que a interface que simulamos seja estável e raramente alterada. Dito de outra
forma, ter simulações para PaymentProvider.begin_payment
introduz acoplamento adicional. Isso não deveria ser um problema, a menos que alguém interprete erroneamente
o teste em regra de isolamento. Isso não significa que uma classe, um método ou uma função devam ser
testados sozinhos. Implica apenas que um teste não deve afetar nenhum outro teste. O código acima usa uma
instância real da classe Auction. Se o substituíssemos por um mock (e o fizéssemos em todos os outros
testes de Caso de Uso), acabaríamos tornando o código imutável do Leilão - qualquer pequena alteração tornaria
todos os mocks inválidos, fazendo com que os testes que os utilizam falhassem.

Resumindo, os testes baseados em comportamento são perigosos quando as simulações são usadas em
excesso. Eles devem ser aplicados com moderação. Bons exemplos são verificar como o sistema em teste
utiliza Ports ou dependências externas que não controlamos.

STUBS VERSUS MOCKS

Zombar não é a única abordagem para substituir objetos para fins de teste. Embora as simulações sejam
flexíveis e nos permitam substituir praticamente qualquer objeto por um comportamento especificado
dinamicamente, elas têm seus limites e casos de uso ideais. De modo geral, os mocks são apenas uma categoria
de objetos falsos usados para fins de teste. O nome mais geral para este tipo de objetos é Test Doubles. Outro
tipo é um stub - implementações simples que retornam respostas prontas. Eles devem ser usados de
uma maneira diferente das simulações. Quando stubs são usados em um teste específico, é preferível escrever
asserções sobre um objeto que os utiliza do que sobre os próprios stubs.

Considere um exemplo:

classe PaymentProviderStub(PaymentProvider):
def start_payment(self,
amount: Money) "- bool:
return True
Machine Translated by Google

def test_EndingAuction_WonEndedAuctionStubbedPaymentProvider_EmitsDomainEvent(
leilão: Leilão,
payment_provider: PaymentProviderStub,
event_bus_mock: Simulado,
) "- Nenhum:
final_auction_uc = EndingAuction(
provedor_de pagamento, event_bus_mock

)
finalizando_auction_uc.execute( EndingAuctionInputDto(auction.id)
)

afirmar event_bus_mock.post.assert_called_once_with(
"".

Embora o teste esteja verificando EndingAuction e PaymentProvider e EventBus


colaboradores foram substituídos, as afirmações são feitas apenas sobre o EventBus simulado. Este teste
verifica se EndingAuction publica um evento de domínio via EventBus , desde que
PaymentProvider.begin_payment seja bem-sucedido. Observe que PaymentProviderStub é uma
implementação personalizada de uma porta.

Normalmente, não há atalhos para escrever stubs, embora as simulações do Python possam ser potencialmente
abusadas para fazer isso:

provedor_de pagamento = Simulado(


spec_set=PaymentProvider,
start_payment=Mock(return_value=True),
)

Existe outra maneira criativa de usar stubs que permite transformar praticamente qualquer teste orientado
a comportamento em um teste baseado em estado. Para fazer isso, temos que quebrar duas
regras mencionadas acima - primeiro, um método de consulta deve ser adicionado ao stub apenas
para fins de teste e a afirmação no teste será escrita no stub (ai).

classe PaymentProviderStub(PaymentProvider):
def _init_(self) "- Nenhum:
self._payments: Lista[Dinheiro] = []
Machine Translated by Google

def start_payment( self,


amount: Money ) "- bool:
#
begin_payment não apenas retorna uma resposta automática, mas também registra o
chamar

# isso torna esta classe um híbrido de Stub e Spy - outro tipo de teste duplo
self._payments.append(quantia) retorna
Verdadeiro

def get_requested_payments() "- Lista[Dinheiro]:


# retorna uma cópia, para que o estado não possa ser manipulado externamente
retornar self._payments[:]

def test_EndingAuction_WonEndedAuction_CallsPaymentProviderWithAuctionCurrentPrice(ending_auction_uc:
EndingAuction, leilão: Leilão,
payment_provider:
PaymentProviderStub,) "-

Nenhum:ending_auction_uc.execute( EndingAuctionInputDto(auction.id)
)

afirmar payment_provider.get_requested_payments() "= [


leilão.preço_atual
]

O método de consulta foi exposto em um stub especificamente para verificar como seu método start_payment foi
chamado. Alguns parágrafos antes, eu avisei sobre a adição de tais métodos especiais, mas esse conselho é
aplicável para classes usadas em produção, não para aquelas escritas exclusivamente para
testes.

TESTE DE UNIDADE DE UM MÓDULO INTEIRO

Já sabemos que devemos evitar acoplar testes com implementação porque isso impedirá a refatoração em
vez de acelerá-la e tornará quase impossível fazer qualquer alteração no código sem falhar em metade do
conjunto de testes. Um encapsulamento adequadamente projetado (ou melhor, descoberto por tentativa
e erro) é o que nos permite escrever testes em uma API estável e manter a liberdade de reorganizar detalhes
privados. Também sabemos que os testes unitários são baratos de escrever, mas muitas vezes são
subestimados devido ao pouco código que exercitam de uma só vez. Ou talvez estejamos errados sobre o
pequeno escopo…?

Um equívoco comum é que um teste de unidade trata apenas da verificação de um método de uma única
classe ou de uma função independente - a menor unidade que se pode imaginar. A confusão vem
Machine Translated by Google

do fato de que nem o teste unitário nem o teste unitário são estritamente definidos. Existem apenas pistas. Esperamos que

os testes de uma unidade sejam estáveis, isolados do mundo externo e significativamente mais rápidos do que outros tipos

de testes. Os desenvolvedores de software devem ser capazes de escrever e executar tais testes em suas máquinas. Portanto,

é hora de uma mudança de perspectiva - vamos pensar em como alguém pode testar um módulo inteiro como uma unidade

única com uma abordagem de caixa preta. Primeiramente, vamos lembrar o que é um módulo e onde estão seus pontos de

contato com o entorno.

Figura 8.5 Pontos de contato de um módulo: Limites de entrada e saída


com respectivos DTOs, consultas e eventos de domínio
Machine Translated by Google

DUPLAS DE TESTE - RESUMO RÁPIDO

Stub - um teste duplo implementado antes do teste que retorna respostas prontas (codificadas). Não
deve falhar no teste. Sua função é substituir a dependência de um sistema em teste. Em seguida,
verificamos se ele se comporta conforme o esperado, desde que com dependência em stub.

Fake – implementação simples, porém funcional, de uma dependência que ainda não está pronta
para uso em produção, pode ser valiosa em testes

Dummy - geralmente um valor primitivo como None que é simplesmente repassado e não usado.
Por exemplo, podemos precisar de um argumento para o construtor da classe que não é usado no teste
específico, mas ainda assim exigido pela classe

Mock - um teste duplo que pode ser consultado sobre como foi usado. Usado em asserções,
especialmente em testes orientados a comportamento

Quando testamos a unidade de uma classe, fazemos isso chamando seus métodos e verificando os resultados
ou campos de uma instância. Felizmente, não estamos usando métodos ou campos privados em testes, pois
quebramos o encapsulamento e dificultamos a refatoração da classe no futuro. Se pensarmos em um módulo,
ele também possui sua própria API pública - são Casos de Uso (ou Comandos + Manipuladores de
Comandos) e Consultas. Um ponto de contacto típico com o mundo exterior é um Porto ou Repositório. O
repositório representa o armazenamento privado de um módulo que precisa ser mantido entre
invocações de Casos de Uso. As coisas que saem de um módulo são eventos de domínio (via barramento de
eventos) e DTOs de saída (se usarmos limites/apresentadores de saída).
Machine Translated by Google

Para escrever qualquer teste em um módulo inteiro, precisamos decidir como conseguir quatro coisas:

1. Colocar um sistema em teste no estado desejado (Organizar/Dado)

2. Invocação de uma ação do sistema em teste (Act/When)

3. Verificando o resultado da ação do ponto 2. (Afirmar/Então)

4. Lidando com dependências (portas e repositórios)

COLOCANDO UM SISTEMA EM TESTE EM UM ESTADO DESEJADO


(ORGANIZAR / DAR)

A forma como a configuração do cenário é implementada depende se tratamos as Entidades como um


detalhe privado de um módulo ou não. No primeiro caso, não se deve tocar em Entidades em testes,
mas é permitido chamar Casos de Uso de um determinado módulo de forma que coloquem o sistema em
teste no estado que desejamos. Se nosso módulo estiver usando um Façade em vez de Casos de
Uso, só podemos chamar os métodos públicos do Facade:

def test_Auction_OverbidFromOtherBidder_EmitsEvents( inicial_auction_uc:


BeginningAuction, place_bid_uc: PlacingBid,
*other_args) "- Nenhum: leilão_id
=1

amanhã =

datetime.now( tz=pytz.UTC ) + timedelta(dias=1)


# Primeiro, o leilão começa

Beginning_auction_uc.execute( BeginningAuctionInputDto( leilão_id, "Foo", get_dollars("1.00"), amanhã,


)
)
# Em seguida, um novo lance vencedor é feito

place_bid_uc.execute( PlacingBidInputDto( 1, leilão_id, get_dollars("2.0")


)
)

# Agir / Quando
"".
Machine Translated by Google

# Afirmar / Então
"".

Pelo lado positivo, isso está muito próximo da forma como o módulo será usado no código de produção.
Por outro lado, a introdução de um bug fatal no BeginningAuction criaria uma cascata de testes com falha, uma vez

que ele é usado em praticamente todos os outros testes. Uma situação como essa pode ser facilmente contida
executando testes com frequência e trabalhando de forma TDD. Assim, podemos saber exatamente qual alteração
causou falhas nos testes e, em seguida, revertê-la ou alterá-la.

Em geral, manter as entidades ocultas melhora o encapsulamento e, portanto, permite alterações de código mais
ousadas. No entanto, quando colocar um módulo no estado desejado usando Casos de Uso se torna mais complexo,
pode-se querer sacrificar algum encapsulamento. Construtores, como AuctionFactory, são então o nosso caminho a
percorrer. Mesmo se optarmos pelo encapsulamento total, os construtores ainda podem ser úteis, por exemplo, para
gerar DTOs de entrada para nossos casos de uso.

INVOCANDO UMA AÇÃO NO SISTEMA EM TESTE (ACT/WHEN)

Esta etapa é a mais simples de todas. A implementação se resume a apenas chamar um Caso de Uso:

def test_Auction_OverbidFromOtherBidder_EmitsEvents( place_bid_uc:


PlacingBid, *other_args
) "- Nenhum:
# Organizar / Dado
"".

# Agir / Quando

place_bid_uc.execute( PlacingBidInputDto( 2, leilão_id, get_dollars("3.0")


)
)

# Afirmar / Então
"".

VERIFICAÇÃO (ASSERT/ENTÃO)

Embora a lógica de negócios realmente interaja com as Entidades, elas raramente são iguais às que os usuários do
sistema veem (a menos que estejamos escrevendo CRUD). Em casos simples, apresentamos um subconjunto de campos.
Em cenários mais complexos, o usuário vê uma combinação de dados de diferentes entidades, provavelmente
aninhados e distorcidos. É por isso que o CQRS é tão útil porque alivia a carga das Entidades.
Eles não precisam mais se preocupar tanto em executar a lógica de negócios quanto em fornecer insights sobre
Machine Translated by Google

o estado do sistema para os usuários. Isso também implica que as consultas não podem ser testadas em unidade,
uma vez que estão fortemente acopladas à infraestrutura subjacente e, portanto, devem ser testadas usando
testes de nível superior. Se utilizássemos um repositório falso implementado em memória, obviamente teríamos que
reimplementar todas as consultas apenas para testes. Isso torna todo o esforço questionável.

Mesmo se tirarmos as consultas da equação, ainda existem maneiras óbvias de fazer afirmações durante o
teste unitário de um módulo inteiro:

• Saída DTO, se usarmos Output Boundary + Presenter

• exceções lançadas

• quais eventos de domínio foram emitidos dentro do módulo

• como os mocks para Portos foram usados

# verificando o DTO de saída


def test_PlacingBid_FirstBidHigherThanIntialPrice_IsWinning(
place_bid_uc: PlacingBid,
output_boundary: PlacingBidOutputBoundaryFake, leilão_id:
AuctionId,) "- Nenhum:

place_bid_uc.execute( PlacingBidInputDto(1,
leilão_id, get_dollars("100")
)
)

esperado_dto = ColocandoBidOutputDto(
is_winner=Verdadeiro,
preço_atual=get_dollars("100"),

) afirmar output_boundary.dto "= esperado_dto


Machine Translated by Google

# verificando exceções lançadas


def

test_PlacingBid_BiddingOnEndedAuction_RaisesException( Beginning_auction_uc: BeginningAuction, place_bid

Beginning_auction_uc.execute( BeginningAuctionInputDto( 1, "Bar", get_dollars("1.00"), ontem + timedelta(ho


)
)

com pytest.raises (BidOnEndedAuction):

place_bid_uc.execute( ColocandoBidInputDto( 1, 1, get_dollars("2,00")


)
)
Machine Translated by Google

# verificando eventos de domínio emitidos


def test_Auction_OverbidFromWinner_EmitsWinningBidEventOnly(
place_bid_uc: PlacingBid,
event_bus: Mock,
leilão_id: AuctionId,
leilão_title: str, ) "-
Nenhum:

place_bid_uc.execute( PlacingBidInputDto( 3, leilão_id, get_dollars("100")


)

) event_bus.post.reset_mock()

place_bid_uc.execute( PlacingBidInputDto( 3, leilão_id, get_dollars("120")


)
)

event_bus.post.assert_called_once_with(
WinningBidPlaced(leal_id,
3,

get_dollars("120"),
leilão_title,
)
)

Você pode estar se perguntando se essas abordagens são suficientes, já que não estamos validando consultas de forma

alguma. A verdade é que devemos ser capazes de verificar muitos cenários possíveis dessa forma. Graças aos Eventos

de Domínio, não poder inspecionar o estado diretamente não deve ser um problema.

Principalmente porque poderíamos implementar modelos de leitura para serem gerados apenas usando Domínio
Eventos. 51

Outra ideia bastante óbvia que se pode ter é fazer afirmações sobre Entidades sendo salvas.

Isso pode parecer uma ótima ideia, mas causa os mesmos problemas de encapsulamento como os mencionados

anteriormente, em parte sobre colocar um módulo no estado desejado antes do teste.

51 Matthias Noback, Guia de estilo de design de objetos, Capítulo 8. Construir modelos de leitura a partir do domínio

eventos
Machine Translated by Google

LIDAR COM DEPENDÊNCIAS (PORTAS E REPOSITÓRIOS)

Embora um único módulo possa lidar com a lógica de negócios mais complexa que possamos imaginar,
ele ainda requer portas e repositórios para realmente fazer alguma coisa. Para que serviriam eles se
não pudessem se comunicar com o mundo?

As portas muitas vezes abstraem sistemas externos que não controlamos. Por exemplo, não podemos
confiar na sua estabilidade ou colocá-los facilmente da forma desejada. Definitivamente não devemos
testar um módulo com um adaptador real. Nós zombamos ou eliminamos esse tipo de dependência.

Há um caso aparentemente especial em que um módulo chama outro – o que fazemos? A solução é
a mesma - simule ou esboce essa dependência.

No caso dos Repositórios, queremos substituí-los por outro teste duplo - falso. Este último é uma
implementação simplificada, mas funcional, de uma determinada interface (AuctionsRepository
nesse caso). Uma implementação simples na memória é o que procuramos. Isso deve estar alinhado
com nossas implementações de nível de produção. Por exemplo, se nosso repositório usa barramento
de eventos para emitir eventos, uma implementação na memória deve fazer o mesmo.

RESUMO DO CAPÍTULO

Embora os testes automatizados não possam garantir que não haverá bugs no código, eles ainda são um
investimento que vale a pena. Não existe uma única prática recomendada (a propósito, não gosto muito desse
termo) para criar conjuntos de testes automatizados que maximizem o retorno do investimento. É típico que
testes de nível inferior, por exemplo, testes unitários, sejam mais rápidos, mais simples e mais baratos do que
testes de API de alto nível. Dito isto, os funcionários da LMAX têm se gabado de seu extenso conjunto de testes,
fortemente focado em testes de nível superior. No entanto, eles estão construindo um sistema de negociação
extremamente rápido, portanto, a menos que alguém esteja construindo exatamente o mesmo produto, eles não
devem copiar imprudentemente outras estratégias de teste. Cada projeto precisa de um pouco de
experimentação e testes fracassados para encontrar uma abordagem ideal para testes automatizados.
Machine Translated by Google

Vamos concluir este capítulo com um exemplo de estratégia de teste:

• testes unitários pesados para todo o módulo, com repositórios falsificados e portas simuladas

• não há testes de unidade para classes individuais dentro dos módulos de Arquitetura Limpa, a menos que sejam
coisas como calculadoras ou validadores e seja muito impraticável testá-los com o módulo inteiro

• testes de nível superior para módulos baseados em Façade , sem testes duplos para banco de dados

• sempre tenha testes para verificar provedores externos, no mínimo imitar o uso real deles, por exemplo, cobrar
cartão de pagamento e depois capturar

• pelo menos três testes em nível de API para cada método de caso de uso / fachada para verificar cenário
positivo, cenário negativo e para verificar se a autorização está funcionando

• alguns testes de UI que cobrirão vários endpoints de uma só vez

• não faça testes de aceitação automatizados no nível da IU

Lembre-se de que um bom conjunto de testes deve capacitar os desenvolvedores e incentivá-los a introduzir mudanças.
Deveria ser uma espécie de rede de segurança que sussurra em seus ouvidos “Eu te protejo!” mesmo quando
devem tocar nas partes mais críticas e lucrativas do sistema. No longo prazo, um conjunto de testes ajuda a manter
uma alta velocidade de entrega. Tenha em mente que seu conjunto de testes só pode ser tão bom quanto o código
que está sendo testado. É preciso prestar atenção ao design e à qualidade do código para garantir alta testabilidade.
Machine Translated by Google

PALAVRAS FINAIS

de: sebastian@cleanarchitecture.io
para: o querido leitor
assunto: Agradecimentos pessoais

Caro leitor,

você fez isso!

Obrigado por me acompanhar durante toda a palestra deste livro. Espero sinceramente que lê-lo tenha sido uma
experiência pelo menos tão esclarecedora para você quanto escrevê-lo foi para mim.

Devo admitir que pensei que seria relativamente fácil escrever sobre a Arquitetura Limpa quando você a
implementasse duas vezes e ensinasse algumas dezenas de pessoas como fazê-lo.

Ao escrever o livro, tentei ver as coisas de diferentes ângulos. Eu não estava procurando verdade absoluta
ou melhores práticas (novamente, eu não diria que gosto desse termo - as melhores práticas dependem do
contexto). É assim que funciona no desenvolvimento de software – é um jogo de compensações. Não existem
soluções universais, apropriadas para todos. Um desenvolvedor escolhe uma solução para não ter um problema
específico de que não gosta. Ao mesmo tempo, eles aceitam várias questões menos significativas com as
quais precisam aprender a conviver.

É por isso que quero que você avalie cada receita mostrada neste livro antes de usá-la. Procure saber se isso
realmente beneficiará você, sua equipe, sua empresa e seu projeto.

Independentemente de você usar 5% ou 100% dos conselhos do livro, gostaria que isso o ajudasse a progredir
em sua carreira.

Sinceramente,

Sebastião
Machine Translated by Google

APÊNDICE A: MIGRANDO DE
LEGADO
DEVO MIGRAR?

Para que servem as técnicas se forem aplicáveis apenas a projetos green-field?

Trabalhar com projetos legados (também conhecidos como brown-field) é um cenário muito mais provável
para a maioria de nós. Portanto, uma estratégia de migração seria muito bem-vinda. Francamente, há pouca
ou nenhuma chance de você conseguir migrar todo o seu projeto para a Arquitetura Limpa.
Não apenas porque nem todo o código se beneficiaria com isso, mas também porque algumas áreas não
são ativamente desenvolvidas. Reescrevê-los apenas para que finalmente pareçam bons é arte pela arte.
A menos que seja uma parte não crítica, você decide reescrever para entender a aplicação da Arquitetura
Limpa.

Você pode negligenciar a refatoração para a Arquitetura Limpa se o seu projeto estiver passando por
grandes mudanças. Condições instáveis tornarão consideravelmente mais difícil (e mais caro) o progresso.

COMO FAZER ISSO?

Comecemos pelo princípio – temos que ter uma ideia sobre os diferentes módulos do nosso projeto. Se não
houver noção de módulos no código, temos que esboçá-lo, usando a imaginação e um quadro branco.
Não pule para o código se não tiver certeza de onde estão os limites dos módulos. Falar com pessoas.
Reúna conhecimento de domínio. Convide alguém que trabalha há mais tempo que você para apoiar
seus esforços. Idealmente, seria melhor se você criasse um mapa de contexto DDD, mas esboços
também serviriam.

Agora que você identificou os componentes, tente marcar sua complexidade/importância na escala de 1 a 3,
onde 1 é o mais importante e 3 é o menos importante. 1 é para componentes que são a vantagem
competitiva da empresa, são mais complexos, são a principal fonte de receita ou são apenas complexos.
Por outro lado, 3 costuma ser um código cola para soluções de terceiros usadas por uma empresa que
não é alterado com muita frequência, se é que é alterado.

Agora que você entende como as coisas funcionam, o próximo passo é fazer alguns trabalhos de base.
Configure a biblioteca de injeção de dependência se ainda não tiver uma.

Identifique o limite do módulo e formalize-o, usando Casos de Uso ou Fachada. Não há necessidade de
começar a reescrever big bang identificando Entidades, escrevendo Repositórios para elas, etc.
Machine Translated by Google

cavalos, pelo menos por enquanto. No momento, basta encerrar o módulo e descobrir como ele é utilizado.

Em seguida, exponha uma API para isso. Então comece a usá-lo em outras áreas de código.

Não se esqueça dos testes. Seria ideal se você tivesse um conjunto de testes de nível superior que pudesse executar

localmente para garantir que suas refatorações sejam seguras.

Na próxima etapa, identifique o código acoplado a provedores externos e comece a movê-lo para um adaptador recém-

criado. Crie uma porta e configure uma ligação no contêiner IoC.

Na etapa final, comece a identificar as Entidades que protegerão suas regras de negócio. Crie repositórios para eles.

Essa será a parte mais desafiadora e duradoura da migração.

“NÃO POSSO PARAR DE ENTREGAR NOVOS RECURSOS”

Francamente, seria melhor nem pensar em congelar novos recursos. Refatorar a base de código é uma tarefa séria. Por

envolver tantas mudanças, é arriscado. Seria melhor adotar uma abordagem como Branch By Abstraction, que permitirá fazer

alterações junto com o desenvolvimento normal e implantar o código alterado.

Não tente fazer uma refatoração radical e definitivamente não pule para o código, a menos que você tenha um plano. Em vez

disso, adote uma abordagem constante para evoluir a base de código na direção desejada. Não é uma refatoração fácil de

fazer, pois você provavelmente aprenderá duas coisas ao mesmo tempo - a Arquitetura Limpa E os arcanos do seu projeto. Você

não precisa buscar a perfeição na primeira tentativa. O perfeccionismo não é aliado de um desenvolvedor de software.

Você não está construindo uma catedral que deveria durar séculos. Saiba onde você quer estar e vá devagar, passo a passo.
Machine Translated by Google

APÊNDICE B: INTRODUÇÃO AO

FONTE DE EVENTOS

O QUE É FONTE DE EVENTOS?

Vamos considerar o pedido de comércio eletrônico. Pode conter o status atual (novo, confirmado,
enviado, etc.) e resumos – preço total, frete e impostos. Naturalmente, a Ordem não existe por si só.
Geralmente transferimos para outra entidade, OrderLine, que se refere a um único produto
pedido com informações de quantidade. Esta estrutura poderia ser representada em um banco
de dados relacional da seguinte forma:

pedidos
-[REGISTRO 1]-------- id
|1
status |
NOVO preço_total | 169.9900

linhas_pedido
-[REGISTRO 1]""-
identificação | 1
id_pedido | 1
id_do_produto | 512
quantidade | 1
-[REGISTRO 2]""-
id |2
do pedido_id | 1
id_do_produto | 614
quantidade | 3

Ao armazenar dados desta forma, podemos sempre obter de forma barata o estado ATUAL do nosso pedido.
Armazenamos um dump do objeto serializado após as últimas alterações. Qualquer mutação de dados,
por exemplo, mudar o status de novo para enviado causa a substituição de dados. Perdemos irreversivelmente
o velho estado. E se precisarmos rastrear todas as mudanças…?
Machine Translated by Google

Vamos ver como isso se encaixa em outra tabela do banco de dados:

order_history -
[REGISTRO 1]--------------------------------- id | 1 id_pedido | 1
nome_do_evento
| Criado criado_at
| 09/08/2018
23:00:09.22674+02 dados
| {}
-[REGISTRO 2]---------------------------------- id | 2 id_pedido | 1
nome_do_evento
| LinhaAdicionada
criada_at | 09/08/2018
23:01:03.47922+02 | {"product_id": 512, "quantidade":
1} dados -[REGISTRO
3]------------------------------------------ --- id | 3 id_pedido | 1
nome_do_evento
| LinhaAdicionada
criada_at | 09/08/2018
23:37:06.93112+02 | {"product_id": 614, "quantidade":
3} dados -[REGISTRO
4]------------------------------------------ --- | 4 ID do pedido_id | 1

nome_do_evento
| Criado_at confirmado |
2020-08-09 23:52:03.08832+02 | dados {"status":
"confirmados"}

Tal representação permite-nos dizer com firmeza o que foi alterado e quando. No entanto, order_history fica em segundo

plano. É apenas um registro auxiliar de pedidos, adicionado apenas para atender a alguma necessidade comercial. Ainda

chegamos à tabela de pedidos originais quando queremos saber o estado exato de qualquer Pedido em todos os outros cenários.

Em particular, contamos com encomendas

sempre que fazemos alguma alteração (por exemplo, alterar a quantidade) ou ler o estado na maioria dos casos (por exemplo,

informar qual é o preço total do pedido).

No entanto, observe que order_history é tão bom quanto a tabela de pedidos quando precisamos obter o estado atual do

pedido . Só precisamos buscar todas as entradas para um determinado pedido e 'repeti-las' desde o início.

No final obteremos exatamente as mesmas informações que estão salvas na tabela de pedidos . Então ainda deveríamos

tratar a tabela de pedidos como nossa fonte de verdade? O Event Sourcing adota uma abordagem diferente. Podemos nos

livrar da mesa com segurança ou pelo menos não confiar mais nela em qualquer situação que realmente altere a Ordem.
Machine Translated by Google

Resumindo, Event Sourcing se resume a:

• Manter seus objetos de negócios (de agora em diante chamados de Agregados) como uma série de objetos reproduzíveis
eventos. Uma coleção desses eventos é chamada de fluxo de eventos

• Nunca excluir quaisquer eventos de um sistema, apenas anexar novos

• Usar eventos como a única maneira confiável de saber em que estado um determinado agregado está

• Se você precisar consultar dados ou apresentá-los em formato de tabela, mantenha uma cópia deles em

um formato desnormalizado. Isso é chamado de projeção

• Projetar seus agregados para proteger certas invariantes de negócios vitais, como pedidos

encapsula o resumo dos custos. Uma boa regra é manter os agregados tão pequenos quanto possível

O que você recebe em troca:

• Um histórico completo do que foi alterado, quando e por quem (se você incluir tal
informações em um evento)

• Depuração de viagem no tempo, permitindo recriar o estado do sistema em qualquer


momento

• Possibilidade de criar modelos de leitura especializados de seus dados para alto desempenho

• Anexar – apenas escreva um modelo que também seja mais fácil de escalar
Machine Translated by Google

EXEMPLO SIMPLES DE UM AGREGADO DE FONTE DE EVENTOS

PEDIDO COMO ENTIDADE

Considere a seguinte classe Order escrita como se fosse uma Entidade clássica:

Ordem da classe:
def _init_( self,
uuid:
UUID,
customer_id: CustomerId, linhas:
Opcional[List[OrderLine]] = Nenhum, status: OrderStatus
= OrderStatus.NEW, ) "- Nenhum:

se as linhas forem Nenhuma:

linhas = []
self._customer_id = customer_id self._status
= status self._lines = linhas

def confirm(self) "- Nenhum: if


self._status "! OrderStatus.NOVO:
raise IllegalStatusChange( "Apenas
NOVO pedido pode ser confirmado!"
)
self._status = OrderStatus.CONFIRMED

Para criar uma instância, precisamos apenas de customer_id e uuid. Existem argumentos padrão fornecidos para
linhas e status. O pedido protege uma invariante de domínio muito simples, ou seja, garante que apenas novos pedidos
possam ser confirmados.

APRESENTANDO EVENTOS

Vamos reescrever a classe Order usando Event Sourcing. Primeiro, precisamos de eventos que representem quaisquer
mutações de estado:

@dataclass(congelado=Verdadeiro)
evento de classe:
_subclasses: ClassVar[Dict[str, Type]] = {}

criado_at: versão de data e


hora: int

def _init_subclass_(cls) "- Nenhum:


"""Registrar subclasses por nome."""
Evento._subclasses[cls."_nome"_] = cls
Machine Translated by Google

@classmethod
def subclass_for_name(cls, name: str) "- Digite: """Obtenha a
subclasse do evento para desserializar."""
retornar cls._subclasses[nome]

def as_dict(self) "- dict: “""Despeja


para ditar para serialização."""
dict_repr = asdict(self)
dict_repr.pop("criado_at")
dict_repr.pop("versão") return
dict_repr

@dataclass(frozen=True) classe
OrderDrafted(Evento):
ID_do_cliente: IDDoCliente

@dataclass(frozen=True) classe
OrderConfirmed(Evento):
passar

ESTES NÃO SÃO EVENTOS DE DOMÍNIO QUE VIMOS ANTES!

Uma semelhança entre eventos de Event Sourcing e eventos de domínio usados principalmente para fins
de integração nos capítulos anteriores é visível a olho nu. Ambos representam um fato significativo dentro de
um domínio.

No entanto, eles não são iguais e não devem ser usados de forma intercambiável! Os eventos do
Event Sourcing nunca devem cruzar os limites de um módulo que contém um agregado
emitindo esses eventos. Os Eventos de Domínio podem ser usados externamente, publicados e usados
para integração. Eventos de Event Sourcing absolutamente não. Este último é mais parecido com detalhes
de persistência e não deve ser usado fora do módulo.

Para obter mais detalhes, leia Por que Event Sourcing é um antipadrão de comunicação de microsserviços
de Olivier Libutzki https://dev.to/olibutzki/why-event-sourcing-is-a-microservice-anti pattern-3mcj

Num exemplo tão simples, existem apenas dois eventos. Observe que sua nomenclatura é tão específica quanto
possível para pedidos e familiar para um especialista no domínio. Deve-se evitar a criação de classes de eventos
gerais para poupar pressionamentos de teclas como StatusChanged com um campo de status. O primeiro evento,
Machine Translated by Google

OrderDrafted é uma maneira padrão de iniciar qualquer fluxo de eventos para Order. O segundo evento,
OrderConfirmed, representa um ato de confirmação do Pedido. Essas classes de eventos carregam apenas a
informação necessária para reconstruir o estado de um agregado. Esta é uma diferença significativa entre
eventos de Event Sourcing e eventos de domínio usados para integração. Para as seguintes chamadas de
pedido :

# instância criada usando @classmethod servindo como fábrica


pedido = Pedido.draft(uuid4(), customer_id=1)
pedido.confirm()

…um fluxo de eventos correspondente tem esta aparência:

event_stream =
[Pedido elaborado(customer_id=1, criado_at="".),
PedidoConfirmado(criado_at="".),
]

PEDIR COMO AGREGADO

No final, deveremos ser capazes de carregar o estado do Pedido usando esses eventos. Lá se vai uma versão reescrita
da classe Order que é um agregado ES completo. Ele aceitará uma instância de

EventStream como argumento:

@dataclass(frozen=True)
classe EventStream:
uuid: eventos
UUID: Lista[Evento]
versão: int
Machine Translated by Google

Esta é uma estrutura de dados simples com UUID do Aggregate, uma lista de eventos passados
para reconstruir o estado e uma versão do Aggregate que será útil um pouco mais tarde.

Ordem da classe :
def _init_(
self, event_stream: EventStream) "-
Nenhum: # 1
self._uuid = event_stream.uuid
self._version = event_stream.version

self._customer_id = 0 # 2
self._status = OrderStatus.NEW
self._lines: Dict[ProductId, int] = {}

para evento em event_stream.events: # 3


self._apply(evento)

self._new_events = [] #4

@propriedade
def uuid(self):
retornar self._uuid

@propriedade
def _next_version(self):
"""Útil para criação de eventos"""

retornar self._version + 1

@propriedade
def alterações(self) "- AggregateChanges: # 5
retornar

AggregateChanges( self._uuid,
self._new_events[:], self._version,
)

def _apply(self, evento: Evento) "- Nenhum: # 6


if isinstance(evento, OrderDrafted):
self._customer_id = event.customer_id
self._status = OrderStatus.NEW elif
isinstance(evento, PedidoConfirmado):
self._status = OrderStatus.CONFIRMED
outro:
raise
ValueError( f"Evento desconhecido {evento}"
)
Machine Translated by Google

@classmethod
def draft( cls,
uuid: UUID, customer_id: CustomerId
) "- "Ordem":
instance = cls(

EventStream( uuid=uuid, versão=0, eventos=[]


)

) instância._new_events = [

OrderDrafted( datetime.now(tz=pytz.UTC),
0,
customer_id,
)
]
instância de retorno

def confirm(self) "- Nenhum: # 7


if self._status "! OrderStatus.NEW:
raise IllegalStatusChange( "Apenas
NOVO pedido pode ser confirmado!"
)

evento = PedidoConfirmado
( datetime.now (tz = pytz.UTC),
self._next_version,) # 8

self._apply(evento)
self._new_events.append(evento) # 9

1. Para instanciar o pedido , é necessário fornecer uma lista de eventos com os quais ele deve ser inicializado

2. Antes de inicializar a classe, às vezes podemos precisar definir valores padrão para os campos sabendo
que eles serão substituídos

3. Cada evento é aplicado, causando mutação de estado

4. O Aggregate manterá novos eventos criados após a inicialização devido a chamadas de métodos...

5. … para ser obtido posteriormente para ser salvo. Aqui, usamos um truque de cópia @property + list para garantir
que ninguém irá interferir nas alterações registradas de fora

6. O coração de todo Event Sourcing Aggregate é o método _apply . Dentro dele mudamos de estado.
A implementação na vida real não deve usar chamadas if-elif + isinstance. Isso ficaria muito melhor em
uma linguagem que suporta sobrecarga de métodos polimórficos.
Machine Translated by Google

7. Finalmente, um método público responsável pela lógica comercial de confirmação


pedidos

8. Em vez de alterar diretamente o estado, criamos um evento e o aplicamos imediatamente

9. Logo depois, anexamos a _new_events.

TESTE DE AGREGADOS

Testar agregados de fornecimento de eventos é trivial. A etapa Organizar (dado) se resume


a instanciar um agregado com determinados eventos. A etapa Act (When) envolve chamar um
método público no agregado enquanto Assert (Then) verifica eventos esperados ou exceções:

@freeze_time("14/01/2019") def
test_order_newly_created_confirmation_changes_status(self):
agora = datetime.now(tz=pytz.UTC)
pedido =

Pedido( EventStream( uuid=uuid4(), events=[OrderDrafted(customer_id=1,


criado_at=now)], versão=1
)
)

pedido.confirm()

afirmar order.changes.events "= [OrderConfirmed(now)]

@freeze_time("14/01/2019") def
test_order_newly_created_cannot_be_confirmed_twice(self):
agora = datetime.now (tz = pytz.UTC)
pedido = Pedido
( EventStream
( uuid = uuid4
(), eventos
= [ OrderDrafted (customer_id = 1, criado_at = agora),
OrderConfirmed (created_at = agora)],
versão = 1
)
)

com self.assertRaises (IllegalStatusChange):


pedido.confirm()
Machine Translated by Google

PERSISTÊNCIA NA FONTE DE EVENTOS

TRANSMISSÕES DE EVENTOS APENAS PARA ANEXOS

Como os eventos são cidadãos de primeira classe necessários para reconstruir o estado de um agregado, precisamos
ser capazes de recuperá-los usando o ID agregado e acrescentar novos ao evento existente
fluxo.

Essas funcionalidades poderiam ser fornecidas por uma classe herdada de uma classe abstrata:

classe EventStore(abc.ABC):
@abc.abstractmethod
def load_stream( self,
agregado_uuid: UUID ) "-
EventStream:
passar

@abc.abstractmethod
def append_to_stream(
próprio, alterações: AggregateChanges
) "- Nenhum:
passar

O método load_stream retorna a instância EventStream - estrutura de dados simples com uma lista de eventos

necessários para a inicialização do Aggregate , UUID e versão atual:

@dataclass(frozen=True)
classe EventStream:
uuid: eventos
UUID: Lista[Evento]
versão: int

Usaremos version para proteção contra atualizações simultâneas usando bloqueio otimista. O método

append_to_stream aceita AggregateChanges - outra estrutura de dados simples:

@dataclass(frozen=True)
classe AggregateChanges:
agregado_uuid: eventos
UUID: Lista[Evento]
versão_esperada: int

Consiste no UUID do Aggregate , na versão esperada (o mesmo valor que obtivemos de load_stream) e em

uma lista de eventos que nosso Aggregate produziu. Observe que não precisamos armazenar eventos antigos, apenas

novos. Isso é possível porque não temos permissão para excluir nenhum evento, portanto, esta é uma estrutura

somente para acréscimos.


Machine Translated by Google

O parâmetro esperado_version mencionado acima serve para proteção contra atualizações simultâneas. Se tal situação

ocorrer, este método deverá gerar uma exceção:

classe ConcurrentStreamWriteError (RuntimeError):


passar

Os detalhes da implementação dependem do mecanismo de banco de dados escolhido.

Outra questão crítica: qual banco de dados deve ser usado? Algumas pessoas afirmam que quase qualquer um está bem. Não acho

que seja uma resposta válida. Um banco de dados transacional como o MySQL é uma boa escolha?

Em que prestar atenção? Para responder precisamente a esta questão, é preciso considerar a natureza do fluxo de eventos e como

eles são usados para reconstruir agregados.

ESTRATÉGIA DE RECUPERAÇÃO

Para reconstruir um agregado, precisamos de todos os eventos que já foram emitidos por ele. Nossos agregados geralmente terão

um ID exclusivo, em particular UUID. Em outras palavras, deveríamos ser capazes de consultar nosso armazenamento de

eventos pelo ID do Aggregate . Esse requisito pode ser facilmente atendido por muitos mecanismos de banco de dados que variam

substancialmente.

Redis

Este é um banco de dados de estrutura de dados popular que pode armazenar dados em algumas estruturas úteis, como:

• cordas

• listas

• conjuntos

• conjuntos classificados

• hashes (também conhecidos como dicionários ou mapas)

Supondo que Redis seja nossa escolha, armazenaríamos eventos usando listas e usaríamos UUID Aggregate como parte de um

nome de chave. Para recuperar todos os eventos para um determinado UUID, usaríamos o seguinte

consulta:

LRANGE event_stream_f42d9a33-81da-45ba-a066-32de5e747067 0 -1
Machine Translated by Google

AVISO: A implementação de listas do Redis faz com que essas consultas diminuam o desempenho
52
linearmente com um aumento de um número de eventos para um determinado Aggregate . Isso significa que o
Redis não é a escolha ideal.

RDBMS (PostgreSQL, MySQL, etc.)

Uma maneira natural de modelar o fluxo de eventos usando RDBMS é criar uma tabela para todos eles.
Apesar de armazenarmos muitos tipos de eventos com campos diferentes, não é viável criar uma tabela
separada para cada um. Em primeiro lugar, o desempenho da consulta será prejudicado.
Em segundo lugar, tornará as consultas mais complicadas de implementar e manter. Em terceiro lugar, os
acontecimentos podem evoluir ao longo do tempo, incluindo dados adicionais. É claro que não podemos
mudar eventos do passado, mas de alguma forma teríamos que armazenar novos eventos com estrutura
alterada ao lado dos antigos. Portanto, manter o design de mesa única para todos os eventos é a coisa certa a fazer.

eventos
-------------------------------------------------- -----

| uuid | agregado_uuid | nome | dados | <sort_column> |


-------------------------------------------------- -----

Por outro lado, podemos considerar a criação de uma tabela separada para cada tipo de agregado para obter
fragmentos lógicos de dados. Devido à natureza dos agregados (eles são separados uns dos outros), também
é possível fragmentar por agregado_uuid.

Cada evento também possui seu próprio uuid. agregado_uuid é uma coluna que nos permite consultá-la
facilmente. Deveríamos colocar um índice nisso. o nome é autoexplicativo (por exemplo, OrderConfirmed).
Depois temos uma parte flexível – os dados. Armazenaremos detalhes codificados em JSON dentro dele.
Dependendo do mecanismo de banco de dados escolhido, usaríamos um tipo de dados dedicado (o
PostgreSQL suporta colunas JSON) ou simplesmente TEXT. Finalmente, precisamos de uma coluna para
classificar. Dependendo do design escolhido, pode ser carimbo de data e hora criado_at ou versão. Pelo
menos um deles deve ser atribuído a eventos quando são criados em código.

A consulta é trivial:

SELECIONE * DE eventos
ONDE agregado_uuid = 'f42d9a33-81da-45ba-a066-32de5e747067'
ORDER BY criado_em;

52
Documentação do comando Redis LRANGE https://redis.io/commands/lrange
Machine Translated by Google

Orientado a documentos (MongoDB, RethinkDB etc)

Analogamente ao RDBMS, deveríamos ser capazes de usar uma única coleção (MongoDB) ou uma tabela (RethinkDB) para

recuperar todos os eventos. Ao lado da parte JSON dos dados, também adicionaríamos agregate_uuid e

colocaríamos um índice nele para leituras mais rápidas.

A consulta também é muito simples:

# MongoDB
db.events.find(
{agregado_uuid: 'f42d9a33-81da-45ba-a066-32de5e747067'}
)

#RepensarDB
r.table('eventos').get_all(
'f42d9a33-81da-45ba-a066-32de5e747067', índice='agregado_uuid'
).correr()

ESTRATÉGIA DE ARMAZENAMENTO

Como já mencionei, nunca excluímos eventos. O Event Sourcing não limita o número de eventos por agregado, portanto

devemos estar preparados para que nossa tabela/coleção de eventos cresça indefinidamente. Portanto, precisamos de um

banco de dados que seja capaz de escalar e manter tempos aproximados de leitura/gravação, independentemente do

número de eventos (até certo ponto, é claro).

Os eventos são a nossa fonte de verdade, por isso não podemos permitir nenhuma perda de dados. Portanto, precisamos de um

banco de dados com fortes garantias de consistência.

O Event Sourcing pressupõe que apenas um agregado deve ser salvo em uma operação comercial. Salvar um único

agregado deve ser atômico. Não precisamos ter uma garantia completa de tudo ou nada, como acontece com bancos de dados

SQL relacionais que abrangem toda a solicitação HTTP ou qualquer outra coisa. Precisamos apenas ter certeza de que,

assim que tentarmos salvar as alterações no Aggregate e aumentar sua versão, será uma operação atômica.

A proteção contra atualizações simultâneas não é algo que eu possa ignorar. Francamente, acho bizarro que a maioria dos artigos

sobre implementação de Event Sourcing não diga uma palavra sobre esses problemas e possíveis soluções. Uma abordagem

READ - MODIFY - WRITE comumente usada é um convite para condições de corrida. Seria ótimo se um mecanismo de banco

de dados fornecesse meios para implementar um relógio otimista para evitar que coisas ruins acontecessem. É claro que poderíamos

contornar esse problema usando bloqueio pessimista e Redis, mas isso dificulta a implementação.
Machine Translated by Google

RESUMO DOS REQUISITOS

• Acesso eficiente a todos os eventos de um determinado agregado

• Fortes garantias de consistência

• Possibilidade de implementar bloqueio otimista/proteção diferente sem


Serviços

• É bom ter: habilite a escalabilidade horizontal por meio de fragmentação/particionamento

EXEMPLO DE IMPLEMENTAÇÃO USANDO POSTGRESQL

Escolher o PostgreSQL como arma preferida não deve ser uma surpresa. Uma solução de código aberto madura e
comprovada em batalha pode ser uma base perfeita para uma loja de eventos. Eventos de tabela única para
todos são uma boa abordagem para fornecer leituras e gravações eficientes. Ele pode ser dimensionado usando
particionamento ou fragmentação de tabela (mais ou menos manual, mas ainda assim – ao alcance). O
PostgreSQL possui fortes garantias de consistência, portanto esse requisito também é atendido. A
proteção contra atualizações simultâneas usando bloqueio otimista é bastante fácil de implementar. Parece uma
escolha perfeita!

Desenho de mesa

Não mudou muita coisa no design da mesa de eventos. Temos as colunascreate_at e version , embora para classificá-
las a versão seja usada:

eventos
-------------------------------------------------- ------------

| uuid | agregado_uuid | nome | dados | criado_em | versão |


-------------------------------------------------- ------------

Entretanto, para obter proteção simples contra atualizações simultâneas, usaremos uma tabela extra:

agregados
------------------

| uuid | versão |
------------------
Machine Translated by Google

Uma versão será aumentada em um sempre que tivermos alguns eventos para salvar. Usando condições
adicionais na consulta UPDATE e um número retornado de linhas afetadas, podemos facilmente saber se
vencemos a corrida ou não:

"- 1 - versão esperada


ATUALIZAR "agregados"
DEFINIR versão = 2
ONDE "agregado_uuid" = 'f42d9a33-81da-45ba-a066-32de5e747067'
AND "versão" = 1 # verificação de versão esperada

Se esta consulta retornar a contagem de linhas afetadas igual a 1 – estamos prontos para prosseguir. Caso
contrário, significa que entretanto alguém mudou a história e devemos levantar a questão
ConcurrentStreamWriteError.

Código para criar ambas as tabelas:

CREATE TABLE "agregados" (


uuid UUID CHAVE PRIMÁRIA NÃO NULA,
versão int NÃO NULO PADRÃO 1
);

CRIAR TABELA "eventos" (


uuid UUID CHAVE PRIMÁRIA NÃO NULA,
agregado_uuid UUID NÃO NULO,
nome VARCHAR(50) NÃO NULO,
dados JSON,
carimbo de data / hora criado_at com fuso horário NOT NULL, versão int NOT
NULL
);

"- Não se esqueça do índice!


CRIAR ÍNDICE agregado_uuid_idx EM "eventos" (“agregado_uuid”, “versão”);

Observe que não há restrições de chave estrangeira presentes. Eles foram omitidos propositalmente como forma de
otimização.

Existem inúmeras maneiras de transferir dados dessas tabelas para Python. Um deles está usando um ORM. O
mapeamento no SQLAlchemy pode ser assim:

classe AggregateModel (Base):


"_tablename"_ = "agregados"

uuid = Coluna
(postgresql.UUID, primária_key=True

) versão = Coluna (BigInteger, padrão = 1)


Machine Translated by Google

classe EventModel(Base, EventMixin):


"_tablename"_ = "eventos"
"_tabela_args"_ = (

Índice("ix_events_gregate_version", "agregado_uuid",
"versão",

),

) uuid = Coluna
(postgresql.UUID, primária_key=True

) agregado_uuid =

Coluna( postgresql.UUID, ForeignKey("agregados.uuid"),

) nome = Coluna (VARCHAR (50))


dados = Coluna (postgresql.JSON, anulável = True)
criado_at = Coluna (DateTime (fuso horário = True))
versão = Coluna (BigInteger)

agregado = relacionamento (
AggregateModel,
uselist=False,
backref="events",
)

Implementação da Loja de Eventos

Baseando-se neste código, podemos implementar o método load_stream do PostgreSQLEventStore:

classe PostgreSQLEventStore(EventStore):
def _init_(self, session: Session) "- None: # contamos com
SQLAlchemy, então precisamos que Session seja passada para uso futuro
self._session = sessão
Machine Translated by Google

def load_stream(self,
agregado_uuid: UUID
) "- EventStream:
events_query: Lista[
Modelo de Evento

]=

self._session.query( EventModel ).filter( EventModel.agregado_uuid "= str(agregado_uuid) ).order_by( EventMode


).todos()

se não eventos:
aumentar NotFound

agregado_uuid = eventos[0].agregado_uuid
agregada_versão = eventos[-1].versão
events_objects =
[ self._to_event_object(model) para
modelo em eventos
]

retornar EventStream
(agregado_uuid,
eventos_objetos,
agregada_versão,
)

def _to_event_object( self,


event_model: EventModel
) "- Evento:
event_cls = Event.subclass_for_name(
modelo_evento.nome

) retorna
event_cls(criado_at=event_model.created_at,
version=event_model.version,
"*event_model.data
)

A única parte mágica está no método _to_event_object. Isso usa o método de classe Event que mantém todas
as subclasses conhecidas em um dicionário interno. Depois de obter a classe correta pelo nome, podemos
facilmente reconstruí-la usando a descompactação com asterisco duplo.
Machine Translated by Google

Deve-se considerar o abandono de recursos ricos de ORM em favor de um núcleo SQLAlchemy mais leve ou
mesmo de consultas brutas para obter algum aumento de desempenho. ORM não agrega muito valor aqui e é
definitivamente menos eficiente que outros métodos.

Como estamos implementando o bloqueio otimista, o append_to_stream é mais complicado de implementar:

classe PostgreSQLEventStore(EventStore):
"".

def anexar_to_stream(
self,
alterações: AggregateChanges, ) "-
Nenhum: se
não alterações.events:
aumentar NoEventsToAppend

se alterações.expected_version:
self._perform_update(mudanças) senão:

self._perform_create(mudanças)

self._insert_events(mudanças)
Machine Translated by Google

def _perform_update( self,


alterações: AggregateChanges ) "-
Nenhum:
stmt =

( AggregateModel."_table"_.update() .values( version=changes.expected_version + 1

) .onde( (

AggregateModel.versão "=
alterações.expected_version

)&(
AggregateModel.uuid "=
alterações.agregado_uuid
)
)

) conexão = self._session.connection() resultado =


connection.execute (stmt)

se resultado.rowcount "! 1:
# bloqueio otimista falhou
aumentar ConcurrentStreamWriteError

def _perform_create( self,


alterações: AggregateChanges ) "-
Nenhum:
stmt =

AggregateModel."_table"_.insert().values( uuid=str(changes.gregate_uuid), versão=1,

) self._session.connection().execute(stmt)
Machine Translated by Google

def _insert_events( self,


alterações: AggregateChanges ) "-
Nenhum:
connection = self._session.connection() para
evento em alterações.events:

connection.execute( EventModel."_table"_.insert().values( uuid=str( uuid4()), agregado_uuid=str( alteraç


),
nome=evento."_class"_."_nome"_,
data=event.as_dict(),
criado_at=event.created_at,
versão=event.versão,
)
)

O método funciona em um dos dois modos. Se for o primeiro salvamento de um agregado, então inserimos na tabela de agregados .

Caso contrário, incrementamos a versão em um usando UPDATE condicional. Pode-se dizer, a partir de uma série de

linhas atualizadas, se alguém não atualizou a agregação nesse meio tempo e, em caso afirmativo, aumente

ConcurrentStreamWriteError.

Armazenamento de eventos baseado em PostgreSQL em ação

Sempre que você quiser carregar um agregado do Event Store você precisará do seu UUID:

event_store = PostgreSQLEventStore(sessão)
event_stream = event_store.load_stream(
UUID (“36b89a56-cbf1-45f2-9f94-b7958481e3d1”)

) pedido = Pedido (event_stream)

... então você chama os métodos necessários ...

pedido.confirm()

... e, finalmente, salve as alterações agregadas em um fluxo somente de acréscimo:

event_store.append_to_stream(order.changes)

O que fazer quando ConcurrentStreamWriteError é gerado?

Depende dos requisitos e do cenário do negócio. No caso de uma instância de Pedido , podemos imaginar que alguém aceita o

pedido e outra pessoa tenta cancelá-lo simultaneamente.

Poderíamos tentar buscar agregado novamente e resolver o conflito manualmente, mas é muito mais simples
Machine Translated by Google

(e na maioria dos casos suficiente!) abordagem é apenas tentar novamente toda a operação que afeta nosso agregado.

Este é um exemplo usando a biblioteca de novas tentativas do Python: 53

@retry( retry_on_exception=lambda exc: isinstance(


exc, ConcurrentStreamWriteError
)

) def
cancel_order( event_store: EventStore, order_uuid:
UUID ) "-
Nenhum: event_stream =
event_store.load_stream( order_uuid

) pedido = Pedido (event_stream)


order.cancel()
event_store.append_to_stream (order.changes)

Suponha que haja uma condição de corrida entre a definição de dois status, cancelado e confirmado. Se o último vencer, o código acima

gerará ConcurrentStreamWriteError durante a execução de event_store.append_to_stream. O decorador @retry se encarregará de

executar tudo novamente e recarregar todo o Aggregate com a versão mais recente. Desde que não haja mais atualizações simultâneas,

finalmente poderemos cancelar nosso pedido recém-confirmado OU levantar outra exceção se nossas regras de negócios não permitirem o

cancelamento de um pedido confirmado. É crucial ter um número limitado de tentativas e não tentar novamente indefinidamente. Em sistemas

altamente simultâneos, isso pode levar a corridas de repetição desagradáveis e de longa duração.

Escondendo o armazenamento de eventos atrás de um repositório

Embora o Event Store em si seja uma abstração, podemos ocultá-lo ainda mais usando um método familiar

Padrão de repositório e tratamento do Order Aggregate como uma Entidade sem saber que ele usa Evento

Abastecimento:

classe PedidosRepositório:
def _init_(
self, event_store: EventStore) "-
Nenhum:
self._event_store = event_store

53 tentando novamente a biblioteca https://pypi.python.org/pypi/retrying


Machine Translated by Google

def get(self, agregado_uuid: UUID) "- Ordem:


event_stream = self._event_store.load_stream( agregado_uuid

) retornar Pedido (event_stream)

def save(self, pedido: Pedido) "- Nenhum:


self._event_store.append_to_stream( pedido.mudanças

INSTANTÂNEOS

Toda a ideia de reconstruir o estado de um agregado a partir de um fluxo de eventos potencialmente muito longo pode

parecer arriscada do ponto de vista do desempenho. No caso do Order Aggregate, é bastante improvável que isso seja

um problema real, uma vez que os pedidos têm vida relativamente curta (minutos, horas, talvez dias), portanto seu fluxo

de eventos não deve exceder a duração de vários eventos.

No caso de agregados de vida mais longa e com história cada vez maior, o problema pode ser muito mais sério.

Considere a conta bancária. Embora a sua interface pública tenha apenas dois métodos principais – depósito e retirada,

pode haver dezenas de milhares deles. A solução é usar instantâneos de estado - estruturas de dados semelhantes a

eventos que podem ser usadas para carregar o estado de um sem ter que iterar sobre eventos antigos. Não removemos

o último, apenas usamos snapshots para limitar o número de eventos necessários para reconstruir o estado de

um agregado.

Tirar um snapshot é tão simples quanto copiar um estado interno de um Aggregate para uma classe de dados
semelhante a um evento designada:

@dataclass(frozen=True)
classe OrderSnapshot(Evento):
customer_id: Status do
CustomerId: OrderStatus
linhas: Dict[ProductId, int]
Machine Translated by Google

Ordem da classe :
"".

def take_snapshot(self) "- OrderSnapshot:


if self._new_events: # para
criação de snapshot após
#execução do comando
versão = self._next_version senão:

# para criação de snapshot em segundo plano


versão = self._versão

retornar OrderSnapshot
( datetime.now (tz = pytz.UTC), versão,

self._customer_id,
self._status,
self._lines.copy(),
)

Restaurar um estado usando um snapshot é tão simples quanto aplicar eventos. Um estende _apply

método do agregado:

Ordem da classe :
"".

def _apply(self, evento: Evento) "- Nenhum: # 6


"".

elif isinstance( event,


OrderSnapshot ): # não está no
primeiro trecho!
self._customer_id = event.customer_id self._status =
event.status self._lines = event.lines.copy()
senão:

raise ValueError( f"Evento


desconhecido {evento}"
)

Naturalmente, os instantâneos precisam ser mantidos em algum lugar. Potencialmente, podemos mantê-
los na mesma tabela, mas devido a certas vantagens de implementação e à natureza diferente dos
instantâneos (eles podem ser removidos/regenerados enquanto os eventos são imutáveis), preferiríamos
usar uma tabela separada para instantâneos. No entanto, será idêntico à tabela de eventos .

A próxima etapa é estender a lógica de busca do fluxo de eventos para o UUID de um determinado
agregado no Event Store. O algoritmo é bastante simples - primeiro, procura-se o instantâneo mais recente. Se lá
Machine Translated by Google

é nenhum, busca-se todos os eventos existentes. Caso contrário, buscaremos apenas eventos mais recentes
que nosso snapshot + o próprio snapshot.

classe PostgreSQLEventStore(EventStore):
"".

def load_stream( self,


agregado_uuid: UUID
) "- EventStream:
events_query: Lista[
Modelo de Evento

]=

self._session.query( EventModel ).filter( EventModel.agregado_uuid "= str(agregado_uuid) ).order_by( EventM


)

tente: last_snapshot =
( self._session.query (SnapshotModel) .filter

( SnapshotModel. agregado_uuid "= str


(agregado_uuid)

) .order_by( SnapshotModel.versão.desc()

) .limit(1) .um()
)
exceto exc.NoResultFound:
events = events_query.all()
outro:
# para que isso funcione, o snapshot precisa
# ser criado APÓS o último comando

newer_events =
events_query.filter( EventModel.version
> último_instantâneo.versão

) eventos =
[instantâneo_mais
recente] + eventos_mais recentes.all()

se não eventos:
aumentar NotFound
Machine Translated by Google

agregado_uuid = eventos[0].agregado_uuid
agregada_versão = eventos[-1].versão
events_objects =
[ self._to_event_object(model) para
modelo em eventos
]

retornar EventStream
(agregado_uuid,
eventos_objetos,
agregada_versão,
)

Neste exemplo simples, os instantâneos foram mantidos na mesma tabela dos eventos. Ao contrário
dos eventos, os instantâneos podem ser excluídos. Na verdade, são totalmente descartáveis, pois
sempre se pode levar outro. Portanto, faria sentido mantê-los em uma tabela separada e manter apenas
um, o instantâneo mais recente de qualquer agregado.

A única questão restante é quando e como criar instantâneos? Existem várias opções. Pode-se
gerá-los de forma assíncrona, procurando agregados que tenham mais de N eventos mais recentes que
seu instantâneo mais recente. Outra opção é colocar a lógica do snapshot em um Repositório
ou Event Store. Neste último caso, teríamos que alterar a assinatura do método append_to_stream ,
para que ele aceite um agregado inteiro.

Por exemplo, podemos tirar um instantâneo de um agregado sempre que a versão for uma multiplicação
de 100:

classe PedidosRepositório:
"".

def save(self, order: Order) "- Nenhum: alterações


= order.changes
self._event_store.append_to_stream( order.changes

) se alterações.expected_version % 100 "= 0:


self._event_store.save_snapshot (
pedido.uuid, pedido.take_snapshot()
)
Machine Translated by Google

O snippet acima usa um novo método de EventStore que armazenará nosso instantâneo:

classe EventStore(abc.ABC):
"".

@abc.abstractmethod def
save_snapshot( self,

agregado_uuid: UUID,
instantâneo: Evento, )
"- Nenhum:
passar

A implementação concreta correspondente é bastante simples; acabamos de salvar nosso evento especial
subclasse para uma tabela designada:

classe PostgreSQLEventStore(EventStore):
"".

def save_snapshot(self,

agregado_uuid: UUID,
instantâneo: Evento,)
"- Nenhum:

self._session.connection().execute(SnapshotModel."_table"_.insert().values(
uuid = str (uuid4 ()),
agregado_uuid = str
(agregado_uuid
),
name=instantâneo."_class"_."_name"_,
data=instantâneo.as_dict(),
criado_at=instantâneo.created_at,
versão=instantâneo.versão,
)
)

Esteja ciente de que esta implementação de criação e armazenamento de snapshots faz certas
suposições. Por exemplo, a versão no snapshot mais recente é sempre igual à versão do evento mais recente em
AggregateChanges. Isso também se reflete no código do método load_stream, que precisa ter uma maneira de

ordenar instantâneos e eventos corretamente para restaurar com êxito o estado do Aggregate.
Machine Translated by Google

PROJEÇÕES

Mesmo que ter um histórico completo possa ser inestimável, consultar dados ou exibir qualquer coisa para um

usuário final seria um pesadelo se tudo o que tivéssemos fossem eventos simples. Como este último é uma fonte de

verdade, podemos usá-los para gerar modelos de leitura personalizados – as chamadas projeções. Uma projeção é

um conceito super simples – pode ser tão simples quanto uma função que pega um fluxo de eventos e produz um

documento que deve ser exibido.

Primeiro, vamos imaginar que temos a seguinte projeção de nossos agregados de pedidos:

classe OrderProjection (Base):


"_tablename"_ = "projection_orders"
uuid = Coluna
(postgresql.UUID, primária_key=True

) versão = Coluna (BigInteger)

customer_id = Coluna (Inteiro) status =


Coluna (VARCHAR (64)) total_quantity =
Coluna (Inteiro) linhas = Coluna
(postgresql.JSON) atualizado_at = Coluna
(DateTime (timezone = True))

Então, precisamos de uma função de projeção real que irá iterar sobre os eventos do fluxo e criar/atualizar adequadamente

a visualização nivelada do nosso agregado:

def project_order (sessão:


Sessão, agregado_uuid:
UUID, eventos: Lista [Evento],)
"- Nenhum: def

_update_version_and_ts (projeção:
OrderProjection, evento: Evento) "- Nenhum: projeção.versão
= evento.versão
projeção.atualizado_at = evento .criado em

@singledispatch
def project(evento: Evento) "- Nenhum: raise
ValueError( f"Evento
desconhecido: {type(event)}"
)
Machine Translated by Google

@project.register def
_(evento: OrderDrafted) "- Nenhum: projeção
= OrderProjection( uuid=str(agregado_uuid),
customer_id=event.customer_id,
status="NEW", total_quantity=0,
linhas={},

) _update_version_and_ts(projeção, evento)
session.add(projeção)
session.flush()

@project.register def
_(evento: OrderConfirmed) "- Nenhum:
projeção = session.query(

OrderProjection .get(str(agregado_uuid)) projeção.status = "CONFIRMADO"


_update_version_and_ts(projeção, evento)
session.flush()

@project.register def
_(evento: NewProductAdded) "- Nenhum:
projeção =

session.query(OrdemProjeção).get(str(agregado_uuid)) projeção.lines = { "*projeção.lines, "*{

str(evento.produto_id):
evento.quantidade
},

} projeção.quantidade_total +=
(evento.quantidade

) _update_version_and_ts(projeção, evento)
session.flush()
Machine Translated by Google

@project.register def

_( evento: ProductQuantityIncreased, ) "-


Nenhum:
projeção =

session.query( OrderProjection ).get(str(agregado_uuid)) linhas_idx = str(event.product_id) projeção.lines = { "*p


linhas_idx: projeção.lines[linhas_idx

] + evento.quantidade
},

} projeção.quantidade_total +=
(evento.quantidade

) _update_version_and_ts(projeção, evento)
session.flush()

para evento em eventos:


projeto(evento)

Esta implementação é o exemplo mais simples, fortemente acoplado ao SQLAlchemy Core. Poderíamos
também imaginar dividir isso em duas etapas. A primeira seria independente da tecnologia e produziria estruturas
de dados simples, como dicionários.

A segunda etapa seria específica da infraestrutura e salvaria as projeções calculadas em locais designados:

classe AccountBalance(TypedDict):
account_uuid: Saldo
UUID: Decimal
Machine Translated by Google

def project_account_balance(eventos:
Lista[Evento], ) "-
AccountBalance: resultado
=
{ "account_uuid": events[0].account_uuid, "balance":
Decimal("0.00"),

} para evento em eventos:


if isinstance(evento, CashDeposited):
resultado["saldo"] += event.amount elif
isinstance(evento, CashWithdrawn):
resultado["saldo"] -= evento.quantidade

resultado de retorno

Então, a contrapartida específica da infra-estrutura da projecção tem de persistir. Por exemplo, se alguém
usasse PostgreSQL, poderia usar UPSERT para atualizar o modelo de leitura convenientemente.

A vantagem de tal solução é que ela aumenta a testabilidade da unidade de um módulo no qual ela reside.
Supondo que o primeiro estágio (transformar eventos em estruturas de dados simples) seja algo
específico da aplicação, pode-se chamar Casos de Uso e testar se as projeções mudam conforme o esperado.

As projeções podem ser geradas dentro da mesma transação/processo, mas nada se opõe ao
processamento delas em segundo plano. Isso é ainda melhor em termos de escalabilidade, mas também
significa fazer amizade com a consistência eventual - os dados do modelo de leitura não são consistentes
com o modelo de gravação.

Por último, mas não menos importante, as projeções são descartáveis, assim como os instantâneos. Deveríamos
projetá-los de tal forma que fosse possível regenerá-los sem complicações extras.

FONTE DE EVENTOS VERSUS UMA APLICAÇÃO MODULAR

A FONTE DE EVENTOS É UM DETALHE PRIVADO DE UM MÓDULO


(INCLUINDO TESTE)

O conhecimento sobre o uso do Event Sourcing não deve vazar para fora de um módulo que o utiliza. Nenhum
código fora de um módulo deve saber sobre eventos de Event Sourcing. Além disso, deve-se ser capaz de
testar a unidade de tal módulo como um todo, sem inspecionar quais eventos ES foram gerados. Para
testes, temos praticamente as mesmas possibilidades do módulo não-ES - chamamos seus Casos de Uso e
verificamos o comportamento do módulo. A maior vantagem do módulo ES é que podemos tratar as projeções
como parte da API do módulo se apenas as dividirmos no processo de duas fases, conforme descrito alguns
parágrafos antes.
Machine Translated by Google

No entanto, ainda podemos verificar eventos quando escrevemos testes no nível de agregados.

PRECISA DE INTEGRAÇÃO? USE EVENTOS DE DOMÍNIO AO LADO DE EVENTOS DE FONTE DE EVENTOS

Os Eventos de Domínio podem ser usados em combinação com módulos de Event Sourcing. Se houver um
evento que deva desencadear ação em outros módulos, poderemos usar Eventos de Domínio em nossos
Agregados como um acréscimo aos eventos ES. A implementação pode ser a mesma que para Entidades - Agregado
mantém eventos de domínio em seu campo (separadamente dos eventos ES!). Mais tarde, após salvar um
agregado, o Repositório coleta eventos de domínio e os publica via Event Bus.
Machine Translated by Google

BIBLIOGRAFIA

Andrea Saltarello, Dino Esposito, Microsoft .NET: Arquitetura de aplicativos para empresas, segunda
edição

Andrew Hunt, David Thomas, The Pragmatic Programmer: sua jornada para a maestria, 20º
Edição de Aniversário, 2ª Edição

Bert Bates, Eric Freeman, Elisabeth Robson, Kathy Sierra, Head First Design Patterns

Bertrand Meyer, Construção de Software Orientado a Objetos

Brian Foote, Joseph Yoder, Grande Bola de Lama http://www.laputan.org/mud/


lama.html#BigBallOfMud

Eric Evans, Design Orientado a Domínio: Enfrentando a Complexidade no Coração do Software

Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Padrões de Design: Elementos de
Software Orientado a Objetos Reutilizável

Fred Brooks, O Mítico Homem-Mês

Gerard Meszaros, Padrões de Teste xUnit: Refatorando Código de Teste 1ª Edição

Gregor Hohpe, Padrões de Integração Empresarial: Projetando, Construindo e Implantando


Soluções de mensagens

Herberto Graça, As Crônicas da Arquitetura de Software https://herbertograca.com/category/


desenvolvimento/série/crônicas-da-arquitetura-de-software/

Jorge Herrera, microlibs Python https://medium.com/@jherreras/python


microlibs-5be9461ad979

Joshua Bloch, Java Efetivo 3ª Edição


Machine Translated by Google

Kent Beck, Desenvolvimento Orientado a Testes: Por Exemplo

Kirk Knoernschild, Arquitetura de aplicativos Java: padrões de modularidade com exemplos usando
OSGi

Nat Pryce, Steve Freeman, Crescendo Software Orientado a Objetos, Guiado por Testes

Mark Seemann, Service Locator é um antipadrão https://blog.ploeh.dk/2010/02/03/


ServiçoLocalizadoranAntipadrão/

Martin Fowler, Ramificação por Abstração https://martinfowler.com/bliki/


BranchByAbstraction.html

Martin Fowler, Padrões de Arquitetura de Aplicativos Corporativos

Martin Fowler, Refatoração: Melhorando o Design do Código Existente 2ª edição

Mary Poppendieck, Tom Poppendieck, Liderando o desenvolvimento de software enxuto: os resultados não são o
ponto

Matthias Noback, Guia de estilo de design de objetos

Miguel Grinberg, Flask Web Development, 2ª Edição

Mike Cohn, Sucesso com Agile: Desenvolvimento de Software Usando Scrum

Nick Chamberlain, Aplicando Design Orientado a Domínio com CQRS e Event Sourcing https://
buildplease.com/products/fpc/

Olivier Libutzki, Por que o Event Sourcing é um antipadrão de comunicação de microsserviços https://dev.to/
olibutzki/por que-event-sourcing-is-a-microservice-anti-pattern-3mcj
Machine Translated by Google

Robert C. Martin, Arquitetura Limpa: Guia do Artesão para Estrutura e Design de Software

Robert C. Martin, A Arquitetura Limpa https://blog.cleancoder.com/uncle-bob/


13/08/2012/the-clean-architecture.html

Sandi Metz, 99 garrafas de OOP: um guia prático para design orientado a objetos

Thomas J. McCabe, uma medida de complexidade http://www.literateprogramming.com/mccabe.pdf

Vaughn Vernon, Implementando Design Orientado a Domínio

Vaughn Vernon, Design Orientado a Domínio Destilado

Você também pode gostar