Lidando com código legado na prática
Dicas de como evitar o sofrimento ao lidar com código sujo e sem testes.
Nem sempre podemos escolher o código que vamos ter que lidar e muitas vezes você vai ter que dar manutenção no código de outras pessoas.
Eu quando ainda estava começando na programação, um mero iniciante que aprendeu Delphi nas coxas, tive que botar as mãos em um "simples" sistema de Ponto De Venda para implementar uma nova funcionalidade. O que poderia dar errado? TUDO DEU ERRADO.
O sistema realmente era "simples", pois a regra de negócio era simples, mas o verdadeiro problema morava no código. Todo o código era basicamente um método que fazia TUDO. Era um método Deus que dentro do sistema era onipotente, quebrando totalmente o principio da responsabilidade única, na qual diz que um método deve fazer apenas uma coisa e fazê-la muito bem. Alterar aquele código era extremamente complicado pois uma simples mudança quebrava toda a regra de negócio. Era absurdo.
Na época eu não conhecia muitas boas práticas, não fazia ideia do que eram testes automatizados e nem se quer tinha ouvido falar de Clean Code, então eu sofri muito para conseguir fazer uma pequena alteração nesse sistema. Mas será que realmente precisa ser assim? Óbvio que não!. Não precisa ser assim.
Respeite o legado
O significado de Código legado pode ter várias interpretações e sentidos, mas normalmente é usado para referenciar aquele código que foi desenvolvido sem pensar nos custos de mante-lo no futuro, ou seja, é um código que você desenvolve sem aplicar boas práticas e testes .
Não aplicar boas práticas e não implementar testes traz como consequência um sistema que vai ser difícil de entender e difícil de manter, que é tudo que nós não queremos, então não devemos cometer esse erro. Mas e quando herdamos esse tipo de código, o que devemos fazer?
Reescrever o código do inicio pode ser uma opção, não é? Afinal, é só reescrever tudo com as boas práticas e com os testes, certo???? Não! Está ERRADO!!
Já imaginou tentar explicar para um cliente que para você instalar uma nova pia no banheiro você terá que reconstruir o banheiro inteiro do ZERO??? É praticamente a mesma situação.
O código, por mais feio e ruim que esteja, provavelmente contém uma parte importante do software que já roda faz muito tempo e funciona. E se o código funciona, então ele agrega valor para alguém. Alguém está tendo algum retorno desse código. Os programadores e o cliente passaram dias implementando a regra de negócio nesse código, independente da qualidade dos métodos utilizados, e agora ele guarda a regra do negócio da aplicação.
Então a primeira coisa que precisamos fazer é aceitar que o código, por mais feio e mau feito que ele seja, guarda uma verdade, e essa verdade é a regra de negócio da aplicação. Você precisa respeitar isso e não sair reescrevendo o código do Zero.
E por respeitar o código e a regra de negócio nós precisamos cobrir esse código com testes!
1... 2... 3... Testando!
Antes de começar a modificar nosso código nós precisamos garantir que não vamos quebrá-lo, ou melhor, nós precisamos garantir que nossas últimas alterações não quebraram a regra de negócio. E para garantir que nada foi alterado nós precisamos cobrir o código legado com testes automatizados.
Mesmo uma quantidade minima de testes já vai nos trazer mais confiança para fazer alterações no código legado, mas o real objetivo dos testes é extrair a verdade que o código guarda. Os testes precisam guardar o comportamento do código, ele precisa guardar a regra de negócio da aplicação, então cobrir o código com testes é extremamente importante.
Um código sem boas práticas também é um código difícil de testar, pois dificilmente pensaram em fazer um código que fosse fácil de testar, mas aqui no Blog nós já falamos anteriormente sobre uma técnica para testar código legado chamada Golden Master Testing onde nós extraímos o mais importante do código, que é o comportamento dele.
Com um código coberto de testes nós ganhamos super poderes. Nós somos capazes de alterar o código e ter um feedback de que não quebramos nada, e essa é uma ótima sensação. Isso nos traz mais confiança. E o maior poder que nós ganhamos é o de poder aplicar Refactoring!!
O Refactoring na prática
O Refactoring foi um livro escrito por Martin Fowler onde ele descreve várias técnicas para melhorar a estrutura do código, mas sem alterar o comportamento, que transforma código feio (Bad Smells) em código limpo e cheiroso (Clean Code). E o melhor de tudo é que são técnicas extremamente simples, com mudanças quase insignificantes, mas que depois de aplicar essas técnicas diversas vezes nós podemos notar que o código melhorou muito.
Vamos ver um exemplo? O código abaixo do retirado de um desafio chamado Gilded Rose Kata:
updateQuality() {
for (var i = 0; i < this.items.length; i++) {
if (this.items[i].name != 'Aged Brie' && this.items[i].name != 'Backstage pass') {
if (this.items[i].quality > 0) {
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
this.items[i].quality = this.items[i].quality - 1;
}
}
} else {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1;
if (this.items[i].name == 'Backstage pass') {
if (this.items[i].sellIn < 11) {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1;
}
}
if (this.items[i].sellIn < 6) {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1;
}
}
}
}
}
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
this.items[i].sellIn = this.items[i].sellIn - 1;
}
if (this.items[i].sellIn < 0) {
if (this.items[i].name != 'Aged Brie') {
if (this.items[i].name != 'Backstage pass') {
if (this.items[i].quality > 0) {
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
this.items[i].quality = this.items[i].quality - 1;
}
}
} else {
this.items[i].quality = this.items[i].quality - this.items[i].quality;
}
} else {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1;
}
}
}
}
return this.items;
}
O código é horrível, não é? Imagina ter que implementar uma nova lógica nesse código... Eu sei como você se sente.
Mas como vamos melhora-lo? Bem, no Refactoring uma das técnicas mais utilizadas é o Extract Method (Extrair Método), e ele faz exatamente o que o nome diz. Essa técnica tem o objetivo de extrair as responsabilidades, ou seja, nós vamos pegar os blocos de código que parecem fazer uma coisa, recorta-los para dentro de um método com um nome bem descritivo e invocar esse método no local onde originalmente havíamos extraído ele.
Por exemplo, o bloco do for
pode ser considerado uma coisa, pois para cada iteração ele executa uma ação, por isso nós podemos extrair um método chamado atualizaUmItem
passando o items[i]
como parâmetro:
updateQuality() {
for (var i = 0; i < this.items.length; i++) {
this.atualizaUmItem(items[i]);
}
return this.items;
}
atualizaUmItem(item) {
if (item.name != 'Aged Brie' && item.name != 'Backstage pass') {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
} else {
if (item.quality < 50) {
item.quality = item.quality + 1;
if (item.name == 'Backstage pass') {
if (item.sellIn < 11) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
if (item.sellIn < 6) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
}
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.sellIn = item.sellIn - 1;
}
if (item.sellIn < 0) {
if (item.name != 'Aged Brie') {
if (item.name != 'Backstage pass') {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
} else {
item.quality = item.quality - item.quality;
}
} else {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
Foi uma alteração extremamente simples. Nós extraímos o conteúdo do for
para um método chamado atualizaUmItem
que descreve a ação do for
, mudamos os valores de dentro do método atualizaUmItem
de this.items[i]
para usar item
que passamos como parâmetro e invocamos esse método dentro do for
.
Sempre rode os testes depois de aplicar uma técnica de Refactoring. Precisamos sempre garantir que nada quebrou depois de aplicar a técnica!!!
A alteração foi extremamente simples e ter melhorado apenas um pouquinho a qualidade do código, então vamos aplicar mais uma técnica para ver se podemos melhorar ainda mais. Para isso, leia com atenção o trecho de código abaixo:
if (item.name != 'Aged Brie' && item.name != 'Backstage pass') {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
} else {
if (item.quality < 50) {
item.quality = item.quality + 1;
if (item.name == 'Backstage pass') {
if (item.sellIn < 11) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
if (item.sellIn < 6) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
}
Se você reparar no código anterior nós temos um bad smell muito sútil, temos uma condicional negativa. Condicionais negativas são ruins, pois temos que pensar um pouco mais sobre o que está acontecendo no código, e quando temos um bloco com um else
, ele representa basicamente a negação da condicional:
if (item.name != 'Aged Brie' && item.name != 'Backstage pass') {
// NÃO É um Aged Brie E NÃO É um Backstage pass
} else {
// É um Aged Brie OU É um Backstage pass
}
Para resolver isso nós precisamos inverter a condicional, mudando de item.name != 'Aged Brie' && item.name != 'Backstage pass'
para item.name == 'Aged Brie' || item.name == 'Backstage pass'
, e como eu disse antes, o else
é justamente a negação da condição, então nós temos que passar o bloco do else
para cima, dessa forma:
if (item.name == 'Aged Brie' || item.name == 'Backstage pass') {
// É um Aged Brie OU É um Backstage pass
} else {
// NÃO É um Aged Brie E NÃO É um Backstage pass
}
Olha só o resultado final:
if (item.name == 'Aged Brie' || item.name == 'Backstage pass') {
if (item.quality < 50) {
item.quality = item.quality + 1;
if (item.name == 'Backstage pass') {
if (item.sellIn < 11) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
if (item.sellIn < 6) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
} else {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
}
Depois de remover a negação da condicional o bloco que o if
executa é exatamente o que está sendo verificado na condicional, diferente da condicional negativa onde nós faziamos a verificação se os itens eram não eram Aged Brie
e Backstage pass
e só iriamos lidar com a lógica desses itens no bloco do else
. É uma alteração extremamente simples, mas elimina uma complexidade desnecessária no if
.
Mais uma vez, não esqueça de rodar os testes depois de aplicar uma técnica de Refactoring!!! SEMPRE RODE OS TESTES!!
Refactoring não é complicado. A ideia é que ele seja simples e para corrigir problemas simples como esse. Quanto mais técnicas aplicarmos, melhor nosso código se torna.
A lógica do Refactoring é bem simples:
- Encontramos o código sujo (bad smell). Pode ser qualquer má prática que você identificar;
- Eliminamos ele usando alguma das técnicas de Refactoring;
- Rodamos os testes!
Vamos ver mais um exemplo para tentar mostrar o poder do Refactoring. Observe o código que ficamos depois do último Refactoring:
if (item.name == 'Aged Brie' || item.name == 'Backstage pass') {
if (item.quality < 50) {
item.quality = item.quality + 1;
if (item.name == 'Backstage pass') {
if (item.sellIn < 11) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
if (item.sellIn < 6) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
} else { /* Implementação do else ... */ }
Aqui nós temos o mesmo problema que tínhamos no for
no começo do artigo. O if
supostamente deveria ter apenas uma responsabilidade, então devemos ser capazes de extrair um método com um nome bem descritivo de dentro dele.
Mas qual a responsabilidade do if
? Talvez o nome atualizaAgedBrieOuBackstagePass
seja descritivo suficiente pois o if
implementa a lógica para os dois itens, não é?
Reparou no que acabei de dizer? "o if
implementa a lógica para os dois itens". Nós acabamos de afirmar que o if
possui mais de uma responsabilidade, e isso é ruim. Apenas uma pequena parte do código é executada de acordo com o tipo do item. Para resolver isso nós precisamos quebrar a nossa condicional. Como nossa condicional possui um ||
, nós podemos quebrar a segunda condição em um else if
, dessa forma:
if (item.name == 'Aged Brie') {
// Implementação SE for Aged Brie
} else if (item.name == 'Backstage pass') {
// Implementação Caso SE for Backstage pass
} else {
// Implementação Senão ...
}
A lógica continua exatamente a mesma, a mesma quando tínhamos o ||
, mas agora podemos separar a responsabilidade de cada um dos blocos em seu próprio método com um nome descritivo implementando apenas sua própria lógica, dessa forma:
if (item.name == 'Aged Brie') {
atualizaItemAgedBrie();
} else if (item.name == 'Backstage pass') {
atualizaItemBackstagePass();
} else {
atualizaItemPadrao();
}
Não se esqueça de rodar os testes! É sério. Rode os testes sempre. Você precisa dos testes!
Esse Refactoring também melhora a legibilidade e organização do código. Agora cada if
ficou com sua responsabilidade chamando métodos com suas próprias responsabilidades. Mas será que melhorou muito? Imagine que tivéssemos mais alguns itens como no código abaixo:
if (item.name == 'Aged Brie') {
atualizaItemAgedBrie();
} else if (item.name == 'Backstage pass') {
atualizaItemBackstagePass();
} else if (item.name == 'Enchanted Sword') {
atualizaItemEnchantedSword();
} else if (item.name == 'Potion') {
atualizaItemPotion);
} else {
atualizaItemPadrao();
}
Fica mais fácil adicionar novos itens se compararmos com o código original. E se você reparar, aqui nós podemos identificar um novo Refactoring!!!
Nós temos várias condicionais, e para cada uma delas nós executamos um comportamento diferente, o que é um sinal claro de que podemos aplicar polimorfismo!!, um dos pilares da Orientação a Objetos. Para você ter idéia, nós já resolvemos esse problema aqui no blog via polimorfismo aplicando a técnica Duck Typing.
Mas é aqui que começamos a chegar em um ponto onde os Refactorings começam a ficar menos triviais. Sempre vão existir situações onde é necessário ter bons conhecimentos de Orientação a Objetos, Design Patterns e Identificação de Bad Smells para continuar aprimorando a estrutura do código.
Os Refactorings continuam sendo alterações pequenas e simples do código, mas em determinado nível você vai precisar ter mais conhecimento e experiência para aplica-los de forma mais eficaz, mas você precisa tomar cuidado, pois podemos aplicar essas técnicas incontáveis vezes.
A importância do pragmatismo
Nós nunca vamos alcançar o código perfeito e podemos sempre melhorar a qualidade do código, então é possível aplicar incontáveis técnicas de Refactoring em um código. Você precisa considerar se o seu prazo é suficiente para tempo a refatorar, a frequência com que o código que vai ser refatorado recebe funcionalidades, se realmente é necessário refatorar o código em questão.
Normalmente adotamos a abordagem do bom escoteiro, que é sempre deixar o caminho por onde passamos um pouquinho mais limpo. Se você tiver que mexer em uma classe e você notar que ela possui algum código sujo (Bad Smells), então limpe esse código sujo para que o próximo que passar por ali tenha menos trabalho, pois o próximo a passar ali pode ser você mesmo.
Então não saia pelo sistema procurando por código que precisa ser refatorado. Refatore apenas o código que você tiver que mexer em algum momento e você não irá se sobrecarregar com refactorings desnecessários.
E não se esqueça de SEMPRE TESTAR!!!. Eu falo muito sério, vocês precisam de testes para utilizar o Refactoring e para ajudar o sistema a envelhecer com um pouquinho mais de qualidade, então implemente testes!
Se você assim como eu entende a importância dos testes, da refatoração contínua e preza pela qualidade do código que produz então não deixe de conhecer nosso curso de TDD e Testes Automatizados com Java.
Conclusão
Esse artigo é um resumo de uma palestra que criei para o último evento da CEJS: Testcase onde tentei passar um pouco do meu aprendizado com código legado.
Nessa palestra eu falo um pouco sobre a importância do Clean Code, da necessidade dos Testes e do Refactoring, com o objetivo de fazer as pessoas conhecerem mais as ferramentas e metodologias criadas para facilitar a vida do desenvolvedor de software.
Os slides da palestra estão disponíveis aqui no meu Slideshade
Refactoring na prática: Lidando com código legado
Como referência para essa palestra eu utilizei os livros:
- Clean Code do Uncle Bob
- Working Effectively With Legacy Code do Micheal Feathers
- Refactoring do Martin Fowler
Eu recomendo muito a leitura desses livros, pois tenho certeza que eles irão melhorar muito suas habilidades como desenvolvedor.
Se você tiver qualquer dica ou sugestão sobre o artigo ou a palestra, deixe um comentário. Toda crítica é muito bem vinda!
You might also be interested in these articles...
Desenvolvedor na TriadWorks - Email
Posted in: clean codecurly lawgolden mastergolden master testone thingrefactoringtestestestes automatizadostesting