Escolar Documentos
Profissional Documentos
Cultura Documentos
A ARQUITETURA LIMPA
Machine Translated by Google
PREFÁCIO
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.
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
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.
"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
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.
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:
) preço_atual =
MoneyField( max_digits=19,
decimal_places=4, default_currency="USD",
)
MoneyField(max_digits=19,
decimal_places=4, default_currency="USD",
) licitante =
models.ForeignKey( get_user_model(), on_delete=models.PROTECT
)
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,
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
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
• 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 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
• 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
Numa forma básica da Arquitetura Limpa, existem quatro camadas. Naturalmente, pode-se usar mais se
for justificado.
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
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
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.
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/.
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:
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.
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
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.
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:
• 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
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
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):
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 (
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:
@abc.abstractmethod
def get_presented_data(self) "- ditado:
passar
Machine Translated by Google
classe ColocaçãoBidWebPresenter(
ColocandoBidOutputBoundary
):
definitivamente presente (
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
data_access: AuctionsDataAccess,
output_boundary: PlacingBidOutputBoundary,) "-
Nenhum:
self._data_access = data_access
self._output_boundary = output_boundary
)
leilão.place_bid( input_dto.bidder_id, input_dto.amount
) self._data_access.save(leilão)
output_dto =
) 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
) 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.
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
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,
@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:
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
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 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):
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í:
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:
) use_case_cls(presenter).execute( input_data
classe ColocaçãoBidJsonPresenter(
Limite de saída de índice
):
PRECISÃO = Decimal("0,01")
definitivamente presente (
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,
}
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.
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.
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
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.
place_bid( dto:
PlacingBidInputDto,
) "- Nenhum:
"".
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:
Durante a inicialização do aplicativo, o Command Bus é configurado para rotear comandos para casos de uso. Para sermos
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.
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:
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
• Sempre que alguém adiciona um novo produto ao carrinho, o número de itens aumenta
• 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
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.
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,
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 ;
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.
leilão_com_bids =
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
É 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
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
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
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.
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.
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?
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.
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:
12 Memcached https://memcached.org/
Machine Translated by Google
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.
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
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
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:
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
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.
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:
• 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
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
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
• 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
• todas as regras de negócios devem ser aplicadas durante sua execução para garantir que o
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:
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 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.
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
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
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.
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
Esta abordagem se assemelha à combinação Comando - Barramento de Comando - Manipulador de Comando com a diferença
@dataclass(frozen=True) classe
GetListOfDeliveryAddresses(Consulta):
ID_do_usuário: int
def query_handler(consulta:
GetListOfDeliveryAddresses,
) "- GetListOfDeliveryAddresses.Dto:
"".
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.
@abc.abstractmethod
def execute(self) "- Dto:
passar
# na camada de infraestrutura
classe SqlGettingListOfDeliveryAddresses(
ObtendoListOfDeliveryAddresses
):
def execute( self,
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
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
)
"".
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.
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.
command_bus.dispatch(UpdateEmailCommand(user_id=1, email="sebastian@cleanarchitecture.io",
)
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.
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
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
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.
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.
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.
É 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
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.
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.
•
representa valor, não um objeto de longa vida - não tem identidade
•
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
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
A moeda é necessária para criar a instância do Money. Como as classes são objetos em Python, poderíamos
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,
@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,
)
tente: quantidade_decimal = Decimal(quantidade)
exceto decimal.DecimalException: raise
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:
"".
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
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.
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.
• Como licitante quero dar uma licitação para poder ganhar o leilão.
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:
O limite de saída deve ser injetado durante a construção do PlacingBid, então estendemos seu __init__:
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,
def executar(
self, input_dto: PlacingBidInputDto ) "-
Nenhum:
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
Assim que identificamos um nome para um conceito no domínio que possa proteger as regras de negócios da
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
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.
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
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
)
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
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
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
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.
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
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"),
)
title="Livro incrível",
preço_inicial=Dinheiro(USD, "9,99"), lances=lances,
) repo = InMemoryAuctionsRepository()
repo.save (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)
classe InMemoryAuctionsRepository(
Repositório de Leilões
):
def _init_(self) "- Nenhum: self._storage:
Dict[ AuctionId, Leilão] = {}
copy.deepcopy( self._storage[auction_id]
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.
INJEÇÃO DE DEPENDÊNCIA
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.
output_boundary: PlacengBidOutputBoundary,
leilões_repo: AuctionsRepository, ) "-
Nenhum:
self._output_boundary = output_boundary
self._auctions_repo = leilões_repo
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
) repo = self._create_repo_with_auction()
self.use_case =
PlacingBid( self.output_boundary_mock, repo
)
def _create_repo_with_auction(self,
) repo.save(fresh_auction)
retornar repositório
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
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
importar atributo
@attr.s(auto_attribs=True) classe
PlacingBid:
_output_boundary: PlacingBidOutputBoundary _auctions_repo:
AuctionsRepository
def executar(
self, input_dto: ColocandoBidInputDto
) "- Nenhum:
"".
https://www.attrs.org/en/stable/ 29
Machine Translated by Google
CÓDIGO DE EMBALAGEM
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.
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
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.
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.
/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
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
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
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.
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
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:
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
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.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.
FlaskInjector(
aplicativo,
módulos=[LeilõesWeb()],
injetor=main.setup_dependency_injection(),
)
aplicativo de devolução
Machine Translated by Google
@auctions_blueprint.route( "/
<int:auction_id>/bids", métodos=["POST"]
) def
place_bid( leilão_id:
AuctionId, colocando_bid_uc:
PlacingBid, apresentador:
colocando_bid_uc.execute( get_input_dto( #1
ColocandoBidSchema,
) retornar apresentador.response # 4
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.
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:
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,
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
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
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.
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,
leilão =
Leilão( self.AUCTION_ID,
) self.repo.save(leilão)
Machine Translated by Google
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
Leilão de classe :
def _init_(
"".
termina_at: datahora,
) "- Nenhum:
"".
self.ends_at = ends_at
Observe que mostro apenas o novo código que foi adicionado à entidade.
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.
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…
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
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.
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.
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.
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 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 /
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"
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,
se não, response.ok:
raise PaymentFailedError # 4
outro:
# registra charge_uuid de pagamento no banco de dados, etc.
"".
Linhas interessantes:
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
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).
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
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
classe CaPaymentsPaymentProvider(PaymentProvider):
"".
) 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,
)
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())
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.
Solicitação de classe
@dataclass(frozen=True) classe
ChargeRequest(Solicitação):
card_token: str
moeda: st
quantidade: str
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:
) retornar card_details_model.card_token
Machine Translated by Google
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.
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]
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
) 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]}""."
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.
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.
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
Exemplo de uso:
detalhes de definição (
) lances
=
.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
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
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]
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.
detalhes de definição (
)
exceto ObjectDoesNotExist: raise
Http404( f"Leilão
#auction_id} não existe!"
)
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.
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:
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.
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.
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.
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
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
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:
"".
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.
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.
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[:]
self._record_event(#4
BidderHasBeenOverbid(self.id,
old_winner,
quantidade,
self.title,
)
)
Lugares interessantes:
3. O repositório usará este método para obter eventos pendentes. Ele retorna uma cópia de uma lista
classe SqlAlchemyAuctionsRepo(AuctionsRepository):
def _init_( self,
self._conn = conexão
self._event_bus = event_bus
Lugares interessantes:
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.
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:
"".
Então, a entidade que chama o caso de uso deve coletar eventos e emití-los usando o Event Bus:
classe ColocaçãoBid:
"".
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:
"".
se valor> self.current_price:
"".
rendimento WinningBidPlaced(self.id,
bidder_id, 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
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:
injetar.configure(di_config)
Machine Translated by Google
event_bus.subscribe( # 3
BidderHasBeenOverbid, evento
lambda: send_email.delay(evento.auction_id,
evento.bidder_id,
evento.money.amount,
),
)
Linhas interessantes:
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
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
32
Aipo: fila de tarefas distribuídas http://www.celeryproject.org/
Machine Translated by Google
leilão.place_bid(bidder_id=1,
valor=valor_vencedor
)
valor_vencedor,
leilão.título,
)
]
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. 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.
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
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
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:
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.
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.
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.
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:
injetar.configure(di_config)
Machine Translated by Google
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:
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
• 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
É 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
ATUALIZAÇÃO #3
leilão
DEFINIR
COMPROMETER-SE; # 4
Linhas interessantes:
1. Tudo está envolvido em uma transação, embora não nos proteja completamente
3. Esta declaração mantém nossas alterações, desde que ninguém altere a versão em
enquanto isso
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
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
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.
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
/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.
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.
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.
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.
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.
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
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
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.
) "- Nenhum:
"".
) "- Nenhum:
"".
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
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
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.
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
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.
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.
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.
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
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.
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.
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
• Eventos de Domínio
• DTOs de entrada
• Portos
"_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",
]
@injector.provider def
retirando_bids_uc( self, repo:
AuctionsRepository
) "- WithdrawingBids:
retornar WithdrawingBids(repo)
Machine Translated by Google
"_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).
"_todos"_ = [#
módulo
"Pagamentos",
"Configuração de Pagamentos",
# fachada
"PagamentosFacade",
# eventos
"Pagamento iniciado",
"Pagamento cobrado",
"Pagamento Capturado",
"Pagamento falhou",
]
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"
]
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:
@injector.singleton
@injector.provider def
customer_relationship_config( self, ) "-
CustomerRelationshipConfig:
return
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:
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:
@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 .
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.
TCommand = TypeVar("TCommand")
class CommandBus:
def _init_( self,
injector: Injector ) "- Nenhum:
self._injector
= injector
classe WithdrawBidHandler:
def _chamar_(
self, comando: WithdrawBid ) "-
Nenhum: #
manipulador fictício apenas imprime o que obtém
print(f"Manipulando {comando}!")
) "- Manipulador[RetirarBid]:
retornar WithdrawBidHandler()
# 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.
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.
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.
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.
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:
PaymentsConfig, conexão:
Conexão, event_bus: EventBus,
) "- Nenhum:
"".
captura de definição (
self, payment_uuid: UUID, customer_id: int
) "- Nenhum:
"".
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.
• Uma classe que emite Eventos permanece totalmente ignorante sobre a forma como eles são tratados
• 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
Com o Python Injector, isso pode ser alcançado usando uma combinação de multibind e genéricos.
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
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
classe BidderHasBeenOverbidHandler:
@injetor.inject def
_init_(
self, fachada: CustomerRelationshipFacade) "-
Nenhum:
self._facade = fachada
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(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."""
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
self._run_async_handler(async_handler, event) # 5
Linhas interessantes:
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.
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
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:
) senão:
"".
# código ignorado
classe PaymentChargedHandler:
@injector.inject def
_init_(
self, fachada: PaymentsFacade) "-
Nenhum:
self._facade = fachada
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.
Leilão de classe :
def place_bid( self,
bidder_id: BidderId, amount: Money ) "- Nenhum:
old_winner
= ( self.winners[0]
if self.bids else Nenhum
if old_winner:
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
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.
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.
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.
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.
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
• 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:
@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:
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:
"".
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.
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.
• 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
ExemploDados(process_uuid=UUID( "9fc15305-2a0f-41ed-8c1c-eafa2416ee75"
),
name="Exemplo",
contador=0,
timeout_at=datetime.datetime(
2019, 9, 29, 20, 20, 11, 426930
),
)
{
"nome": "Exemplo",
"contador": 0,
"timeout_at": "2019-09-29T20:20:11.426930"
}
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,
@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:
• 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
A primeira solução é simples de implementar e resulta em um código mais simples, mas afetará os módulos
envolvidos. Na prática:
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
evento: PaymentCaptured,
dados: PayingForWonItemData, ) "-
Nenhum:
data =
Linhas interessantes:
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:
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.
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
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:
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
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
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á
_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:
• definir uma porta em um módulo e permitir que outro módulo forneça um adaptador,
ÿ 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
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.
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/
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:
Por outro lado, um antipadrão quando a maior parte do esforço de teste é feito manualmente é chamado
de casquinha de sorvete:
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.
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
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.
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,
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.
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.
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.
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 “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.
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:
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
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:
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:
) 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, ""),
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.
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.
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.
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
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:
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
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
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.
quantidade=get_dollars("19,99"),
)
def test_EndedAuction_Ending_RaisesException(
ontem: datetime, ) "-
Nenhum:
leilão = AuctionFactory(ends_at=yesterday)
leilão.end_auction() # 1
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:
# 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:
parâmetros de classe :
terminou = Falso
id = fábrica.Sequence(lambda n: n)
"".
senão Nenhum
)
se não terminou:
terminou_at = Nenhum
outro:
terminou_at = datetime.now() - timedelta(dias=1
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.
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.
repo.save(auction_with_pending_event)
event_bus_mock.post.assert_called_once_with(evento_pendente
)
Machine Translated by Google
repo.save(auction_with_pending_event)
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.
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()
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
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.
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:
ending_auction_uc.execute( EndingAuctionInputDto(auction.id)
)
pagamento_provider.begin_payment.assert_called_once_with( leilão.preço_atual
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.
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(
"".
Normalmente, não há atalhos para escrever stubs, embora as simulações do Python possam ser potencialmente
abusadas para fazer isso:
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
# isso torna esta classe um híbrido de Stub e Spy - outro tipo de teste duplo
self._payments.append(quantia) retorna
Verdadeiro
def test_EndingAuction_WonEndedAuction_CallsPaymentProviderWithAuctionCurrentPrice(ending_auction_uc:
EndingAuction, leilão: Leilão,
payment_provider:
PaymentProviderStub,) "-
Nenhum:ending_auction_uc.execute( EndingAuctionInputDto(auction.id)
)
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.
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
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:
amanhã =
# 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.
Esta etapa é a mais simples de todas. A implementação se resume a apenas chamar um Caso de Uso:
# Agir / Quando
# 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:
• exceções lançadas
place_bid_uc.execute( PlacingBidInputDto(1,
leilão_id, get_dollars("100")
)
)
esperado_dto = ColocandoBidOutputDto(
is_winner=Verdadeiro,
preço_atual=get_dollars("100"),
) event_bus.post.reset_mock()
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
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
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
• 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
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,
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?
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.
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
Na próxima etapa, identifique o código acoplado a provedores externos e comece a movê-lo para um adaptador recém-
Na etapa final, comece a identificar as Entidades que protegerão suas regras de negócio. Crie repositórios para eles.
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
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
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
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.
sempre que fazemos alguma alteração (por exemplo, alterar a quantidade) ou ler o estado na maioria dos casos (por exemplo,
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
• 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
• 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
• 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
• Um histórico completo do que foi alterado, quando e por quem (se você incluir tal
informações em um evento)
• 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
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:
linhas = []
self._customer_id = customer_id self._status
= status self._lines = linhas
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]] = {}
@classmethod
def subclass_for_name(cls, name: str) "- Digite: """Obtenha a
subclasse do evento para desserializar."""
retornar cls._subclasses[nome]
@dataclass(frozen=True) classe
OrderDrafted(Evento):
ID_do_cliente: IDDoCliente
@dataclass(frozen=True) classe
OrderConfirmed(Evento):
passar
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 :
event_stream =
[Pedido elaborado(customer_id=1, criado_at="".),
PedidoConfirmado(criado_at="".),
]
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
@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] = {}
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,
)
@classmethod
def draft( cls,
uuid: UUID, customer_id: CustomerId
) "- "Ordem":
instance = cls(
) instância._new_events = [
OrderDrafted( datetime.now(tz=pytz.UTC),
0,
customer_id,
)
]
instância de retorno
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
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
TESTE DE AGREGADOS
@freeze_time("14/01/2019") def
test_order_newly_created_confirmation_changes_status(self):
agora = datetime.now(tz=pytz.UTC)
pedido =
pedido.confirm()
@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
)
)
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
@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
@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
O parâmetro esperado_version mencionado acima serve para proteção contra atualizações simultâneas. Se tal situação
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
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
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.
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
-------------------------------------------------- -----
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
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
# 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
Os eventos são a nossa fonte de verdade, por isso não podemos permitir nenhuma perda de dados. Portanto, precisamos de um
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
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
-------------------------------------------------- ------------
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:
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.
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:
uuid = Coluna
(postgresql.UUID, primária_key=True
Índice("ix_events_gregate_version", "agregado_uuid",
"versão",
),
) uuid = Coluna
(postgresql.UUID, primária_key=True
) agregado_uuid =
agregado = relacionamento (
AggregateModel,
uselist=False,
backref="events",
)
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
]=
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,
)
) 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.
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
) .onde( (
AggregateModel.versão "=
alterações.expected_version
)&(
AggregateModel.uuid "=
alterações.agregado_uuid
)
)
se resultado.rowcount "! 1:
# bloqueio otimista falhou
aumentar ConcurrentStreamWriteError
) self._session.connection().execute(stmt)
Machine Translated by Google
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.
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.confirm()
event_store.append_to_stream(order.changes)
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
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.
) def
cancel_order( event_store: EventStore, order_uuid:
UUID ) "-
Nenhum: event_stream =
event_store.load_stream( order_uuid
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
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.
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
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
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 :
"".
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 :
"".
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):
"".
]=
tente: last_snapshot =
( self._session.query (SnapshotModel) .filter
) .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:
"".
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
Primeiro, vamos imaginar que temos a seguinte projeção de nossos agregados de pedidos:
Então, precisamos de uma função de projeção real que irá iterar sobre os eventos do fluxo e criar/atualizar adequadamente
_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(
@project.register def
_(evento: NewProductAdded) "- Nenhum:
projeção =
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.quantidade
},
} projeção.quantidade_total +=
(evento.quantidade
) _update_version_and_ts(projeção, evento)
session.flush()
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"),
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.
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.
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
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Padrões de Design: Elementos de
Software Orientado a Objetos Reutilizável
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
Mary Poppendieck, Tom Poppendieck, Liderando o desenvolvimento de software enxuto: os resultados não são o
ponto
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
Sandi Metz, 99 garrafas de OOP: um guia prático para design orientado a objetos