Refactoring: técnicas para encarar código legado
Muitas vezes nos deparamos com montanhas de código legado. Códigos que já passaram por muitas mãos, muitas manutenções feitas às pressas para atender requisitos de última hora, código sem testes, etc.
Essas verdadeiras montanhas de código bagunçado, inflexível, desestruturado etc nos desafiam algumas vezes, fazendo-nos ter vontade de sair correndo.
Pensar na melhor forma de realizar manutenções nessas montanhas e, por que não, melhorar a qualidade do código, não é tarefa fácil. Pensando nessa tarefa, chamada refactoring, temos o famoso livro de Martin Fowler, chamado "Refactoring: Improving the design of existing code".
Neste artigo, gostaria de discutir alguns dos padrões defendidos por Fowler no livro, que acho muito interessantes. A fim de melhor explanar os padrões, teremos exemplos em Java no decorrer do artigo, mesmo linguagem utilizada pelo livro.
Mover método
Vamos começar nosso primeiro padrão com um exemplo simples. Imaginemos uma classe Produto com nome, descrição e valor, além de um método de cálculo de juros:
package br.com.vivareal.refactoring;
public class Produto {
private String nome;
private String descricao;
private double valor;
private double juros;
public double calcularJuros() {
return juros * valor;
}
// getters e setters omitidos
}
Nossa classe suporta muito bem o conceito de juros por produto, atendendo esse requisito plenamente. Vamos agora supor que ocorra uma mudança de requisitos e agora tenhamos juros não por produto, mas apenas por grupo (categoria) de produtos. Nesse cenário, poderíamos mudar o código criando uma classe Categoria:
package br.com.vivareal.refactoring;
public class Categoria {
private String nome;
private String descricao;
private double juros; private List<Produto> produtos; //getters e setters omitidos}
E modificar a classe Produto do seguinte modo:
package br.com.vivareal.refactoring;
public class Produto {
private String nome;
private String descricao;
private double valor;
private Categoria categoria;
public double calcularJuros() {
return categoria.getJuros() * valor;
}
//getters e setters omitidos
}
Tudo certo, não? Bom, não sei quanto a vocês, mas para mim, ainda sinto que algo está errado. Quando mudamos o nosso negócio para que agora os juros sejam aplicáveis por categoria e não por produto, não parece mais natural que mantenhamos o método calcularJuros() na classe Produto.
É aí que entra a nossa primeira técnica de refactoring: mover o método. Para utilizar a técnica neste caso, basta movermos o método da classe Produto para a classe Categoria, passando agora por parâmetro o nome do produto que desejamos calcular os juros:
package br.com.vivareal.refactoring;
import java.util.List;
public class Categoria {
private String nome;
private String descricao;
private double juros;
private List<Produto> produtos;
public double calcularJuros(String nomeProduto) {
double juros = 0;
for (Produto produto : produtos) {
if (produto.getNome().equals(nomeProduto)) {
juros = this.juros * produto.getValor();
}
}
return juros;
}//getters e setters omitidos}
E por fim, removemos o método e o atributo da classe Categoria, não mais necessários na classe Produto:
package br.com.vivareal.refactoring;
public class Produto {
private String nome;
private String descricao;
private double valor;
//getters e setters omitidos
}
Assim, temos uma hierarquia de classes mais natural aos requisitos de negócio.
Extrair classe
Vamos supor agora o seguinte cenário. Suponhamos que temos a seguinte classe:
package br.com.vivareal.refactoring;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class CarrinhoDeCompras {
private List<Produto> produtos = new ArrayList<>();
private Map<String, Produto> estoque;
public CarrinhoDeCompras(Map<String, Produto> estoque) {
this.estoque = estoque;
}
public boolean adicionarProduto(Produto produto) {
boolean adicionou = false;
if (estoque.containsKey(produto.getNome())) {
produtos.add(produto);
estoque.remove(produto.getNome());
adicionou = true;
}
return adicionou;
}
public boolean removerProduto(Produto produto) {
produtos.remove(produto);
estoque.put(produto.getNome(), produto);
return true;
}
}
A classe acima representa um carrinho de compras, que também efetua o controle do estoque. Os métodos adicionarProduto e removerProduto realizam a adição/exclusão de produtos do carrinho, bem como do estoque.
Notou algo estranho com o código? A nossa classe carrinho de compras está realizando mais do que deveria ser responsável. Um carrinho deve apenas atuar como uma cesta de produtos, não controlar o estoque dos mesmos de maneira direta. Nós resolvemos esse problema através da técnica de extração de classe.
Essa técnica consiste de remover tudo que não for de responsabilidade da classe de origem e agrupar o código extraído em uma ou mais novas classes. No nosso exemplo, criamos uma nova classe Estoque, que ficaria da seguinte forma:
package br.com.vivareal.refactoring;
import java.util.Map;
public class Estoque {
private Map<String, Produto> estoque;
public Estoque(Map<String, Produto> estoque) {
this.estoque = estoque;
}
public boolean constaNoEstoque(Produto produto) {
return estoque.containsKey(produto.getNome());
}
public void atualizaEstoque(Produto produto) {
if (constaNoEstoque(produto)) {
estoque.remove(produto.getNome());
} else {
estoque.put(produto.getNome(), produto);
}
}
}
E finalmente teríamos a nossa nova classe CarrinhoDeCompras, mais organizada e concisa, da seguinte forma:
package br.com.vivareal.refactoring;
import java.util.ArrayList;
import java.util.List;
public class CarrinhoDeCompras {
private List<Produto> produtos = new ArrayList<>();
private Estoque estoque;
public CarrinhoDeCompras(Estoque estoque) {
this.estoque = estoque;
}
public boolean adicionarProduto(Produto produto) {
boolean adicionou = false;
if (estoque.constaNoEstoque(produto)) {
produtos.add(produto);
estoque.atualizaEstoque(produto);
adicionou = true;
}
return adicionou;
}
public boolean removerProduto(Produto produto) {
produtos.remove(produto);
estoque.atualizaEstoque(produto);
return true;
}
}
Consolidar expressão condicional
Vamos agora imaginar um método que fizesse algum tipo de lógica de validação em cima dos dados da nossa classe Produto, conforme podemos ver abaixo:
public boolean meuMetodo(Produto produto) {
boolean retorno = false;
if (produto.getValor() > 10) {
if (produto.getNome().contains("abc")) {
retorno = true;
} else if (produto.getDescricao().contains("def")) {
retorno = true;
}
} else if (produto.getValor() < 30) {
if (produto.getNome().contains("abc")) {
retorno = true;
} else if (produto.getDescricao().contains("def")) {
retorno = true;
}
}
return retorno;
}
O método acima, além de complexo, demonstra uma repetição de código: temos expressões condicionais repetidas, que precisaram ser alteradas em conjunto sempre que uma manutenção na lógica precisar ser efetuada. Para resolver esse problema, aplicamos a técnica de consolidação de expressão condicional.
A forma mais recomendada de aplicar essa técnica é extraindo a lógica duplicada para um método, desse modo centralizando a lógica em um único ponto. Desse modo, o nosso método pode ser evoluído para a forma abaixo, mais legível e robusta:
public boolean meuMetodo(Produto produto) {
boolean retorno = false;
if (produto.getValor() > 10) {
retorno = possuiNomeEDescricaoValidos(produto);
} else if (produto.getValor() < 30) {
retorno = possuiNomeEDescricaoValidos(produto);
}
return retorno;
}
private boolean possuiNomeEDescricaoValidos(Produto produto) {
boolean retorno = false;
if (produto.getNome().contains("abc")) {
retorno = true;
} else if (produto.getDescricao().contains("def")) {
retorno = true;
}
return retorno;
}
Trocar condicional por polimorfismo
Vamos agora imaginar uma classe Funcionario, com os seguintes getters e setters. Atentar para o método que retorna o salário do funcionário:
package br.com.vivareal.refactoring;
public class Funcionario {
private String nome;
private String cargo;
private double salario;
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getCargo() {
return cargo;
}
public void setCargo(String cargo) {
this.cargo = cargo;
}
public double getSalario() {
double valor = 0;
switch (cargo) {
case "Analista":
valor = salario;
break;
case "Gerente":
valor = salario * 2;
break;
case "Diretor":
valor = salario * 4;
break;
}
return valor;
}
public void setSalario(double salario) {
this.salario = salario;
}
}
Como podemos ver, temos nesse cenário um condicional baseado no tipo de funcionário. O ruim dessa estratégia é que temos um código mais complexo de dar manutenção, principalmente se tivermos que incluir novas regras de cálculo de salário com base em novos cargos.
A solução para este cenário é a técnica de trocar condicional por polimorfismo. Seguindo essa técnica, transformarmos cada perna do switch em uma classe, elegendo a classe Funcionario como uma superclasse abstrata.
Assim, a classe Funcionario passa a ficar da seguinte forma:
package br.com.vivareal.refactoring;
public abstract class Funcionario {
private String nome;
private String cargo;
protected double salario;
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getCargo() {
return cargo;
}
public void setCargo(String cargo) {
this.cargo = cargo;
}
public abstract double getSalario();
public void setSalario(double salario) {
this.salario = salario;
}
}
E por fim temos as classes Analista, Gerente e Diretor, onde implementamos cada perna da nossa lógica condicional substituída pelo polimorfismo.
Classe Analista:
package br.com.vivareal.refactoring;
public class Analista extends Funcionario {
@Override
public double getSalario() {
return salario;
}
}
Classe Gerente:
package br.com.vivareal.refactoring;
public class Gerente extends Funcionario {
@Override
public double getSalario() {
return salario * 2;
}
}
Classe Diretor:
package br.com.vivareal.refactoring;
public class Diretor extends Funcionario {
@Override
public double getSalario() {
return salario * 4;
}
}
Extrair subclasse
Vamos analisar agora a seguinte classe:
package br.com.vivareal.refactoring;
public class Cinema {
private long pipocasDisponiveis;
private long lugaresDisponiveis;
public Cinema(long lugaresDisponiveis, long pipocasDisponiveis) {
this.lugaresDisponiveis = lugaresDisponiveis;
this.pipocasDisponiveis = pipocasDisponiveis;
}
public boolean pegarPipoca() {
boolean pegouPipoca = false;
if (pipocasDisponiveis > 0) {
pegouPipoca = true;
pipocasDisponiveis--;
}
return pegouPipoca;
}
public boolean pegarLugar() {
boolean pegouLugar = false;
if (lugaresDisponiveis > 0) {
pegouLugar = true;
lugaresDisponiveis--;
}
return pegouLugar;
}
public boolean pegarVagaCarro() {
boolean pegouLugar = false;
if (lugaresDisponiveis > 0) {
pegouLugar = true;
lugaresDisponiveis = lugaresDisponiveis - 2;
}
return pegouLugar;
}
}
A classe Cinema representa um cinema, com pipocas e lugares disponíveis. Reparou algo estranho? O que significa aquele método "pegarVagaCarro"?
Aquele método é para Drive-ins. Em um Drive-in, as pessoas assistem filmes de seus carros, portanto os lugares se traduzem como vagas de carros. Como uma carro ocupa mais espaço do que uma pessoa, neste caso cada lugar ocupado por um carro ocupa mais espaço do que por uma pessoa.
Neste exemplo é óbvio, mas vale o mesmo para ressaltar problemas que nem sempre enxergamos: por que razão não criamos uma subclasse de cinema, especializando o método pegarLugar, ao invés de criar novos métodos que essencialmente fazem o mesmo?
Esse é o conceito da técnica extrair subclasse. Assim, utilizando a técnica, modificamos a nossa classe Cinema e introduzimos a nova classe Drive-in, do seguinte modo:
Classe Cinema:
package br.com.vivareal.refactoring;
public class Cinema {
protected long pipocasDisponiveis;
protected long lugaresDisponiveis;
public Cinema(long lugaresDisponiveis, long pipocasDisponiveis) {
this.lugaresDisponiveis = lugaresDisponiveis;
this.pipocasDisponiveis = pipocasDisponiveis;
}
public boolean pegarPipoca() {
boolean pegouPipoca = false;
if (pipocasDisponiveis > 0) {
pegouPipoca = true;
pipocasDisponiveis--;
}
return pegouPipoca;
}
public boolean pegarLugar() {
boolean pegouLugar = false;
if (lugaresDisponiveis > 0) {
pegouLugar = true;
lugaresDisponiveis--;
}
return pegouLugar;
}
}
Classe Drivein:
package br.com.vivareal.refactoring;
public class Drivein extends Cinema {
public Drivein(long lugaresDisponiveis, long pipocasDisponiveis) {
super(lugaresDisponiveis, pipocasDisponiveis);
}
@Override
public boolean pegarLugar() {
boolean pegouLugar = false;
if (lugaresDisponiveis > 0) {
pegouLugar = true;
lugaresDisponiveis = lugaresDisponiveis - 2;
}
return pegouLugar;
}
}
Extrair Superclasse
Por fim, temos a técnica de extrair superclasse. Vamos imaginar as seguintes classes, Pistola e Metralhadora:
Pistola:
package br.com.vivareal.refactoring;
public class Pistola {
private long balas;
public Pistola(long balas) {
this.balas = balas;
}
public void disparar() {
if (balas > 0) {
System.out.println("BLAM!");
balas--;
}
}
public void recarregar(long balas) {
this.balas = this.balas + balas;
}
}
Metralhadora:
package br.com.vivareal.refactoring;
public class Metralhadora {
private long balas;
public Metralhadora(long balas) {
this.balas = balas;
}
public void disparar(long tempoDisparo) {
if (balas > 0) {
while (tempoDisparo > 0 && balas > 0) {
System.out.println("RA-TA-TA-TA!");
balas--;
tempoDisparo--;
}
}
}
public void recarregar(long balas) {
this.balas = this.balas + balas;
}
}
No código acima, temos classes semelhantes com código duplicado — o método recarregar — que poderia ser centralizado em um único ponto. Nós resolvemos essa questão com a técnica de extrair superclasse, onde criamos uma superclasse e movemos o código duplicado para lá.
Assim temos uma nova classe, Arma e modificamos as nossas classes Pistola e Metralhadora da seguinte forma:
Arma:
package br.com.vivareal.refactoring;
public class Arma {
protected long balas;
public void recarregar(long balas) {
this.balas = this.balas + balas;
}
}
Pistola:
package br.com.vivareal.refactoring;
public class Pistola extends Arma {
public Pistola(long balas) {
this.balas = balas;
}
public void disparar() {
if (balas > 0) {
System.out.println("BLAM!");
balas--;
}
}
}
Metralhadora:
package br.com.vivareal.refactoring;
public class Metralhadora extends Arma {
public Metralhadora(long balas) {
this.balas = balas;
}
public void disparar(long tempoDisparo) {
if (balas > 0) {
while (tempoDisparo > 0 && balas > 0) {
System.out.println("RA-TA-TA-TA!");
balas--;
tempoDisparo--;
}
}
}
}
É claro que temos outras técnicas além das listadas acima, mas com as que temos acima já conseguimos fazer bastantes melhorias no nosso código :D.
Sempre teste entre os refactorings!
Um ponto importante a se frisar em todas essas técnicas, é a importância dos testes. Ao se ter uma boa suíte de testes amparando o nosso código, podemos testar o código a cada passo de nosso refactoring, desse modo garantindo que o nosso código não será quebrado por nossas mudanças.
Assim, antes de se realizar qualquer refactoring, um bom primeiro passo a se fazer é, se o nosso código não possui uma boa cobertura de testes, vamos implementar!
Conclusão
E assim concluímos o nosso papo sobre refactoring. Espero ter conseguido passar ao leitor bons conceitos e técnicas que vão lhes auxiliar nas manutenções de código legado.
Até a próxima!