Trabalhando com código legado sem enlouquecer

Acredito que a maioria dos desenvolvedores que estão há um tempo no mercado já teve que lidar com código escrito em tempos remotos, feito por pessoas que provavelmente ninguém mais tem contato, usando frameworks antigos, com padrões de código esquisitos… Parece familiar?

Normalmente chamamos todo código antigo de Código Legado, ou seja, aquele código que está em produção mas ninguém sabe bem quem foi que fez ou quem foi o responsável pelas decisões. Mas e se o código antigo for bom? Se estiver bem escrito, com testes, claro em propósito e funcionando bem?

Então, definir código legado apenas por antiguidade não parece um fator muito determinístico. Recentemente, Michael Feathers deu uma definição nova para código legado em seu livro Working Effectively with Legacy Code (Trabalhando eficientemente com Código Legado, em tradução livre):

Código legado é aquele que não tem testes.

Por que usar essa definição? É possível argumentar que existem códigos bons sem testes e que existem códigos bem ruins que têm testes. Portanto, o que levamos mais em conta ao considerar um código como legado é o risco de alterar alguma coisa.

Trabalhando no legado

Suponha que você seja responsável por criar uma funcionalidade nova nesse código legado. Como saber se o código que você introduzir (ou retirar) não vai fazer com que funcionalidades antigas deixem de funcionar?

E se você tiver que corrigir um bug, como garantir que realmente o corrigiu e que o resto do código continua com o comportamento esperado?

E se você achar uma função ou uma classe que esteja muito grande e confusa, e queira refatorar, como proceder?

Percebe o drama? Para você, que não é expert no código que está trabalhando, possa ser produtivo, é necessário confiança de que os cenários que funcionavam antes devem continuar funcionando, e é isso que os testes bem escritos te garantem.

E como ter confiança?

Se testes garantem o funcionamento esperado do código em cenários conhecidos, o primeiro passo para ser capaz de modificar código legado é escrever alguns testes. Mas como escrever testes sem conseguir entender exatamente o que está sendo feito?

Os Testes de Caracterização são testes que buscam aliviar justamente os casos em que testes unitários não existem. O objetivo de um teste de caracterização é garantir que o comportamento de uma função ou trecho de código se mantenha. Então, para criar um teste desse tipo, é necessário descobrir qual o valor de saída da função a ser testada para um conjunto de valores de entrada. Dando um exemplo, suponha que você encontre a função abaixo:

Mesmo num caso simples desses, se o número de operações for grande ou obscuro, não é simples de entender qual o objetivo da função, ou o que ela está fazendo exatamente. Para fazer um teste de caracterização dessa função, precisamos descobrir o que acontece com o valor de b quando modificamos o valor de a. Vamos supor que usando valores {2, 10, 100} para a, temos como saída (valores de b) os valores {5, 20, 30}. Com isso podemos fazer testes para cada combinação dessa:

Exemplo em alto nível de testes para a função fazAlgumaCoisa

Repare que esse tipo de testes não cobre tudo o que a função fazAlgumaCoisa faz, mas te dá uma segurança maior para alterá-la, pois agora você já consegue provar que em 3 cenários diferentes o resultado se manteve o mesmo. Caso alguma alteração seja feita e um desses testes falhar, você já sabe que essa alteração não foi certa — em situações de código legado, assuma que o comportamento existente é o correto, ao menos até você ter mais domínio do contexto do código.

Melhorando o código existente (Refatoração)

Agora que você tem seus testes de caracterização, você pode fazer com que o código existente não seja mais legado, isto é, que ele tenha testes! Nem sempre as funções presentes em nossos códigos estão pequenas e claras o suficiente, é normal que elas possam ser refatoradas em funções menores.

No caso da fazAlgumaCoisa, podemos por exemplo escrever algo assim:

O que fizemos? Extraímos a primeira linha de código para uma nova função, a elevaAoQuadrado. Podemos rodar nossos testes de caracterização e ver que os resultados são os mesmos de antes, então nossa mudança não alterou o funcionamento do código. E agora, podemos escrever testes unitários para nossa nova função, garantindo que ela funciona da maneira esperada. No nosso caso de exemplo, a função elevaAoQuadrado é muito simples, normalmente as funções extraídas teriam mais um pouco de lógica de negócio, mas essa é a ideia central.

Dessa forma, quando alguma outra pessoa vier olhar esse código no futuro, estará mais separado e coberto por testes, aí adicionar novas funcionalidades ou alterar algum comportamento será uma tarefa bem mais simples :).

Sempre deixe o código melhor do que você o encontrou.
— Regra dos escoteiros para programação

Corrigindo defeitos ou alterando comportamento

E se, ao invés de adicionar funcionalidades, você precisasse corrigir um defeito ou alterar a execução de algum cenário? Assumindo que o que você tem que alterar não está claro, a melhor forma de atacar esse problema é usar uma técnica chamada Estrangulamento de código (Strangler pattern, em inglês). A ideia dessa abordagem é ir ganhando confiança em partes do código, para conseguir localizar o defeito de maneira mais certeira e eficiente.

Isso quer dizer que, caso você tenha um cenário com comportamento errado, você como desenvolvedora/desenvolvedor tentará navegar pelo fluxo de execução do código, seja debuggando ou mesmo através de um editor. Mas, se o código estiver com vários níveis de abstração, se perder é só questão de tempo. Nesses casos, vamos usar as duas técnicas descritas acima — Testes de Caracterização e Refatoração.

O primeiro passo é criar os testes de caracterização, como já discutimos, para ter certeza que suas mudanças não estão mudando o comportamento do código, apenas tornando-o mais limpo. Depois disso, cada vez que você sentir que uma refatoração é necessária (uma função muito grande, ou com muitas responsabilidades, ou mal escrita), você deve garantir que os testes de caracterização continuam passando e deve escrever novos testes para essa função alterada. Se você não encontrou o defeito nessa função, você pelo menos agora tornou ela melhor e sabe que o defeito está no resto do fluxo.

Dessa maneira, você garante que o defeito não está no código que já foi refatorado, aumenta a confiança no código da funcionalidade e deixa o defeito cada vez mais “encurralado” no código. Além disso, toda vez que for necessária uma nova mudança, todas as partes daquele fluxo já estarão testadas e fáceis de alterar.


Você já trabalhou com código legado? O que achou das técnicas mencionadas? Alguma outra que queira mencionar?