♻️

(PT) Reflexões Sobre Trabalho Eficaz com Código Legado

Projetos de software são criados para durar décadas. Se um projeto alcança esse tempo de vida, há grandes chances de boa parte dele — senão a maioria — ser considerada código legado. Aliás, mesmo projetos jovens já podem conter código legado. Por isso, acredito que seja essencial para qualquer programador saber lidar com esse tipo de situação.
Em busca de melhorar nessa área, li o livro Trabalho Eficaz com Código Legado, de Michael C. Feathers. Nele, encontrei diversas técnicas e padrões para realizar alterações seguras em código legado. No entanto, o que mais me impactou foram as reflexões que compartilho neste post. O conteúdo do livro se conectou perfeitamente com sistemas nos quais venho trabalhando, e essa combinação certamente contribuiu para meu amadurecimento como desenvolvedor.

O Que é Código Legado?

Um projeto de software não é como um prédio que se deteriora com o tempo. Se o software foi implementado de maneira escalável e livre de bugs há 10 anos e nunca mais foi alterado, ele continuará funcionando conforme o esperado¹. O verdadeiro problema surge quando o código é modificado de forma inadequada.
A introdução de código mal projetado é quase inevitável no ciclo de vida do software. As chances de isso ocorrer aumentam significativamente quando o código que estamos alterando carece de documentação, possui arquitetura confusa, nomes inadequados e, principalmente, ausência de testes automatizados.
Portanto, uma definição prática para código legado poderia ser: código difícil de entender e modificar sem introduzir novos bugs.
¹ Exceto em casos de falhas de hardware ou de segurança desconhecidas na época de sua criação.

Entenda Antes de Alterar

No dia a dia, conforme surgem novas demandas, é necessário fazer uma análise cuidadosa de como implementá-las no sistema. Essa análise precisa encontrar um equilíbrio: se for muito detalhada, atrasamos a entrega de valor para o usuário; se for superficial, corremos o risco de esquecer detalhes importantes e comprometer a qualidade ou a viabilidade da entrega.
Desenvolvedores com maior conhecimento do projeto conseguem ser mais ágeis e precisos nessas análises. No entanto, ao trabalhar com código legado, estamos sempre pisando em terreno delicado. Confiar apenas na experiência pode nos levar a ignorar comportamentos inesperados, obscuros e não intuitivos que podem comprometer o planejamento. Às vezes, conseguimos identificar esses problemas durante o ciclo de desenvolvimento e corrigir a rota antes da entrega. Mas, em outras ocasiões, só percebemos o erro quando o usuário final relata que algo deixou de funcionar — e isso acontece porque código legado geralmente não possui testes automatizados para barrar a publicação de código problemático.
Para evitar esses problemas, é fundamental entender o comportamento atual do código. Se os desenvolvedores não têm confiança suficiente na análise, o primeiro passo deve ser gastar tempo entendendo como os componentes do sistema se conectam. Michael Feathers apresenta várias estratégias para realizar esse estudo. Uma das mais interessantes e poderosas é a técnica das refatorações transitórias, que são alterações temporárias no código, sem compromisso de mantê-las, feitas apenas para obter clareza sobre determinada área do sistema.
Desde que adotei essa prática, tenho o cuidado de concluir uma análise apenas quando estou seguro de que compreendi o sistema o suficiente para fazer alterações. Isso permite que meu time tenha clareza sobre o que está se comprometendo e estabeleça prazos mais realistas.

Trabalhe com Feedback Confiável

Modificar código sem cobertura de testes é arriscado; modificar código legado sem testes é uma receita para o desastre. Quando você altera algo que mal compreende, como pode ter certeza de que o impacto será apenas o desejado?
Testes manuais são ajudam nessa tarefa, mas estão sujeitos a falhas humanas, facilitando a omissão de partes do sistema afetadas pelas mudanças. Testes automatizados, por outro lado, garantem que os mesmos comportamentos sejam verificados repetidamente e ajudam a identificar possíveis impactos. Além disso, quando bem feitos, eles servem como uma rede de segurança, detectando efeitos colaterais não intencionais e garantindo que áreas críticas do sistema não sejam afetadas por acidente. Os testes automatizados também orientam o design do código, funcionando como os primeiros "clientes" das novas implementações e dando uma prévia de como o sistema utilizará as mudanças.
Por mais confiante que eu esteja em relação a uma alteração, só a faço se a área do código estiver coberta por testes. Se não houver cobertura, realizo o mínimo de refatorações necessárias para permitir os testes e uso esboços de efeitos para mapear os testes que precisam ser criados. Esses esboços, também descritos no livro de Feathers, são fluxogramas simples que ajudam a entender quais partes do sistema são afetadas por mudanças específicas. Dessa forma, construo uma rede de segurança que previne a introdução de bugs e melhora o projeto de forma gradual.

Quebre Regras de Boas Práticas, Se Necessário

Em projetos de código legado, enfrentamos inúmeros desafios. Muitas vezes, a complexidade e a falta de clareza dificultam a compreensão de várias partes do sistema. Nesse contexto, as boas práticas de desenvolvimento precisam ser reconsideradas e adaptadas à realidade.
Imagine que você está desenvolvendo uma nova funcionalidade. Ao finalizar grande parte do trabalho, percebe que precisa adicionar um comportamento a uma classe já existente. Você encontra um método que parece perfeito para isso, mas ele já está sobrecarregado e confuso. O que fazer? Alterar esse método, correndo o risco de piorá-lo, ou criar um novo método que possa ser chamado quando necessário?
Existem dois caminhos ao modificar um método: a melhoria ou a piora. A melhoria envolve refatorar o código e criar testes automatizados, o que demanda mais tempo, mas reduz a dívida técnica. Já a piora consiste em simplesmente adicionar o novo comportamento de maneira rápida e desleixada, sacrificando a clareza e a manutenção futura do código.
A longo prazo, o caminho da melhoria é sempre o mais vantajoso. No entanto, quando a refatoração não é essencial para a entrega em questão, insistir nela pode atrasar o projeto, gerar pressão e aumentar as chances de decisões equivocadas. Pior ainda, ao introduzir mudanças em métodos não relacionados diretamente à funcionalidade principal, há um risco maior de criar bugs que passarão despercebidos, já que não haverá testes automatizados para cobrir essas alterações. Nesses casos, a criação de um novo método, separado e focado na nova funcionalidade, pode ser uma solução mais segura e eficiente, evitando bugs e mantendo a integridade do design.
Essa abordagem não se aplica apenas à adição de novas funcionalidades. Às vezes, vale a pena quebrar regras de encapsulamento, por exemplo, quando é impossível testar uma classe de forma simples sem acessar membros privados. Da mesma forma, pode ser necessário introduzir código de produção apenas para permitir a criação de testes. Quando o tempo é curto, um código simples e testado é preferível a um código complexo com “boas práticas” mal aplicadas.

Respeite o Código

No início da minha carreira, sempre que me deparava com código legado, eu tendia a refatorá-lo quase que imediatamente. Eu lia o código, interpretava seu funcionamento e começava a alterá-lo para algo "melhor". No entanto, com o tempo e experiência, percebi o quão irresponsável essa abordagem era.
Mesmo que um projeto esteja cheio de código legado, se ele ainda está em uso, é porque continua gerando valor para as pessoas. Alterar esse código sem primeiro entender o domínio do sistema, quem são seus usuários e como cada parte se conecta pode levar a decisões de design equivocadas. Além disso, a falta de compreensão profunda aumenta drasticamente o risco de introduzir novos bugs.
Outro pensamento comum ao lidar com código legado é “por que não reescrever isso aqui tudo do zero?”. A princípio, essa ideia parece atrativa, mas a reescrita completa de um sistema exige um grande esforço da equipe de desenvolvimento, geralmente deixando outras prioridades, como novas funcionalidades, em segundo plano. Isso pode fazer o produto estagnar. Além disso, a reescrita nunca será uma simples cópia do sistema original (até porque, se fosse, qual seria a vantagem?). Ao tentar recriar o sistema, é quase certo que você esquecerá alguma regra obscura e introduzirá bugs que já haviam sido corrigidos no código legado. Como resultado, o projeto não só deixará de evoluir, como provavelmente irá piorar. (Compreendi bem essa armadilha ao ler um post do Joel on Software.)
Depois de entender melhor essas dinâmicas, passei a respeitar mais o conhecimento acumulado no código, mesmo quando ele segue uma "arquitetura espaguete". Melhorar o código é essencial para garantir a longevidade do sistema, mas isso deve ser feito com cautela e respeito ao que já foi construído.