Arquitetura limpa através dos olhos de um desenvolvedor Python

Olá! Meu nome é Eugene, sou desenvolvedor de Python. No último ano e meio, nossa equipe começou a aplicar ativamente os princípios da Arquitetura Limpa, afastando-se do modelo clássico do MVC. E hoje vou falar sobre como chegamos a isso, o que isso nos dá e por que a transferência direta de abordagens de outros PLs nem sempre é uma boa solução.



O Python é minha principal ferramenta de desenvolvimento há mais de sete anos. Quando me perguntam o que mais gosto nele, respondo que essa é sua excelente legibilidade . O primeiro conhecido começou com a leitura do livro “Programming a Collective Mind” . Eu estava interessado nos algoritmos descritos nele, mas todos os exemplos estavam em uma linguagem que ainda não me era familiar. Isso não era usual (o Python ainda não era popular no aprendizado de máquina), as listagens eram frequentemente escritas em pseudo-código ou usando diagramas. Mas, depois de uma rápida introdução à linguagem, apreciei sua concisão: tudo era fácil e claro, nada supérfluo e perturbador, apenas a essência do processo descrito. O principal mérito disso é o design incrível da linguagem, o açúcar sintático muito intuitivo. Essa expressividade sempre foi apreciada na comunidade. O que é " importar isso ", necessariamente presente nas primeiras páginas de qualquer livro: parece um superintendente invisível, avalia constantemente suas ações. Nos fóruns, valia a pena para um iniciante usar de alguma forma o CamelCase no nome da variável na listagem; assim, imediatamente o ângulo de discussão mudou para o idioma do código proposto com referências ao PEP8.
A busca pela elegância e o poderoso dinamismo da linguagem criaram muitas bibliotecas com uma API verdadeiramente deliciosa. 

No entanto, o Python, apesar de poderoso, é apenas uma ferramenta que permite escrever códigos expressivos e auto-documentáveis, mas não garante isso , nem a conformidade com o PEP8. Quando nossa aparentemente simples loja on-line no Django começa a ganhar dinheiro e, como resultado, aprimora os recursos, a certa altura percebemos que não é tão simples, e até mesmo fazer mudanças básicas exige cada vez mais esforço, e o mais importante, essa tendência está crescendo. O que aconteceu e quando tudo deu errado?

Código incorreto


Código incorreto não é aquele que não segue o PEP8 ou não atende aos requisitos de complexidade ciclomática. O código incorreto é, antes de tudo, dependências não controladas , que levam ao fato de que uma mudança em um local do programa leva a alterações imprevisíveis em outras partes. Estamos perdendo o controle sobre o código; expandir a funcionalidade requer um estudo detalhado do projeto. Esse código perde sua flexibilidade e, por assim dizer, resiste a fazer alterações, enquanto o próprio programa se torna "frágil". 

Arquitetura limpa


A arquitetura escolhida do aplicativo deve evitar esse problema, e não somos os primeiros a encontrar isso: houve uma discussão na comunidade Java sobre a criação de um design de aplicativo ideal por um longo tempo.

Em 2000, Robert Martin (também conhecido como tio Bob) em seu artigo " Principles of Design and Design " reuniu cinco princípios para o design de aplicativos OOP sob o memorável acrônimo SOLID. Esses princípios foram bem recebidos pela comunidade e foram muito além do ecossistema Java. No entanto, eles são muito abstratos por natureza. Mais tarde, houve várias tentativas de desenvolver um design geral de aplicativo com base nos princípios do SOLID. Isso inclui: “Arquitetura hexagonal”, “Portas e adaptadores”, “Arquitetura bulbosa” e todos eles têm muito em comum, embora diferentes detalhes de implementação. E em 2012, um artigo foi publicado pelo mesmo Robert Martin, onde ele propôs sua própria versão chamada “ Arquitetura Limpa ”.



De acordo com o tio Bob, a arquitetura é principalmente " fronteiras e barreiras ", é necessário entender claramente as necessidades e limitar as interfaces de software para não perder o controle sobre o aplicativo. Para isso, o programa é dividido em camadas. Mudando de uma camada para outra, apenas os dados podem ser transferidos (estruturas simples e objetos DTO podem atuar como dados ) - essa é a regra dos limites. Outra frase citada com mais frequência que “o aplicativo deve gritar ” significa que o principal no aplicativo não é a estrutura usada ou a tecnologia de armazenamento de dados, mas o que esse aplicativo realmente faz, qual a função que ele executa - a lógica de negócios do aplicativo . Portanto, as camadas não têm uma estrutura linear, mas têm uma hierarquia . Daí mais duas regras:

  • A regra de prioridade para a camada interna - é a camada interna que determina a interface através da qual ela irá interagir com o mundo exterior;
  • Regra de dependência - As dependências devem ser direcionadas da camada interna para a externa.

A última regra é bastante atípica no mundo Python. Para aplicar qualquer cenário complicado de lógica de negócios, você sempre precisa acessar serviços externos (por exemplo, um banco de dados), mas para evitar essa dependência, a própria camada de lógica de negócios deve declarar a interface pela qual interagirá com o mundo externo. Essa técnica é chamada de " inversão de dependência " (a letra D no SOLID) e é amplamente usada em idiomas com tipagem estática. Segundo Robert Martin, esta é a principal vantagem que veio com o OOP .

Essas três regras são a essência da arquitetura limpa:

  • Regra de passagem de fronteira;
  • Regra de dependência;
  • A regra de prioridade da camada interna.

As vantagens dessa abordagem incluem:

  • Facilidade de teste - as camadas são isoladas, respectivamente, podem ser testadas sem remendo de macacos, você pode definir granularmente o revestimento para diferentes camadas, dependendo do grau de importância;
  • A facilidade de alterar as regras de negócios , uma vez que todas elas são coletadas em um único local, não são espalhadas pelo projeto e não são misturadas ao código de baixo nível;
  • Independência de agentes externos : em alguns casos, a presença de abstrações entre a lógica de negócios e o mundo externo permite alterar fontes externas sem afetar as camadas internas. Funciona se você não vinculou a lógica de negócios aos recursos específicos de agentes externos, por exemplo, transações de banco de dados;
  • , , , .

, . . , . « Clean Architecture».

Python


Esta é uma teoria, exemplos de aplicação prática podem ser encontrados no artigo original, relatórios e livro de Robert Martin. Eles contam com vários padrões de design comuns do mundo Java: Adaptador, Gateway, Interactor, Fasade, Repository, DTO , etc.

Bem, e o Python? Como eu disse, o laconicismo é valorizado na comunidade Python.O que criou raízes em outros está longe do fato de que ele criará raízes conosco. A primeira vez que me virei para esse tópico há três anos, não havia muitos materiais sobre o uso da arquitetura limpa no Python, mas o primeiro link no Google foi o projeto Leonardo Giordani: o autor descreve detalhadamente o processo de criação de uma API para um site de pesquisa de propriedades usando o método TDD, aplicando arquitetura limpa.
Infelizmente, apesar da explicação escrupulosa e de seguir todos os cânones do tio Bob, este exemplo é bastante assustador

A API do projeto consiste em um método - obter uma lista com um filtro disponível. Eu acho que, mesmo para um desenvolvedor iniciante, o código para esse projeto não terá mais de 15 linhas. Mas neste caso, ele pegou seis pacotes. Você pode se referir a um layout não totalmente bem-sucedido, e isso é verdade, mas, em qualquer caso, é difícil alguém explicar a eficácia dessa abordagem , referindo-se a este projeto.

Há um problema mais sério, se você não ler o artigo e começar imediatamente a estudar o projeto, será bastante difícil de entender. Considere a implementação da lógica de negócios:

from rentomatic.response_objects import response_objects as res

class RoomListUseCase(object):
   def __init__(self, repo):
       self.repo = repo
   def execute(self, request_object):
       if not request_object:
           return res.ResponseFailure.build_from_invalid_request_object(
               request_object)
       try:
           rooms = self.repo.list(filters=request_object.filters)
           return res.ResponseSuccess(rooms)
       except Exception as exc:
           return res.ResponseFailure.build_system_error(
               "{}: {}".format(exc.__class__.__name__, "{}".format(exc)))

A classe RoomListUseCase que implementa a lógica de negócios (não muito semelhante à lógica de negócios, certo?) Do projeto é inicializado pelo objeto repo . Mas o que é um repositório ? Claro que, a partir do contexto, podemos compreender que repo implementa o modelo de repositório para acessar dados, se olharmos para o corpo de RoomListUseCase, entendemos que ele deve ter um método lista, cuja entrada é uma lista de filtros, que não está claro na saída, você precisa olhar no ResponseSuccess. E se o cenário for mais complexo, com acesso múltiplo à fonte de dados? Acontece que, para entender o que é um repositório, você pode apenas se referir à implementação. Mas onde ela está localizada? Ele está em um módulo separado, que não está de forma alguma associado ao RoomListUseCase. Portanto, para entender o que está acontecendo, você precisa ir para o nível superior (o nível da estrutura) e ver o que é alimentado na entrada da classe ao criar o objeto.

Você pode pensar que listo as desvantagens da digitação dinâmica, mas isso não é totalmente verdade. É a digitação dinâmica que permite escrever código expressivo e compacto . A analogia com microsserviços vem à mente: quando cortamos um monólito em microsserviços, o design ganha rigidez adicional, pois tudo pode ser feito dentro do microsserviço (PL, frameworks, arquitetura), mas deve estar em conformidade com a interface declarada. Então aqui: quando dividimos nosso projeto em camadas,Os relacionamentos entre as camadas devem ser consistentes com o contrato , enquanto dentro da camada, o contrato é opcional. Caso contrário, você precisa manter um contexto bastante amplo em sua cabeça. Lembre-se de que eu disse que o problema com código incorreto é dependências e, portanto, sem uma interface explícita, voltamos novamente para onde queríamos fugir - para a ausência de relacionamentos explícitos de causa-efeito .

repo RoomListUseCase, execute — . - , , . - . , , , repo .

Em geral, naquela época eu abandonei a Arquitetura Limpa em um novo projeto, novamente aplicando o MVC clássico. Mas, depois de preencher o próximo lote de cones, ele voltou a essa idéia um ano depois, quando finalmente começamos a lançar serviços no Python 3.5+. Como você sabe, ele trouxe anotações de tipo e classes de dados: Duas poderosas ferramentas de descrição de interface. Com base nelas, esbocei um protótipo do serviço, e o resultado já era muito melhor: as camadas pararam de se espalhar, apesar do fato de ainda haver muito código, principalmente na integração com a estrutura. Mas isso foi suficiente para começar a aplicar essa abordagem em pequenos projetos. Gradualmente, começaram a aparecer estruturas focadas no uso máximo de anotações de tipo: apistar (agora estrela), quadro de fundição. O pacote pydantic / FastAPI agora é comum e a integração com essas estruturas se tornou muito mais fácil. É assim que o exemplo acima de restomatic / services.py seria:

from typing import Optional, List
from pydantic import BaseModel

class Room(BaseModel):
   code: str
   size: int
   price: int
   latitude: float
   longitude: float

class RoomFilter(BaseModel):
   code: Optional[str] = None
   price_min: Optional[int] = None
   price_max: Optional[int] = None

class RoomStorage:
   def get_rooms(self, filters: RoomFilter) -> List[Room]: ...

class RoomListUseCase:
   def __init__(self, repo: RoomStorage):
       self.repo = repo
   def show_rooms(self, filters: RoomFilter) -> List[Room]:
       rooms = self.repo.get_rooms(filters=filters)
       return rooms

RoomListUseCase - uma classe que implementa a lógica de negócios do projeto. Você não deve prestar atenção ao fato de que tudo o que o método show_rooms faz é chamar o RoomStorage (eu não vim com esse exemplo). Na vida real, também pode haver um cálculo de desconto, classificando uma lista com base em anúncios etc. No entanto, o módulo é auto-suficiente. Se quisermos usar esse cenário em outro projeto, teremos que implementar o RoomStorage. E o que é necessário para isso é claramente visível desde o módulo. Diferentemente do exemplo anterior, essa camada é auto-suficiente e, ao mudar, não é necessário manter todo o contexto em mente. De dependências não sistêmicas apenas pydantic, por que, ficará claro no plug-in da estrutura. Sem dependências, outra maneira de melhorar a legibilidade do código, não o contexto adicional, mesmo um desenvolvedor iniciante será capaz de entender o que este módulo faz.

Um cenário de lógica de negócios não precisa ser uma classe; abaixo está um exemplo de cenário semelhante na forma de uma função:

def rool_list_use_case(filters: RoomFilter, repo: RoomStorage) -> List[Room]:
   rooms = repo.get_rooms(filters=filters)
   return rooms


E aqui está a conexão com o framework:

from typing import List
from fastapi import FastAPI, Depends
from rentomatic import services, adapters
app = FastAPI()

def get_use_case() -> services.RoomListUseCase:
   return services.RoomListUseCase(adapters.MemoryStorage())

@app.post("/rooms", response_model=List[services.Room])
def rooms(filters: services.RoomFilter, use_case=Depends(get_use_case)):
   return use_case.show_rooms(filters)

Usando a função get_use_case, o FastAPI implementa o padrão de Injeção de Dependência . Não precisamos nos preocupar com a serialização de dados, todo o trabalho é feito pela FastAPI em conjunto com a pydantic. Infelizmente, os dados nem sempre são formato de lógica de negócios adequado para transmissão ao vivo no restaurante <e, pelo contrário, a lógica de negócios não sabe onde os dados vieram - com barras, pedido corpo, biscoitos, etc . Nesse caso, o corpo da função da sala terá uma certa conversão de dados de entrada e saída, mas na maioria dos casos, se trabalharmos com a API, uma função proxy tão fácil será suficiente. 

, , , RoomStorage. , 15 , , , .

Intencionalmente, não separei a camada da lógica de negócios, como sugere o modelo canônico de Arquitetura Limpa. A classe Room deveria estar na camada de região do domínio da entidade que representa a região do domínio, mas, para este exemplo, não há necessidade disso. Ao combinar as camadas Entity e UseCase, o projeto não deixa de ser uma implementação de Arquitetura Limpa. O próprio Robert Martin disse repetidamente que o número de camadas pode variar tanto para cima quanto para baixo. Ao mesmo tempo, o projeto atende aos principais critérios da arquitetura limpa:

  • Regra de passagem de fronteira: modelos pydantic, que são essencialmente DTOs, atravessam fronteiras ;
  • Regra de dependência : a camada de lógica de negócios é independente de outras camadas;
  • A regra de prioridade para a camada interna : é a camada de lógica de negócios que define a interface (RoomStorage), através da qual a lógica de negócios interage com o mundo externo.

Hoje, vários projetos de nossa equipe, implementados usando a abordagem descrita, estão trabalhando no produto. Eu tento organizar até os menores serviços dessa maneira. Ele treina bem - perguntas sobre as quais eu não havia pensado antes surgiram. Por exemplo, o que é lógica de negócios aqui? Isso está longe de ser sempre óbvio, por exemplo, se você estiver escrevendo algum tipo de proxy. Outro ponto importante é aprender a pensar de maneira diferente.. Quando obtemos uma tarefa, geralmente começamos a pensar nas estruturas, nos serviços utilizados, se haverá uma necessidade de uma linha aqui, onde é melhor armazenar esses dados, que podem ser armazenados em cache. Na abordagem que dita a Arquitetura Limpa, devemos primeiro implementar a lógica de negócios e só depois passar à implementação da interação com a infraestrutura, pois, segundo Robert Martin, a principal tarefa da arquitetura é atrasar o momento em que a conexão com qualquer A camada de infraestrutura será parte integrante do seu aplicativo.

Em geral, vejo uma perspectiva favorável ao uso da Arquitetura Limpa no Python. Mas o formulário, provavelmente, será significativamente diferente de como é aceito em outros PLs. Nos últimos anos, vi um aumento significativo no interesse pelo tópico da arquitetura na comunidade. Portanto, no último PyCon, havia vários relatórios sobre o uso de DDD, e os funcionários dos laboratórios a seco devem ser anotados separadamente . Em nossa empresa, muitas equipes já estão implementando a abordagem descrita em um grau ou outro. Estamos todos fazendo a mesma coisa, crescemos, nossos projetos cresceram, a comunidade Python tem que trabalhar com isso, definir o estilo e a linguagem comuns que, por exemplo, uma vez se tornaram para todo o Django.

All Articles