Precisamos falar sobre complexidade

Depois de um tempo sem publicar, estou de volta aos artigos com um foco um pouco diferente, compartilhando reflexões sobre desenvolvimento de software e sobre algumas tecnologias “novas” (pelo menos para mim) com as quais tenho trabalhado com mais frequência.
Um ponto, no entanto, tem me incomodado: a crescente complexidade no desenvolvimento de software, muitas vezes justificada em nome da escalabilidade. Frequentemente, isso decorre de otimizações prematuras ou simplesmente da propaganda exagerada em torno de uma nova ferramenta promovida por grandes empresas ou influenciadores. Depois de ver muitos projetos se tornarem monumentos de padrões arquiteturais, dignos de serem explorados por Indiana Jones em um de seus filmes, decidi escrever este artigo, incluindo não apenas minhas opiniões, mas também de pessoas envolvidas em ótimas soluções e ferramentas que priorizam a simplicidade em vez da complexidade para os desenvolvedores.
O que me motivou a escrever isso?
Ultimamente, tenho trabalhado um pouco mais com JavaScript/TypeScript na minha rotina diária e com PHP em alguns projetos pessoais, enquanto continuo estudando e trabalhando com .NET. Recentemente, me deparei com um episódio de podcast com Taylor Otwell, o criador do Laravel, e algo que ele disse me chamou muito a atenção: a falta de simplicidade no desenvolvimento de software está se tornando menos um efeito colateral e mais uma característica, muitas vezes justificada pelo hype em vez de uma necessidade real. Abaixo está o link para o podcast, onde os destaques já apontam para isso como o foco principal da conversa…
Taylor Otwell: What 14 Years of Laravel Taught Me About Maintainability
E a citação do trecho específico a seguir:
Software, he said, should be “simple and disposable and easy to change.” Some problems are genuinely complex, but in general, if a developer finds a “clever solution” which goes beyond the standard documented way in a framework such as Laravel or Ruby on Rails, “that would be like a smell.”
Tradução:
Software, ele disse, deve ser “simples, descartável e fácil de mudar.” Alguns problemas são genuinamente complexos, mas, em geral, se um desenvolvedor encontra uma “solução engenhosa” que vai além da forma documentada padrão em um framework como Laravel ou Ruby on Rails, “isso seria como um code smell.”
Trazendo um pouco para a realidade e também evitando uma generalização, obviamente que temos diversos cenários onde realmente a arquitetura de software realmente demanda soluções mais complexas mas que atendam a alta demanda do negócio, como em marketplaces como Amazon e Mercado Livre, Netflix, Prime Video e outros.
Mas, quanto de nossos projetos realmente chegam perto das necessidades de escalabilidade de um marketplace ou Streaming?
A Hype em torno de novas ferramentas e a otimização prematura
Sempre que uma nova ferramenta ou uma nova versão de uma já existente é lançada, a internet se inunda de artigos e vídeos sobre ela, mostrando como usá-la em exemplos simples e como ela tornará tudo mais fácil e escalável para milhares de usuários. Até eu, em uma fase mais jovem e entusiasmada, costumava criar conteúdo sobre novas ferramentas mais focado nos benefícios que eu enxergava do que no uso cuidadoso delas. E… não me interpretem mal, não é ruim, é muito bom aprender coisas novas, estar preparado para projetos pequenos e grandes… mas é realmente importante saber o que e quando usar cada coisa.
Já vi diversos projetos e soluções que certamente tinham grande potencial de crescimento — e muitos deles cresceram —, mas não a ponto de justificar a complexidade de usar padrões como CQS, CQRS, bibliotecas como MediatR ou mesmo configurações infinitas dentro dos mecanismos de injeção de dependência da plataforma escolhida, seja .NET, JVM, PHP ou qualquer outra. Além disso, tenho visto algumas pessoas migrando do MediatR, por exemplo, devido a mudanças recentes em seu modelo de licenciamento.
Tomando o MediatR como exemplo, trabalhei em um projeto onde cerca de 60% dele era relacionado a APIs CRUD, com muitos comandos e objetos de consulta. Depurar não era um pesadelo, mas um pouco complicado em comparação com o que poderia ser. Refatorar para usar manipuladores de comandos ou padrões de serviço diretamente não só reduziu a complexidade do código, como também melhorou o desempenho da aplicação, já que nenhuma reflexão era usada, apenas chamadas simples a serviços, principalmente porque a maioria das entidades não disparava eventos de domínio. Estava sendo usado como um roteador, função que os controladores já desempenhavam.
Ao projetar software, você deve começar entendendo o negócio:
-
Como funciona? Quem são os clientes? Quais são seus problemas?
-
Qual é o seu potencial de lucro?
-
Quais são as principais prioridades?
-
O que o cliente está pedindo é realmente necessário?
Mais do que simplesmente embelezar seu castelo arquitetônico, você precisa entender se vale a pena construí-lo, quem vai morar lá e se uma casa simples ou um pequeno edifício seria suficiente.
Simpels e descartável
Este é o principal pecado de todos nós, desenvolvedores: pensar que nosso código é intocável, que é o “Santo Graal” e que deve ser feito para durar anos sem alterações em seus padrões, e que deve impedir qualquer possibilidade de mudança; em poucas palavras, que deve fazer milagres.
É claro que o software deve ser feito de forma a ser escalável, de fácil manutenção e seguro. Mas, além da segurança, que hoje em dia é algo inegociável, exigido desde o primeiro dia, a escalabilidade é definida não apenas pelo uso de todas as melhores práticas e padrões em conjunto, mas sim pelo uso deles em conjunto com as necessidades do negócio.
A facilidade de manutenção também é algo que, às vezes, ao tentar prever e tornar o software “adaptável” a todos os cenários possíveis, criamos um Frankenstein tentando facilitar a vida dos futuros mantenedores, mas, em vez disso, causando-lhes pesadelos após um árduo dia de trabalho.
Quem nunca viu esta imagem sobre a criação de um MVP?
Imagem que mostra as etapas de criação de um MVP (Produto Viável Mínimo), pensando no valor entregue aos usuários em cada iteração, e não apenas no final.
A arquitetura/design de software segue os mesmos princípios: entregar valor ao negócio, mas também manter as estruturas preparadas para mudanças com o mínimo de esforço possível. Às vezes, as mudanças exigem mais esforço, outras vezes menos… Depende de como o negócio muda e do impacto que isso terá nos usuários. O processo deve ser orgânico, com revisões e ajustes constantes. Às vezes, parte do código precisa ser descartada, e isso não é o fim do mundo. Não significa que você escreveu algo ruim, mas sim que algo deve abrir espaço para novas funcionalidades e melhorias.
O Laravel foi um ótimo exemplo de como arquiteturas complexas podem ser problemáticas em determinado período, como quando Taylor foi forçado a reescrever boa parte do núcleo do Laravel, introduzindo diversas mudanças que quebraram a compatibilidade, a fim de simplificar e facilitar a evolução. Esse evento o marcou e será lembrado, mas o ponto principal é que ele aprendeu que, com o tempo, simplificar e facilitar a evolução é fundamental. E podemos ver como isso impactou positivamente os resultados, com o Laravel chegando à versão 12 e um ecossistema robusto ao seu redor.
Outro bom exemplo foi o .NET/ASP.NET: estruturado em torno de muitas bibliotecas monolíticas e gigantescas, como System.Web, com dependências diretas das bibliotecas do Windows. O .NET Core/ASP.NET Core quebrou esse ciclo dividindo toda a estrutura em módulos menores, removendo as dependências diretas do sistema operacional e tornando-o disponível em outros sistemas operacionais, como Linux, macOS, Android e até mesmo no seu navegador, usando tecnologias como WebAssembly.
Algumas dicas antes de encerrar
Quero finalizar este artigo convidando você a refletir sobre as questões que levantei a respeito do desenvolvimento de software, mas também com algumas dicas que utilizo diariamente quando um novo projeto ou um projeto existente chega à minha mesa para ser analisado:
-
Receitas arquiteturais encontradas na internet não são a solução para todos os problemas. É preciso estudar e conhecer diferentes padrões, investigar novas soluções. Se o negócio enfrenta um desafio, ele também será um desafio para você.
-
Não seja vaidoso/não se apegue ao seu código, pois ele nunca será alterado ou guardado em uma prateleira como um troféu. Códigos serão escritos, aprimorados, corrigidos e, às vezes, descartados. O que funciona perfeitamente hoje pode não ser a melhor solução amanhã. Lembre-se de que estamos sempre aprendendo e aprimorando coisas em nossas vidas. O mesmo se aplica ao nosso código.
-
Uma linguagem ou ferramenta não resolverá necessariamente o seu problema só porque você acha que ela é melhor do que outra. Cada linguagem, framework/ferramenta escala à sua maneira, com seus próprios benefícios e desafios. Certa vez, pensei que substituir Java por GoLang em um desafio específico de enviar um arquivo zip enorme para o navegador resolveria o problema. Passei horas aprendendo Go e reescrevendo um pequeno microsserviço e, no final das contas, implementei uma forma de transmitir o arquivo enquanto ele estivesse pronto, adicionando novos arquivos ao fluxo ZIP. A maior parte do trabalho de um desenvolvedor não é código… São 30% do trabalho… 70% do tempo você estará conversando com as partes interessadas, pensando em soluções para os problemas delas, como fazer isso mantendo o software escalável, mas simples. Programar é a parte mais fácil… Saber o que programar é o desafio.
E agora, eu quero saber a opinião de vocês sobre isso. Por favor, comentem com seus feedbacks e opiniões. Vamos discutir e melhorar o desenvolvimento de software, um dia de cada vez.
Por enquanto é só, vejo vocês no próximo post, trazendo algumas novidades que aprendi lidando com desafios no desenvolvimento de software!