Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 20, 2022 06:59 pm GMT

A Arquitetura simples

Disclaimer: Os exemplos usados aqui embora baseados em projetos reais so de minha autoria. Eu simplifico/altero alguns detalhes de forma a simplificar o post e preservar a IP das empresas. Esse post 100% baseado apenas em minhas opinies e no reflete meus empregadores.

A ideia desse post surgiu de uma discusso que eu tive com o @rponte. Com todos os debates recentes sobre "Arquitetura Limpa" e "Ports and Adapters" ns conversvamos como nos ltimos 7 anos os projetos que eu passei foram estruturados em empresas como Amazon e Twitter.

A Arquitetura simples

Eu pensei nesse termo "Arquitetura Simples" como uma brincadeira com o termo arquitetura limpa. Esse estilo de arquitetura no proposital, ele nasce naturalmente a partir do momento que o time de desenvolvimento abraa os dois seguintes princpios:

  1. YAGNI - You aren't gonna need it (Voc no vai precisar disso).
  2. KISS - Keep it Simple Stupid ou Keep it Super Simple (Mantenha simples estpido ou mantenha super simples).A ideia "central" que o time no vai gastar energia e esforo projetando sistemas tentando prever o futuro ou desacoplando camadas sem que os requisitos exijam isso.

Eu seria hipcrita em dizer que isso /foi uma escolha consciente. Nos ltimos 8 anos que trabalhei em empresas como Amazon e Twitter ns nunca nos sentamos e decidimos explicitamente "essa ser a arquitetura do nosso sistema". Os sistemas simplesmente eram escritas e vinham " tona" nessa forma simples. Talvez pela facilidade e naturalidade de como comear um projeto dessa forma.

Tambm importante mencionar que a forma como esses sistemas so escritos no a forma perfeita para todas as aplicaes e to pouco eu acho que essa seja a soluo para todos os problemas. "There's no silver bullet" (No h bala de prata) um outro princpio que eu acredito e que eu acho que deve ser sempre ponderado por todos os times nos mais diversos projetos.

As "camadas" das "Arquitetura Simples"

As camadas da nossa arquitetura so o mais simples possvel (com o perdo do trocadilho) e eu sei que isso pode incomodar muita gente. Normalmente as dependncias so diretas, por exemplo, um controller depende diretamente da classe que implementa o caso de uso daquele controller, ou o caso de uso depende diretamente da classe de persistncia que salva os dados ou recupera os dados que ele precisa.
Os componentes podem ser observados no diagrama abaixo:

Image description

Controlers

Um controller por API, aqui a gente recebe um RequestDTO que representa a requisio de uma API RPC, fazemos todas as validaes de input e outras coisas como logging.
Depois existem diversas opes aqui que variam de projeto pra projeto:

  1. Passamos o RequestDTO diretamente para o objeto de caso de uso. Essa opo favorece simplicidade (KISS e YAGNI), porm acopla teu objeto de caso de uso com modificaes no modelo da API. Muitas vezes, isso est totalmente ok.
  2. Esse RequestDTO traduzido pra uma entidade de negcio do caso de uso se as entidades forem extremamente anmicas. Essa opo favorece simplicidade porm em muitos casos a entidade vai ser criada incompleta e vai ser um objeto mutvel que preenchido com mais informaes na camada de negcio.
  3. Traduzimos o RequestDTO para algum outro DTO que usado exclusivamente como input da API de caso de uso. Esse o modelo mais flexvel que desacopla negcios da API porm mais verboso e exige transferir os dados de um DTO pra outro. A flexibilidade ajuda se o caso de uso utilizar inputs diferentes, por exemplo, uma API sncrona e um worker assncrono.

Por que? comum o controller fazer vrias regras referentes a obteno de dados, logging e outras validaes. Misturar isso com a coordenao entre diversos outros objetos como invocar servios externos e salvar no banco de dados deixa o controller grande demais. No geral, ns quebramos o controller pra:

  • Reduzir o tamanho e complexidade da classe.
  • Facilitar a segregao de responsabilidades e consequentemente os testes de unidade.Sobre traduzir o RequestDTO para um outro modelo interno, comum a API e consequentemente seu DTO evoluir a passos diferentes do modelo interno. Em 100% dos projetos que participei, toda vez que tentamos utilizar um nico modelo interno e pra request a coisa desandou e depois tnhamos que fazer um refactoring pra desacoplar os modelos um do outro. Hoje em dia, eu j insisto em comearmos com modelos separados pra evitar essa dor.

Casos de Uso/Entidades

As classes dessa camada normalmente caem em 2 grupos:

  • Classes de caso de uso: Essas classes implementam as regras de negcio da aplicao colando entidades, persistncia e dependncias. Aqui o modelo bem flexvel e varia de aplicao pra aplicao. No geral, se tem uma classe de caso de uso por caso de uso do usurio mas diversos modelos e entidades de suporte que ajudam a extrair responsabilidades especficas em classes menores.
  • Entidades: So as entidades do nosso sistema. Elas podem ser anmicas sendo apenas simples estrutura de dados ou agrupar comportamento que alteram o valor do objeto. As entidades tambm nos ajudam a encapsular mdulos e comportamentos que so comuns ao resto da aplicao.

Por que? Uma coisa que eu aprendi ao longo dos anos ter muito cuidado em reusar essas classes de caso de uso para mltiplas APIs. No incio essa ideia parece funcionar mas com o tempo as regras de negcio vo mudando e as classes de caso de uso vo ficando cheias de regras especiais para diferentes APIs. Um bom exemplo quando chamamos essas classes de "Manager", por exemplo, ProductManager e a voc implementa create/update/delete/read na mesma classe s pra reusar algumas funes em comum. Agora imagine que o caso de uso de update tem 3 dependncias, o de delete tem 2 e o create tem outras 3. Mesmo que haja sinergia entre elas, as vezes as dependncias vo ser diferentes e no pior dos casos a sua classe pode acabar com 8 dependncias diferentes. Nesse caso, talvez seja melhor quebrar em classes de caso de uso diferentes e abstrair os comportamentos comuns que so independentes de caso de uso nas entidades de domnio.

Ainda nessa camada a maior polmica , devo anotar nossas entidades com anotaes de persistncia ORM? Na maioria dos projetos que eu entrei entidades de persistncia e entidades de domnio so mantidas separadamente e sempre temos que converter de uma pra outra. Porm, olhando pra trs eu me pergunto se essa separao realmente necessria. Novamente, nessa mesma maioria dos projetos, sempre houve um mapeamento 1:1 de um campo de domnio pra um campo na persistncia. Essa separao s nos causa mais dor na hora de implementar algo novo e definitivamente quebra o nosso princpio KISS e YAGNI. Se eu fosse comear um projeto novo, provavelmente eu comearia com o domnio mapeado com o framework de ORM. Se as coisas comearem a divergir ento eu faria uma task para separ-los.

Dependncias externas

Para cada servio externo, onde externo significa uma chamada de rede, ns criamos uma classe. Essa classe funciona como uma Facade e Anti-Corruption Layer entre o nosso domnio e as dependncias externas. A API com essa classe normalmente se d em tipos simples ou atravs das nossas prprias entidades.

Por que? Aqui queremos isolar as dependncias daquele sistema externo em um nico ponto. Logo se a API mudar, for deprecated ou algo especfico daquele sistema acontecer, apenas 1 classe do nosso cdigo afetada.

Camada de Persistncia

Similar a camada que interage com os servios externos. Ns temos uma camada de persistncia que contm a classe que faz a chamada ao banco de dados e as entidades de persistncia que so mapeadas por algum framework ORM (se voc j nao tiver feito isso no domnio).

Por que? Ns tentamos manter a persistncia to simples quanto possvel. Inclusive, hoje em dia possvel implementar a maioria dessas operaes utilizando frameworks, como por exemplo, o Spring DATA.

Configuraes e preocupaes transversais

Colando todas essas camadas ns temos a camada de configurao. Normalmente, essa camada representada pelo framework de injeo de dependncias de sua escolha que cria todos os objetos e faz o wiring para que eles funcionem juntos.

nessa camada tambm que carregamos as variveis de ambiente que variam por regio(US, JP, BR, etc...) e/ou stage (dev, pre-prod e prod).

Outra configurao que eu colocaria nessa camada so preocupaes transversais. Classes que interceptam algum comportamento entre camadas, por exemplo. Esses interceptadores so bem comuns na camada de Controller, por exemplo. Eles podem realizar funes como validao de dados, autenticao, logging de chamadas, etc... tudo isso feito ANTES da chamada ser tratada pelo controller. Como essas funes so bem genricas, fcil isolar elas e acopla-las com todos os controllers da aplicao.

Por que? Por conveno. A maioria dos locais que eu trabalhei tem a conveno de no colocar muitas annotations no cdigo da camada de casos de uso. Alm disso, sobre as config classes, que muitos dos nossos objetos so criados com algumas lgicas interessantes. Logo usar config classes em vez de annotations acaba facilitando por nos dar mais controle e flexibilidade.

Eu j passei por alguns exemplos bem especficos onde as anotaes atrapalharam mas dito isso, pros projetos que ns tnhamos no h muita diferena entre usar as anotaes ou no, logo, se estivesse comeando um projeto novo eu faria o que mais simples e segue as convenes da empresa.

Estudo de caso 1: ReviewsService

Para ilustrar a "arquitetura" que eu mencionei acima, vamos imaginar como seria o sistema de submisso de avaliaes (reviews) de produto de um grande e-commerce.

Disclaimer: Qualquer semelhana com a realidade mera coincidncia.

Imagine que ns temos que implementar o caso de uso: CreateReview. Depois de esboamos o system design e discutimos com o time ns chegamos ao seguinte fluxo:

Image description

  1. Usurio invoca o servio para criar uma nova review.
  2. reviewsService invoca um servio externo para obter detalhes adicionais da review. Por exemplo, apelido do usurio.
  3. reviewService salva a review no banco de dados.
  4. Banco de dados notifica um servio de moderao com uma nova review criada.
  5. Servio de moderao retorna o resultado da avaliao para o reviewsService.
  6. ReviewsService faz update no banco de dados com o resultado da avaliao.

Agora na hora da implementao ns imaginamos as seguintes classes/camadas:

Image description

Controllers

Ns temos 2 controllers:

  1. CreateReview que implementa nossa API. Esse controller recebe a requisio, faz todas as validaes de entrada necessrias e transforma o RequestDTO em CreateReviewDTO que enviado para a nossa classe de caso de uso. Essa transformao opcional e nem todo projeto faz isso, o porqu eu fiz isso aqui? Pra manter consistncia com o UpdateReview controller/caso de uso. Mais detalhes no passo 8.
  2. UpdateReview controller que implementa uma outra API. Eu coloquei essa API aqui pra dar uma viso pra um caso de uso que utilizado por duas entradas diferentes. Mais detalhes no passo 8 abaixo.

Casos de Uso/Entidades

  1. CreateReviewUseCase cola a nossa regra de negcio. Essa classe chama um servio externo atravs do Adapter DetalheReviewsAdapter. Com a informao do DTO e do retorno do Adapter, uma nova review criada.
  2. Review e outras entidades de domnio (no representadas no diagrama). Essas entidades esto anotadas com nosso framework de ORM para simplificao.

Persistncia

  1. ReviewDAO persiste a nossa entidade Review no banco de dados. Essa classe fortemente acoplada ao nosso BD de escolha, nesse caso, o AWS DynamoDB

Esses dois componentes no so da camada de persistncia, mas eu vou deixar a explicao aqui por curiosidade/completude:

  1. O DynamoDB atravs da funcionalidade de streams (post em breve, prometo ) envia uma mensagem sempre que uma nova review criada. A mensagem contm as informaes que foram criadas na review e enviada para uma fila no AWS SQS (outro post futuro).
  2. A fila do SQS consumida por um servio de moderao de outro time. (Eu estou simplificando bastante aqui, dificilmente ns iramos expor uma fila SQS, isso tem mais cara de um tpico SNS com mltiplos subscribers).

Adapters/Integrao

  1. Depois de moderada, a nossa review aprovada ou rejeitada. Um pequeno Worker implementado por uma classe chamada ModerationListener fica ouvindo por mensagens enviadas pelo sistema de moderao. O ModerationListener recebe a mensagem e transforma em um UpdateReviewDTO necessrio para chamar a nossa classe de UpdateReviewUseCase.

Percebeu agora porque eu decidi criar um DTO de entrada por caso de uso? Como meu requisito contm mltiplas entradas no meu sistema eu no quis fazer o Listener depender de uma classe da camada de APIs (o RequestDTO).

Note que a soluo aqui seguiu uma estratgia "clssica". Em um ambiente de cloud poderamos ter resolvido esse problema com um microservio que roda separadamente dentro de um AWS Lambda.

Simplificaes e Trade-offs nesse caso de uso

Como escrever um sistema que escala para milhares de requisies por segundo muito complexo, vrias simplificaes foram feitas:

  1. Vrias das chamadas que fazemos poderiam ser Decorators assncronos que reagem a um primeiro update no banco de dados. Algo mais event-driven para garantir mais escalabilidade. Um bom exemplo o DetalhesReviewAdapter que poderia ser, por exemplo, um decorator que adiciona informaes depois que a review escrita no nosso banco de dados e nos desacoplaria de esperar uma chamada sncrona ser feito a um servio externo.
  2. Eu no adicionei em nenhum caso de uso outras checagens como Idempotncia. Normalmente voc faz isso assim que possvel para evitar chamadas a outras dependncias desnecessariamente.
  3. Toda a moderao, como isso afeta a review e como lidar com escritas concorrentes tambm no foram lidadas aqui.
  4. No estou usando outros padres como "event source". Estou considerando que os dados vo ser stateful em vez de guardar as aes e dali tirar um snapshot do review resultante (Se estiver curiosa(o) d uma olhada no padro de Event Sourcing).

J em termos de trade-offs, notem o seguinte:

  1. Percebem que nossas entidades de domnio esto acopladas ao banco de dados. Por qu? Na maioria dos projetos, o mapeamento quase 1-1. Se os dois comeassem a divergir, eu faria o seguinte refactoring.
    1. Criar um novo DTO especfico para a persistncia.
    2. Manter a entidade de domnio como est e copiar o cdigo para o novo DTO.
    3. Atualizar as classes de persistncia para usar o novo DTO.
  2. Eu adicionei uma complexidade extra com a adio de DTOs por caso de uso. As vezes, isso nem necessrio e voc pode simplesmente passar o DTO do controller direto.
  3. O nosso listener chama o caso de uso mas muitas vezes, ns queremos que todas as chamadas, mesmo quando feitas dentro do mesmo sistema passem pela API. Nesse caso, o nosso ModerationListener invocaria a API no tendo visibilidade ao caso de uso ou banco de dados. Isso protege os dados e nos d mais segurana forando todo mundo a passar pela API.

As complexidades esto nas "bordas"

Voc pode estar pensando. "Essa "arquitetura" simplria demais" ou "Mas e os testes?". Pois bem, as solues escritas nesse "padro" so todas bem testadas, tanto do ponto de vista de unidade quanto de end-to-end. Na unidade, com frameworks modernos como Mockito (Nem to moderno assim), ns conseguimos fazer mocks de classes concretas facilmente. Tambm possvel criar fakes atravs de herana sobrescrevendo os mtodos pblicos e dando o comportamento que desejamos.

Com relao a simplicidade, a ideia que seja simples mesmo. A maioria dos problemas que enfrentei nesses ltimos 8 anos de carreira no foi relacionado a como implementar uma regra de negcio ou classe X estar fortemente acoplada a classe Y. Os problemas que enfrentei foram mais do tipo:

  • Servio externo A retorna erro Y quando deveria retornar Z.
  • Execues concorrentes afetando os dados
  • Infra no escalando ou problema especfico da infra.
  • Interao e coordenao entre 3 ou mais sistemas sem uso de transaes.
  • Mensagens duplicadas nas filas ou falta de retries.

Percebeu como um domnio desacoplado dificilmente nos ajudaria com os problemas acima? A maioria dos problemas grandes que enfrentamos se deram por decorrncia de erros no System Design ou de comportamentos inesperados entre sistemas, afinal de contas, sistemas distribudos so estranhos.

Concluso

Nesse artigo eu apresentei uma proposta que vai de encontro a outros estilos de arquitetura como "Ports and Adapters" ou a "arquitetura limpa". No me entendam mal, eu no sou 100% contra esses modelos, e h exemplos de sucesso na indstria com o uso arquitetura hexagonal e/ou clean. Porm tais necessidades devem emergir dos seus requisitos.

Vejam o caso do Netflix. Ali eles tinham a necessidade de conseguir trocar entre datasources de forma rpida e o input/output deles obedecia uma certa forma homognea que o estilo da Arquitetura Hexagonal podia suprir.

Finalmente, esse artigo no uma receita do que voc deve fazer. Eu apenas decidi a minha experincia e demonstrar que tem muito software sendo escrito, entregue e rodando com sucesso mundo afora que no segue os estilos de "arquitetura" que vemos nos livros. Esses sistemas so bem testados e rodam todo dia para milhes de usurio simultneos.

Espero que vocs tenham gostado do artigo e que isso te faa pensar se voc realmente precisa de tantas classes na hora de escrever seu sistema. Se tiverem alguma dvida, no deixem de perguntar nos comentrios ou no twitter.


Original Link: https://dev.to/hugaomarques/a-arquitetura-simples-lb

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To