Refatoração de código legado em C++: guia passo a passo
Trabalhar com código legado em C++ é uma das tarefas mais difíceis, mas inevitáveis, na carreira de um desenvolvedor. O código legado frequentemente carece de testes unitários, contém números mágicos, vazamentos de memória e dependências confusas. No entanto, a refatoração desse código sem o risco de quebrar o sistema é possível se seguirmos estratégias comprovadas. Neste artigo, vamos analisar as principais abordagens para refatorar código C++ legado com exemplos práticos.
1. Preparação: criando uma rede de segurança
Antes de alterar o código, é necessário criar uma "rede de segurança" de testes. Para C++, isso é especialmente importante, pois erros podem levar a comportamento indefinido ou falha do programa.
Passo 1.1: Teste de caracterização
Escreva testes que capturem o comportamento atual do código, mesmo que esteja incorreto. Use frameworks como Google Test ou Catch2.
// Exemplo de teste de caracterização para uma função legada#include <gtest/gtest.h>
// Função legada que vamos refatorarint calculatePrice(int quantity, float basePrice) { if (quantity > 100) { return static_cast<int>(basePrice * quantity * 0.9); } else { return static_cast<int>(basePrice * quantity); }}
TEST(PriceCalculatorTest, LegacyBehavior) { EXPECT_EQ(calculatePrice(50, 10.0f), 500); EXPECT_EQ(calculatePrice(150, 10.0f), 1350); // 150*10*0.9}Passo 1.2: Integração em CI/CD
Adicione os testes ao sistema de integração contínua. Isso garante que as alterações não quebrem a lógica existente.
2. Estratégia: Refatoração "Inteligente" sem Dor
Não tente reescrever tudo de uma vez. Use uma abordagem incremental.
2.1. Extração de Interfaces (Extract Interface)
Se o código está fortemente acoplado a estados globais ou classes específicas, comece extraindo abstrações.
Problema: A classe trabalha diretamente com o sistema de arquivos.
// Código legadoclass DataProcessor {public: void process(const std::string& filename) { std::ifstream file(filename); // lógica complexa de processamento }};Solução: Introduzimos uma interface e injeção de dependência.
// Nova interfaceclass IFileReader {public: virtual std::string read(const std::string& path) = 0; virtual ~IFileReader() = default;};
// Refatoraçãoclass DataProcessor {public: explicit DataProcessor(std::unique_ptr<IFileReader> reader) : reader_(std::move(reader)) {} void process(const std::string& filename) { auto data = reader_->read(filename); // lógica de processamento }
private: std::unique_ptr<IFileReader> reader_;};2.2. Substituição de números mágicos por constantes
Código legado frequentemente contém números cujo significado não é claro.
// Antes da refatoraçãoif (buffer.size() > 4096) { resizeBuffer(8192);}
// Após a refatoraçãoconstexpr size_t kMaxBufferSize = 4096;constexpr size_t kResizeTarget = 8192;
if (buffer.size() > kMaxBufferSize) { resizeBuffer(kResizeTarget);}3. Combate a vazamentos de memória e construções inseguras
Código C++ legado frequentemente usa ponteiros brutos e gerenciamento manual de memória. Substitua-os por ponteiros inteligentes e wrappers RAII.
3.1. Substituição de new/delete por std::unique_ptr
// Código antigo (risco de vazamento em caso de exceção)void processData() { Data* data = new Data(); // ... trabalho com data ... delete data;}
// Novo código (seguro)void processData() { auto data = std::make_unique<Data>(); // ... trabalho com data ... // delete não é necessário}