A Refatoração como Técnica de Detecção de Bugs 🔍🪲
A refatoração nos permite notar detalhes antes ignorados.
Em meio ao emaranhado complexo de funções, classes e componentes de um código, os engenheiros e engenheiras de software enfrentam desafios constantes ao tentar manter a qualidade e a eficiência de seus projetos. Bugs são inevitáveis no desenvolvimento de software, surgindo muitas vezes em áreas inesperadas e provocando efeitos indesejados que podem prejudicar a funcionalidade e a experiência do usuário. Consequentemente, a detecção e correção de bugs são partes vitais do ciclo de vida do desenvolvimento de software.
Nesse cenário, a refatoração emerge como uma técnica crucial. É o processo de modificar o código para melhorar a sua estrutura interna, sem alterar o seu comportamento externo. É um conceito muito importante, sendo muito mais que uma mera tarefa de "limpeza" - a refatoração é uma oportunidade para melhorar o design, a performance, testabilidade e a manutenibilidade do código.
E mais que isso, a refatoração pode ser um aliado poderoso na detecção de bugs. Durante o processo de revisão e reestruturação do código, os programadores podem descobrir problemas ocultos e sutilezas que passaram despercebidas durante a codificação.
Portanto, o objetivo deste texto é explorar como a refatoração de código pode ser utilizada como uma técnica eficaz para a detecção de bugs, demonstrando através de exemplos práticos e discutindo as implicações e benefícios desse processo para as equipes de engenharia de software. Vamos nessa!
Por que falar sobre refatoração de código?
Bom podemos responder essa pergunta com a frase abaixo:
O código é um passivo, não um ativo.
O que isso significa?
Essa frase pode ser um pouco confusa, mas se pensarmos no código como uma casa, talvez possamos entender melhor.
Imagine que você comprou uma casa. Inicialmente, pode parecer um ativo, um bem que tem valor. No entanto, qualquer proprietário de imóvel sabe que a casa exige manutenção contínua. O telhado pode precisar ser substituído, a pintura pode começar a descascar. Se você ignorar esses problemas, eles só vão piorar com o tempo, podendo até mesmo levar a danos estruturais que reduzem drasticamente o valor da sua casa. Em certo sentido, sua casa é um "passivo" - ela constantemente exige seu tempo, esforço e dinheiro para mantê-la em boas condições.
Da mesma forma, o código que escrevemos para projetos de software é muito semelhante a essa casa. Pode parecer um ativo no começo - afinal, é o que permite que nossos programas funcionem e as soluções produzidas com esse código geram muitas vezes lucro. Mas assim como uma casa, o código requer manutenção contínua.
Bugs precisam ser corrigidos, a lógica pode precisar ser atualizada à medida que os requisitos mudam, e à medida que o sistema cresce, o código que era perfeitamente adequado no passado pode se tornar um obstáculo para a adição de novos recursos. Se ignorarmos aspectos cruciais de qualidade e apenas vamos introduzindo novos recursos no código sem planejamento e boas práticas, vamos sabotando o software com o tempo, tornando o código cada vez mais difícil de entender, modificar e principalmente testar!
E é nesse momento que a refatoração entra em cena. Assim como reformar uma casa para mantê-la em boas condições, a refatoração é o processo de organizar (praticamente melhorar) o código para mantê-lo coeso. Ela nos permite corrigir problemas de design de código, acoplamento entre outros antes que se tornem críticos, torna o código mais fácil de entender e modificar e garante que nosso "passivo" de código não nos custe mais do que deveria em esforço e produtividade.
Portanto, embora possamos pensar inicialmente no nosso código como um ativo, é útil considerá-lo como um passivo - algo que exige manutenção contínua para manter seu valor.1
É por isso que precisamos sempre falar sobre refatoração. Você tem utilizado essa técnica a seu favor? Não? Como então podemos fazer isso? Vamos entender!
Como utilizar essa técnica ao nosso favor?
Um dos principais benefícios da refatoração é que ela pode ajudar na detecção de bugs no código. Como é isso possível? Para entender isso, precisamos primeiro olhar para a natureza dos bugs de software. Os bugs são essencialmente defeitos no código que causam comportamentos indesejados. Muitas vezes, esses defeitos são o resultado de suposições incorretas, mal-entendidos ou erros de programação. Vamos esclarecer isso:
Suposições incorretas: Ocorre quando um desenvolvedor fez uma suposição sobre como algo deve funcionar, mas essa suposição estava errada. Um exemplo clássico é supor que uma lista de itens sempre terá pelo menos um item. Isso pode levar a um bug se a lista estiver vazia, porque o código pode tentar acessar o primeiro item da lista e, em vez disso, gerar um erro porque não há itens para acessar.
Mal-entendidos: Às vezes, um programador pode simplesmente não entender completamente como uma parte específica do código deve funcionar, ou como uma determinada função ou classe deve ser usada. Por exemplo, talvez um método retorne um valor nulo em certas circunstâncias, mas o desenvolvedor pensou que sempre retornaria um valor válido. Quando o método retorna nulo, isso pode causar um erro "objeto nulo" quando o código tenta usar o valor retornado.
Erros de programação: Esses são os "erros" no sentido mais clássico. Talvez o desenvolvedor digitou algo errado, esqueceu de inicializar uma variável, usou o operador errado em uma expressão, ou cometeu algum outro erro simples. Esses tipos de erros podem ser difíceis de detectar, porque o código pode parecer correto à primeira vista, mas não se comporta da maneira esperada.
Agora vamos para um exemplo simples, mas que provavelmente todo desenvolvedor já deixou passar.
// Código inicial em C#
public class Calculadora
{
public int Dividir(int numerador, int denominador)
{
return numerador / denominador;
}
}
Este é um exemplo simples de uma função que divide dois números. No entanto, há um bug oculto aqui, que passou despercebido pelo programador: a função não leva em conta a possibilidade de o denominador ser zero. Durante a refatoração, um programador(a) atento pode perceber isso e corrigir o bug.
// Código refatorado em C#
public class Calculadora
{
public int Dividir(int numerador, int denominador)
{
if(denominador == 0)
throw new DivideByZeroException("Denominador não pode ser zero.");
return numerador / denominador;
}
}
Alguns desenvolvedores acreditam que a refatoração só serve para deixar o código mais limpo e mais legível. Mas programadores experientes e que aplicam boas práticas de programação sabem que a refatoração vai além!
Durante a refatoração, temos a oportunidade de entender o código profundamente. Podemos revisar e ajustar o código para torná-lo mais eficiente, organizado e legível. É nesse processo que os bugs geralmente são encontrados. Quando você está realmente se aprofundando no código, tem a chance de perceber algo que não está funcionando como deveria. Como os bugs são frequentemente o resultado de erros sutis ou equívocos na lógica, essa revisão cuidadosa é uma oportunidade valiosa para detectá-los.
Mas sempre é importante lembrar, para refatorar com segurança precisamos também entender as regras de negócio e ter uma perspectiva clara do comportamento original do código que vamos refatorar. Mas esse assunto pode ficar para outro artigo.
Refatorar é crucial!
A refatoração de código é um componente crucial do processo de desenvolvimento de software. É uma prática tão importante que Martin Fowler certa vez descreveu-a como "uma técnica controlada para melhorar o design de um código existente". E, em essência, é exatamente isso.
Vamos imaginar que você está revisando um antigo álbum de fotos. Quanto mais você olha, mais você começa a notar detalhes que antes passavam despercebidos. Uma pessoa escondida ao fundo, um objeto que não estava lá antes, uma expressão facial que você não havia notado. Esses são detalhes que só são percebidos com um olhar atento, e a mesma ideia se aplica à refatoração de código.
Assim como na revisão do álbum de fotos, a refatoração nos permite notar detalhes antes ignorados. É possível que você encontre uma variável não inicializada, um loop infinito se determinada situação ou retorno ocorrer, um método que não faz o que deveria. São esses pequenos detalhes, muitas vezes negligenciados, que geralmente se manifestam como bugs.
Além disso, um código limpo e bem organizado é menos propenso a bugs. Um código bagunçado e confuso pode levar a um pensamento igualmente confuso. Erros são facilmente cometidos quando a estrutura do código é complexa e difícil de acompanhar. Ao refatorar, você está simplificando essa estrutura, tornando mais fácil para todos entenderem o que está acontecendo.
Vamos para o exemplo de uma classe de Voucher
em Csharp, onde o método IsVoucherValid
verifica se o voucher ainda é válido, o exemplo é bem simples, mas serve muito bem para propor uma linha de raciocínio e destacar a importância de praticar a refatoração após a conclusão de escrita de qualquer classe ou método:
public class Voucher
{
public string Code { get; set; }
public DateTime ExpiryDate { get; set; }
public bool IsVoucherValid()
{
if (DateTime.Now > ExpiryDate)
{
return false;
}
else
{
return true;
}
}
}
Neste caso, o código aparentemente parece correto. O método IsVoucherValid
verifica se a data atual é maior do que a ExpiryDate
do voucher e, se for, retorna false
, indicando que o voucher não é mais válido.
Entretanto, existe um problema nesse código que pode não ser imediatamente óbvio, mas durante o processo de refatoração poderia ser descoberto. A comparação de data e hora está levando em consideração a hora atual e a hora em que o voucher expira. Isso significa que se a data de expiração do voucher for hoje, mas a hora da data de expiração for antes da hora atual, o voucher será considerado inválido. Como assim?
O problema encontrado é que DateTime.Now
retorna tanto a data quanto a hora atuais até o milissegundo. Se ExpiryDate
só contiver uma data (sem um componente de tempo) ou a hora estiver definida como meia-noite (o padrão para uma data sem tempo especificado em C#), então o voucher será considerado expirado na virada do dia, mesmo que tecnicamente ainda seja o dia de expiração.
Por exemplo, se a ExpiryDate
do voucher é definida como "2023-07-06" e o cliente tenta usar o voucher às 12:01 da manhã em "2023-07-06", a condição (DateTime.Now > ExpiryDate)
será verdadeira, porque "2023-07-06 12:01:00" é maior que "2023-07-06 00:00:00", e o voucher será considerado expirado.
Se o intuito é permitir que o voucher seja usado até o final do dia de expiração, você precisa considerar isso em sua lógica de validação. Uma maneira de fazer isso é garantir que a comparação seja feita apenas na parte da data, ignorando a hora. Isso pode ser alcançado usando o método DateTime.Date
para remover a parte do tempo da data atual:
public bool IsVoucherValid()
{
DateTime currentDateWithoutTime = DateTime.Now.Date;
DateTime expiryDateWithoutTime = ExpiryDate.Date;
if (currentDateWithoutTime > expiryDateWithoutTime)
{
return false;
}
else
{
return true;
}
}
Agora, o método IsVoucherValid
está comparando apenas as partes da data, ignorando a hora. Isso garante que o voucher será válido por todo o dia da data de expiração, independentemente da hora.
O caso acima é uma situação real relatado por um colega de trabalho alguns anos atrás. Adivinhe como ele encontrou o comportamento incorreto? Quando estava refatorando! O mais engraçado é que ele não percebeu que foi graças a refatoração que foi possível detectar esse problema. E se o programador não fizesse uma refatoração, será que teria detectado esse problema na verificação? Muito dificilmente. Talvez ao testar? No code review? Tudo depende de outros fatores, mas o ponto é a refatoração, o cuidado em olhar novamente para o código buscando por pontos de melhoria ajudou a evitar um problema que afetaria o usuário final!
Ao refatorar, questione!
Enquanto estamos imersos no processo de refatoração, frequentemente nos perguntamos:
Este código faz sentido?
Ele é legível e compreensível?
Se eu voltasse a este código daqui a alguns meses, ou mesmo anos, ainda seria capaz de entender o que está acontecendo aqui?
Se um novo membro da equipe visse este código, ele seria capaz de pegar rapidamente a lógica e a estrutura?
Estas não são perguntas triviais - elas são vitais para o processo de refatoração e para a saúde a longo prazo do software.
Esse tipo de questionamento, esse pensamento crítico, é o que nos permite identificar os pontos fracos e problemáticos do nosso código. É aqui que nós detectamos os bugs, as ineficiências, os códigos duplicados e as complexidades desnecessárias.
Digamos que você está lendo um bloco de código e percebe que a lógica está confusa e emaranhada. Se você se depara com esse código e se sente confuso, é bem possível que outros desenvolvedores também se sintam assim. E a confusão é a porta de entrada para erros. Se o código não é compreendido adequadamente, as chances de introdução de bugs, durante a alteração ou extensão do código, aumentam exponencialmente.
Assim, ao questionar o código durante a refatoração, você não apenas melhora a qualidade do código, mas também cria uma oportunidade para identificar e corrigir bugs que possam ter se infiltrado nessa confusão.
Além disso, quando você se questiona sobre a legibilidade e compreensibilidade do código, está indiretamente pensando em todos os outros membros da equipe. Está tentando tornar a vida deles mais fácil. Este tipo de empatia conduz a um melhor trabalho em equipe, menos frustração e uma maior produtividade.
Por isso agora acho apropriado listar algumas perguntas que devemos fazer antes e após a refatoração:
Antes da Refatoração:
Qual é o objetivo da refatoração? Antes de começar, você deve ter um objetivo claro em mente. Isso pode ser melhorar a legibilidade do código, reduzir a complexidade, melhorar o desempenho ou corrigir um design ruim.
O código possui testes? Antes de começar a refatoração, é essencial que você tenha testes sólidos em vigor. Isso permitirá que você verifique se a funcionalidade permanece a mesma após a refatoração.
Como essa refatoração afetará outras partes do sistema? Você precisa entender as dependências do código que planeja refatorar para evitar efeitos colaterais indesejados.
Há recursos suficientes (tempo, habilidades, ferramentas) disponíveis para a refatoração? A refatoração pode levar algum tempo e esforço, então é importante considerar se você tem os recursos necessários para realizá-la.
Após a Refatoração:
Os objetivos da refatoração foram alcançados? Após a refatoração, verifique se você atingiu os objetivos que definiu inicialmente.
O código refatorado passou em todos os testes? Isso é essencial para garantir que a refatoração não tenha alterado nenhuma funcionalidade.
O código refatorado é mais fácil de entender e manter? A refatoração deve resultar em código que seja mais fácil de entender e manter para qualquer pessoa que esteja trabalhando nele.
Existem lições aprendidas que podem ser aplicadas a futuras refatorações? A refatoração é uma oportunidade de aprendizado. Após cada refatoração, é útil refletir sobre o que funcionou bem, o que poderia ter sido feito de maneira diferente e o que pode ser aplicado no futuro.
Por fim, é importante lembrar que a refatoração de código não é um processo que você faz uma vez e esquece. É um processo contínuo, uma prática que deve ser integrada em seu fluxo de trabalho diário. Quando tratada dessa maneira, a refatoração se torna um poderoso instrumento para a detecção e correção de bugs, a melhoria da legibilidade do código e a promoção de uma cultura de trabalho em equipe
Mas como isso beneficia todos na equipe?
A resposta é simples: melhora a compreensão, a comunicação e a colaboração. Cada membro da equipe, ao adotar a refatoração como parte de sua rotina diária, torna-se mais consciente de sua própria codificação e de seus colegas. Eles aprendem a questionar, a entender profundamente a lógica por trás do código, e se tornam mais capazes de detectar e corrigir bugs.
A refatoração, em última análise, é uma forma de cuidado com o código, cuidado com o trabalho dos colegas de equipe e com a qualidade do produto final. Isso cria uma cultura de responsabilidade e colaboração que vai além da detecção de bugs. Cria um ambiente onde todos são conscientes da importância da qualidade do código e se esforçam para alcançá-la.
Conclusão
Podemos concluir com as palavras de Fowler em sua palestra na [XConf Brasil 2019] - Introdução à Refatoração, a apresentação está disponivel no Youtube:
Mas a razão pela qual você quer refatorar regularmente é que você produz novas features rapidamente. É por isso. A refatoração permite que você faça coisas mais rápido com o tempo. Então, é importante sempre que você discutir sobre refatoração com alguém e tentar justificá-la, concentre-se na parte econômica. Foque no fato de que ela permite que você libere mais features mais rapidamente. Então, não é um caso de refatoração ou mais features. É refatorar para produzir mais features. - Martin Fowler
A refatoração é fundamental no ciclo de vida do desenvolvimento de software e, como Fowler enfatiza, sua utilidade vai além da mera melhoria da qualidade do código: é uma técnica que acelera a entrega de novas funcionalidades. Embora muitas vezes vista como uma tarefa que desvia a equipe de suas metas de desenvolvimento de novos recursos, é na verdade a chave para manter uma velocidade de desenvolvimento sustentável a longo prazo.
Nesse contexto, a refatoração também se destaca como uma eficiente técnica de detecção de bugs. Em um primeiro momento, pode parecer que estamos desviando recursos valiosos que poderiam ser usados para desenvolver novos recursos. No entanto, se considerarmos a quantidade de tempo que uma equipe de desenvolvimento gasta lidando com bugs, veremos que a refatoração pode, de fato, economizar tempo e esforço.
Ao reorganizar o código para torná-lo mais claro e menos acoplado, estamos tornando os erros mais fáceis de detectar e corrigir. Código limpo e bem estruturado não apenas facilita a identificação de bugs, como também torna a inserção de novas funcionalidades um processo menos propenso a gerar novos erros.
Então, quando colocamos a refatoração como uma prática regular, estamos não apenas aumentando a velocidade com que podemos adicionar novas funcionalidades, mas também estamos diminuindo a incidência de bugs e, consequentemente, o tempo gasto em sua correção. É como Martin Fowler mencionou: "Não é um caso de refatoração ou mais features. É refatorar para produzir mais features".
Portanto, a refatoração não deve ser vista como um gasto extra de tempo, mas sim como um investimento. Um investimento que permitirá uma detecção mais eficaz de bugs e, por consequência, um processo de desenvolvimento de software mais rápido e eficiente.
Agradeço muito por ler até o final e fico feliz se compartilhar esse post com outros programadores. Qualquer dúvida deixe nos comentários. Até o próximo post! 😄
Quando falamos sobre código como um "ativo" ou um "passivo", estamos utilizando uma metáfora do mundo financeiro. No mundo dos negócios, um ativo é algo que gera valor para a empresa, enquanto um passivo é algo que custa dinheiro à empresa. Quando aplicamos essa metáfora ao código de software, ela ganha uma interpretação interessante.
Se considerarmos o código como um ativo, isso implica que o código por si só traz valor. Poderíamos pensar assim porque, afinal, o código é a manifestação concreta do produto de software que, por sua vez, gera receita para a empresa. Nesse sentido, quanto mais código, mais funcionalidades e, consequentemente, mais valor.
Por outro lado, se considerarmos o código como um passivo, isso sugere que cada linha de código representa uma obrigação futura, um custo potencial. O código precisa ser mantido, testado, corrigido e atualizado. Erros de código podem levar a falhas que custam tempo, dinheiro e até a reputação da empresa. Além disso, cada nova linha de código torna o software como um todo mais complexo, potencialmente introduzindo novos bugs e tornando o software mais difícil de entender e modificar.