Monolitos x Cloud Native: é possível modernizar um legado? | Arquitetura em Nuvem
Uma das práticas mais comuns no desenvolvimento de aplicações consiste em projetá-las como uma simples unidade, abordagem esta que levou ao surgimento de um termo bastante conhecido na área de software: monolito. A própria palavra monolito remete à noção de “bloco”, sendo que tal representação acaba por fazer bastante sentido quando consideramos a modelagem de uma aplicação desse tipo.
As diferentes funcionalidades que constituem um projeto monolítico guardam uma forte ligação entre si (lembrando assim um bloco), sendo que um novo release da aplicação invariavelmente levará à atualização de todas as suas partes. Uma típica arquitetura monolítica também terá um alto acoplamento entre seus componentes.
Tomando por exemplo o desenvolvimento Web, um projeto será normalmente formado por aplicação executando em um servidor (back-end/server-side application), uma interface client-side (acessível via browser) e envolverá ainda a persistência em um mecanismo de banco de dados: trata-se de um clássico modelo de software multicamadas. Por mais que haja esta separação do ponto de vista de tecnologias empregadas, a forte ligação entre os elementos presentes sempre será um obstáculo a ser superado na evolução do projeto como um todo.
Alto acoplamento em aplicações, cross-cutting concerns
O conceito de acoplamento também é importantíssimo a nível de código nas aplicações. Embora seja perfeitamente possível projetar um monolito em conformidade com boas práticas de desenvolvimento de software, frequentemente uma série de recomendações fundamentais para um código de qualidade serão ignoradas. As causas para isto são as mais variadas, indo desde questões envolvendo a maturidade dos membros de uma equipe a decisões técnicas equivocadas. Certamente existirão impactos ao longo do ciclo de vida do projeto, dificultando a realização de mudanças e, até mesmo, a migração para novas tecnologias que se mostrem mais adequadas num dado momento.
Um dos grandes desafios em monolitos está relacionado a implementações não diretamente ligadas a aspectos do negócio. E que funcionalidades seriam essas? Construções de código envolvendo logging, segurança, caching, monitoramento, gerenciamento de estados representam exemplos clássicos destas funcionalidades, sendo conhecidas como cross-cutting concerns.
Levemos em conta um sistema financeiro. Certamente o mesmo contará com funções para controlar o acesso seguro de usuários, o uso até mesmo de caching visando uma maior performance na manipulação de dados, além de mecanismos voltados a logging e visando o monitoramento da aplicação. Ainda assim, o foco principal de tal projeto claramente será o processamento de transações monetárias. Estas funcionalidades de apoio são fundamentais, mas não representam o objetivo principal da aplicação e muito menos o “core” daquele negócio.
Não raramente encontraremos aplicações em que as cross-cutting concerns estarão relacionadas a um mau design de código. Dentre os problemas mais frequentes teremos trechos duplicados ao longo de um projeto, extensas implementações que poderiam ser substituídas pela adoção de soluções pré-existentes (e em muitos casos open source), além de uma forte dependência de funcionalidades de negócio com tais recursos.
A escolha de tecnologias já consagradas no mercado, bem como a adoção de frameworks amplamente utilizados e a preocupação em se adequar a recomendações básicas para uma melhor codificação constituem uma ótima resposta a estas adversidades, normalmente advindas de uma má organização no que se refere às cross-cutting concerns. Aqui podemos nos valer de clássicos princípios da Orientação a Objetos como Reponsabilidade Única (Single-responsability) e Inversão de Controle (Inversion of Control) para um código bem estruturado, diminuindo o acoplamento e permitindo que a qualquer momento possamos migrar de uma tecnologia de logging, autenticação, caching ou qualquer outro mecanismo de apoio a uma aplicação para outra opção disponível.
Uma implicação direta do princípio da Responsabilidade Única está no fato de que devemos criar classes coesas em nossos projetos de software, não misturando o código de funcionalidades de negócio com implementações típicas das cross-cutting concerns. Cada classe estará voltada a um tipo de finalidade bem específica, esteja relacionada a um contexto específico do negócio ou a uma necesssidade acessória como logging.
Mas separar funcionalidades não é o único cuidado importante no tocante ao código de uma aplicação. Como podemos otimizar a interação entre tais classes, diminuindo o acoplamento entre as mesmas? E se for possível também adaptar o código para facilitar em um momento futuro a mudança de uma tecnologia por outra?
A solução a tais questionamentos está na técnica conhecida como Inversão de Controle. Favorecendo o uso de um modelo de programação orientado a interfaces, esse princípio possibilita a implementação de classes seguindo um contrato/estrutura pré-estabelecido. Esta característica permite que cheguemos a uma arquitetura plugável, do ponto de vista do software. A troca de uma tecnologia por outra produzirá então um menor impacto, bastando que se implemente uma classe a partir de uma interface previamente definida e incluindo eventuais ajustes adicionais no projeto. Isto certamente abrirá caminho para viabilizar iniciativas de modernização do projeto ao longo do tempo e sem grandes traumas.
Legados e dificuldades em evoluir o código
A convivência com projetos legados representa um grande desafio, sobretudo quando pensamos na necessidade de evolução de nossas aplicações para que estejam em conformidade com boas práticas de desenvolvimento e se beneficiem também de tendências em alta da área de software. Devemos considerar também as vantagens da adoção de serviços em nuvem em tais soluções, os quais priorizam questões como confiabilidade, disponibilidade e segurança.
As implicações práticas desses fatores resultarão, quase que invariavelmente, em mudanças no código. E é justamente neste momento que surgirão adversidades. A falta de preocupação com um bom design de software em estágios iniciais do projeto, aliada a um relaxamento natural com padrões e boas técnicas de codificação com o decorrer do tempo, constituem aspectos que trarão certamente dificuldades na modernização de nossas aplicações.
Deve-se somar a isso também o uso de serviços de apoio defasados, como por exemplo versões antigas de bancos de dados e mecanismos de logging. A migração para versões mais recentes ou, mesmo, a substituição por tecnologias que entreguem novas capacidades, são atividades que estarão em pauta em cenários como esses. Ajustes se farão necessários a fim de adequar tais aplicações a características específicas de um release ou produto, algo que em um projeto mal estruturado exigirá um esforço extra dos profissionais envolvidos.
Outro ponto crítico em aplicações legadas diz respeito à escalabilidade e às alternativas para hospedagem destas. Muitos projetos foram concebidos sem a visão da utilização em nuvem ou desconsiderando questões como concorrência em seu uso, valendo-se de práticas como cache em memória e possuindo um forte acoplamento com servidores de aplicações (como JBoss ou Internet Information Services - IIS).
Modernizando aplicações legadas
Uma readequação de nossas aplicações legadas deverá se guiar pelo uso de recomendações de disciplinas como a Orientação a Objetos, além de princípios que integrem abordagens como a metodologia The Twelve-Factor App. Conseguiremos a partir disto chegar a aplicações robustas e com um código bem estruturado, dotadas ainda de uma arquitetura plugável no que se refere a serviços de apoio.
Esta preocupação em possibilitar que se facilite a troca de um backing service por outro é fundamental não apenas por visar a obtenção de softwares com um design que privilegie uma boa testabilidade, como também resultará em um menor acoplamento com estes serviços por meio de um código que isole a interação com os mesmos. E esse baixo acoplamento será alcançado aplicando princípios da Orientação a Objetos como a Inversão de Controle, comumente implementado através do uso de frameworks de injeção de dependências e tomando por base um desenvolvimento orientado a interfaces.
Um benefício direto de todo esse trabalho estará na facilidade em se trocar de uma tecnologia de apoio por outra. Poderemos assim tirar proveito de inúmeros projetos apoiados pela Cloud Native Computing Foundation (CNCF), valendo-se sempre que necessário de funcionalidades e capacidades que solucionem novas demandas que inevitavelmente surgirão ao longo do tempo.
O desenvolvimento de uma aplicação stateless, que não mantenha estados em memória, é primordial ao projetarmos uma aplicação robusta e escalável. A prática conhecida como cache distribuído representa uma solução a este tipo de demanda, com a adoção de um mecanismo centralizado de cache como o Redis (uma alternativa que se destaca por sua excelente performance) para atender a múltiplas instâncias de uma aplicação.
Projetos cloud native têm na containerização uma importante escolha estrutural, sempre com vistas a diminuir a dependência destas soluções para com recursos específicos dos ambientes em que estejam hospedadas. A possibilidade de modernizar um legado transformando o mesmo em uma aplicação auto-contida (self-contained) trará grandes vantagens no que se refere a questões de infraestrutura. Esta abordagem evitará uma forte ligação (lock-in) com servidores de aplicação específicos, ao mesmo tempo em que a aplicação poderá se beneficiar das vantagens advindas da containeirização.
Cloud providers como Amazon Web Services (AWS), Google Cloud e Microsoft Azure podem também trazer vantagens neste processo de modernização. A existência de inúmeras alternativas gerenciadas nestas soluções possibilita que se acelere a adoção da containerização em monolitos/legados, além de facilitar o uso de inúmeros projetos cloud native.
Soluções Cloud Native em monolitos: um caminho para modernizar aplicações
A Cloud Native Computing Foundation em seu trabalho de promover iniciativas open source multiplataforma apoia diversos projetos que se enquadram dentro da noção de cross-cutting concerns. Podem ser destacadas aqui soluções de apoio voltadas ao monitoramento de aplicações (Prometheus), comunicação remota para intercâmbio de dados (gRPC) e voltadas a logging/tracing (OpenTelemetry).
Um bom design de código, com aplicações bem projetadas e que se valem de princípios consagrados dentro da área de software (Responsabilidade Única, Inversão de Controle) facilitará em muito a adoção de soluções Cloud Native como estas. O empenho em evoluir tais projetos do ponto de vista tecnológico tende então a ser menos caótico, possibilitando assim sua modernização com a adoção de alternativas amplamente utilizadas dentro do mercado.
E com monolitos bem estruturados podemos ir mais além, tirando proveito de outras soluções promovidas pela Cloud Native Computing Foundation. Nada impede que um monolito seja publicado em um cluster Kubernetes, fazendo até mesmo uso de soluções voltadas a deployment (Helm) ou escalabilidade (KEDA).
A adoção de tecnologias Cloud Native em monolitos também trará benefícios em novos projetos. A vivência com estas soluções de apoio simplificará sua adoção no desenvolvimento de novas aplicações ou, até mesmo, na reformulação de softwares já existentes (como a evolução de um monolito para uma arquitetura mais sofisticada).
Já discuti os diferentes aspectos que envolvem soluções cloud native em outro artigo deste blog:
O que é Cloud Native? E o que não é… | Arquitetura em Nuvem
O uso de projetos cloud native também já foi abordado em 2 lives do Canal .NET, com os vídeos disponíveis para se assistir gratuitamente no YouTube:
Conclusão
Aplicações construídas como monolitos representam um grande desafio quando consideramos necessidades envolvendo a adoção de novas tecnologias. Um código bem estruturado facilitará a condução deste tipo de demanda, tornando inclusive mais fácil a modernização de aplicações através do uso de soluções cloud native de larga aceitação.
A modernização de aplicações legadas demandará grandes esforços, sobretudo quando da tentativa de adaptar tais soluções a cenários cloud native. Diretrizes extremamente úteis para a restruturação de projetos de software podem ser encontradas em metodologias como The Twelve-Factor App e em princípios da disciplina de Orientação a Objetos, de maneira que estas aplicações consigam evoluir sem grandes percalços.
Referências
Monolith First | Martin Fowler
Microservices vs Monolithic Architecture | MuleSoft
Cross-cutting concern | Wikipedia
The Single Responsibility Principle | The Clean Code Blog by Robert C. Martin (Uncle Bob)
Inversion of Control | Martin Fowler
Graduated and incubating projects | Cloud Native Computing Foundation