Design: Monolitos Modulares
Olá!
Este é mais um post da sessão Design e, desta vez, falaremos sobre um tema que, volta e meia, reaparece: monolitos.
Entretanto, a intenção deste post é expor uma forma diferente de lidar com este estilo arquitetural: monolitos modulares.
Esta abordagem ajuda a manter o monolito organizado, coeso e menos acoplado e, por isso, é ótima para evitar a chamada "big ball of mud" (grande bola de lama), que se caracteriza por um código com muitas dependências entre as funcionalidades, circulares inclusive, o que torna mais difícil manter e evoluir uma dada aplicação.
Vamos lá!
O Monolito
Durante a febre dos microsserviços, o monolito passou a ser tratado como obsoleto. E muito desse tratamento veio da falta de compreensão sobre quais cenários podem se beneficiar deste modelo, tornando muitas aplicações desnecessariamente complexas quando poderiam ser mais simples. Infelizmente, como efeito colateral deste tratamento, acabamos por ver a repetição da "big ball of mud" neste estilo, o que chamamos de "monolito distribuído", uma vez que os mesmos erros de gestão de dependências que ocorriam nos monolitos, afetaram também os processos distribuídos.
A imagem abaixo ilustra este cenário:
Nota: Algo importante, a esta altura, é entender que, além de repetir a "big ball of mud", há um outro efeito colateral: aumento da complexidade, que leva a um aumento direto do custo do software. Este aumento de complexidade se dá pela preocupação com falhas na rede, mensageria, aglutinação de logs entre outras. Ou seja, uma escolha inadequada por um estilo arquitetural pode levar o negócio a ter maiores despesas com o software, aumentando seu custo operacional.
Vejamos, então, quando o monolito faz sentido para, em seguida, entendermos como a ideia de módulos se ajusta a ele.
Lei de Conway
A Lei de Conway (em inglês) nos diz que, em organizações onde se desenvolve software, este tende a replicar a estrutura de comunicação dessas organizações.
Isso, no que tange à escolha de um estilo arquitetural, significa dizer que não faz sentido utilizar microsserviços quando uma dada aplicação não é mantida por múltiplos times, onde cada um é responsável por um dado conjunto de funcionalidades e, mais importante, onde todos precisem realizar deploys independentemente.
Este último critério, por sinal, é a régua do polegar para a escolha de microsserviços em detrimento do monolito. Se não há a necessidade de deploys independentes, não há razão para que se escolha microsserviços e, neste caso, monolito se torna o estilo de primeira escolha.
A ideia de modularidade
Modularidade, em software, significa criar conjuntos de funcionalidades agrupadas por coerência, e separadas do programa principal, aquele que serve de ponto de entrada.
Para os desenvolvedores dotnet e Java, por exemplo, isso seria equilavente a ter bibliotecas/pacotes, que vou chamar de componentes para generalizar, que representam as funcionalidades, separadas do programa que executa o método Main
.
A partir desta separação, e do agrupamento entre as funcionalidades, seria definida uma API (não confundir com Web API) que serviria de ponto de entrada a este grupo, a partir do qual toda a comunicação entre o este grupo e os demais seria mediada.
Encapsulamento
Para garantir que a modularidade seja respeitada, é necessário isolar o código da API do restante do código contido no módulo e, para isso, é necessário ocultar o código interno às funcionalidade.
Para programadores dotnet, por exemplo, isso se faz possível pelo uso da palavra-chave internal
, que permite que um dado código seja executado apenas a partir do arquivo onde o mesmo se encontra.
Como neste cenário apenas o código da API ficará visível, não será possível invocar o código ocultado em seu componente, tornando mais fácil manter a modularidade.
O resultado final seria um esquema como o da imagem abaixo:
Isolando a interface com o usuário
Outro ponto de isolamento importante é aquele feito sobre a interface do usuário. Da mesma forma que o acesso a dados é entendido como parte da infraestrutura da aplicação, assim o é para as interfaces (ou controllers no caso de uma Web API). Assim sendo, é recomendável que sejam criados componentes separados para essas interfaces.
Em um projeto ASP.NET, por exemplo, haveria em uma solução um projeto como ponto de entrada da aplicação (aquele que possuirá o método Main
e a classe de Startup
), e um outro projeto para cada módulo com seus respectivos controllers (e/ou views).
Esta separação é importante pois, conforme a evolução da organização, caso surja a necessidade de realizar a decomposição da aplicação em serviços, basta mover estes projetos para uma nova solução, mantendo a infra (aplicação Web) separada do domínio (componente).
Isolando o acesso a dados
Outro ponto muito importante é manter o mesmo isolamento das funcionalidades no nível dos dados. Para que a modularidade se mantenha, cada módulo é exclusivamente responsável por obter seus dados, condição esta que pode ser forçada por meio da criação de esquemas separados em uma base de dados (ou mesmo o uso de múltiplas bases), e impedindo que joins
sejam feitos entre eles a partir de restrições a cada usuário do banco associado a um módulo.
Desta forma, chegaríamos a um esquema semelhante ao da imagem abaixo:
DDD
A abordagem que considero mais indicada para a correta separação dos módulos é a ideia de bounded contexts
do DDD. Modelando-se o domínio a partir deles, a separação entre os módulos se define automaticamente, assim como a comunicação entre os mesmos pode ser mapeada com maior precisão.
Esta abordagem nos faria chegar a um esquema semelhante ao da imagem abaixo:
Conclusão
Com este simples conjunto de princípios é possível manter os módulos de sua aplicação isolados, evitando assim que haja dependências indesejáveis seja por porções de código (classes e interfaces, ou funções), seja por tabelas e outros objetos de banco de dados.
Procedendo desta forma, a aplicação se mantém coesa, fácil de manter e evoluir e, portanto, com menor custo para o negócio (tempo) e para o time (estresse).
É possível que, a esta altura, você tenha notado algo interessante: os princípios mencionados acima são uma reprodução daqueles utilizados em microsserviços. A única diferença é o meio pelo qual a troca de mensagens acontece: com microsserviços ela se dá pela rede e, com monolitos, ela se dá pela memória do processo.
Isso significa, conforme dito na seção "Isolando a interface com o usuário", que uma eventual decomposição da aplicação em serviços, se assim se fizer necessário no futuro, seja facilitada, o que reduz -- e muito! -- a necessidade de uma eventual reescrita por conta de dependências diretas entre porções de diferentes módulos.
Na Parte 2 deste artigo vou me concentrar na aplicação destes princípios em uma aplicação de demonstração. A ideia é mostrar não apenas como o código seria organizado como, também, as diferentes formas de comunicação entre os módulos (síncrona e assíncrona).
Gostou? Me deixe saber pelos indicadores. Tem dúvidas ou sugestões? Deixe um comentário ou me procure pelas redes sociais.
Até a próxima!