Introdução à Refatoração

Neste artigo é apresentado o conceito e os benefícios da Refatoração através de um exemplo prático, refatorando um pedaço de código de uma Locadora de Filmes.

Fique por dentro
Este artigo é importante para ajudar a entender o que é, como utilizar a Refatoração e o porquê do retrabalho utilizado para aplicar esta técnica não ser um desperdício de tempo, como muitos a avaliam.

Quando a estrutura interna do software começa a perder um pouco da sua integridade (ficar desorganizada) devido a alguns pequenos deslizes dos desenvolvedores, como códigos duplicados e métodos localizados em classes indevidas, a adoção da refatoração será uma excelente opção para solucionar este problema.

A Refatoração é o processo de alterar um software de uma maneira que não mude o seu comportamento externo e ainda melhore a sua estrutura interna. Ela é utilizada para manter um software bem projetado mesmo com o decorrer do tempo e as mudanças que ele virá a sofrer. Pensando nisso, para facilitar a compreensão deste conceito, veremos neste artigo como empregar técnicas de refatoração em uma pequena aplicação de exemplo.

Durante o processo de desenvolvimento de software, mais especificamente na fase de codificação, é comum que uma equipe de desenvolvedores fique encarregada do trabalho de escrever os códigos, o que significa que várias pessoas irão escrever e compartilhar códigos entre si até que o software tenha sido finalizado.

Quanto maior essa equipe, menor será o contato entre os desenvolvedores e maior a chance de que ocorram pequenos deslizes, como código repetido, classes localizadas em pacotes errados, métodos localizados em classes erradas, entre outros erros que podem tornar a estrutura interna do software um pouco bagunçada, e até confusa.

Em um determinado ponto da fase de desenvolvimento, o entendimento do código poderá estar tão confuso e/ou complexo que começará a atrapalhar o desenvolvimento de novas funcionalidades. Neste momento, pode ser que o desenvolvedor leve mais tempo corrigindo um erro causado devido a um código mal escrito do que adicionando uma nova funcionalidade.

Uma boa prática para evitar que isso aconteça é realizar modificações no código, isto é, refatorações, sempre que encontrar algo que fuja dos padrões (da orientação a objetos, do framework utilizado, dos padrões da empresa, entre outros), para que o mesmo continue simples e organizado.

Neste artigo iremos explicar o conceito de refatoração e demonstrar o seu uso na prática através de uma série de refatorações aplicadas em um código que foi (intencionalmente) mal escrito para demonstrar o quanto a refatoração pode melhorar o entendimento do código e o projeto do software. Para realizar tais refatorações utilizaremos o Eclipse IDE e as facilidades que ele nos fornece para automatizar a refatoração.

O que é Refatoração?

Refatoração é o processo de alterar o código fonte de uma maneira que não altere seu comportamento externo e ainda melhore a sua estrutura interna. É uma técnica disciplinada de limpar e organizar o código, e por consequência minimizar a chance de introduzir novos bugs. – Martin Fowler

Durante o desenvolvimento e manutenção do software, é comum que algumas pessoas precisem alterar e/ou adicionar novas funcionalidades em códigos que foram escritos por outras pessoas. Isso pode fazer com que o sistema acabe perdendo a integridade pouco a pouco, pois as pessoas geralmente não programam da mesma maneira, e mesmo que o software tenha sido muito bem projetado e documentado, alguns desenvolvedores podem cometer alguns deslizes, como repetir código e colocar métodos em classes inapropriadas. Isso faz com que o código fonte acabe se distanciando um pouco do que foi planejado.

Pensando em evitar que isto ocorra, é interessante refatorar o código para melhorar a sua conformidade com padrões e a legibilidade, o que facilita o seu entendimento sem alterar o seu comportamento externo. Isto é, quando um código refatorado é executado, ele continua tendo o mesmo resultado final. Como já informamos, a refatoração visa apenas à melhoria interna do código fonte.

Por que refatorar?

Refatorar o software é uma técnica que pode, e deve, ser utilizada por diversas razões. Vejamos algumas delas:

Refatorando

Entender o código de outra pessoa não é uma tarefa trivial. Ainda mais quando é um software muito grande e complexo. Quando uma pessoa modifica um código que não é de sua autoria é possível que ela cometa alguns deslizes (métodos e variáveis mal nomeadas, duplicação de código, etc.) e com isso o código vai se tornando mais difícil de compreender.

Deste modo, após algum tempo, a inserção de novas funcionalidades se tornará uma tarefa mais complicada do que deveria, pois o desenvolvedor não conseguirá, ou terá muitas dificuldades em, entender exatamente o que código está fazendo.

Para evitar que o código fique complexo com o passar do tempo devido às atividades de manutenção e adição de novas funcionalidades, devemos refatorá-lo conforme as atualizações forem implementadas.

Apesar de sua importância, a refatoração não é algo comum nos dias de hoje, e muitos a consideram um retrabalho desnecessário (“se está funcionando por que mudar?”). Uma forma de avaliar que este pensamento não está correto é acompanhando o processo de refatoração que demonstraremos a seguir, e observar as melhorias que o código irá sofrer.

Refatoração na prática

A pequena aplicação que iremos refatorar é responsável por controlar os aluguéis de uma vídeo-locadora, mais precisamente o código responsável por exibir o registro de aluguéis de um cliente.

Tal aplicação possui algumas regras de negócio que já estão implementadas:

A Figura 1 demonstra como será o nosso sistema através de um pequeno diagrama de classes. Este diagrama indica a existência de três classes: Filme, Aluguel e Cliente, descritas nas Listagens 1, 2 e 3, respectivamente.

Nota: Este exemplo foi extraído e adaptado do livro Refactoring: Improving the Design of Existing Code, escrito por Martin Fowler.
Figura 1. Diagrama de classes da locadora
Listagem 1. Classe que representa um Filme


public class Filme {
        
  public static final int NORMAL = 0;
  public static final int INFANTIL = 1;
  public static final int LANCAMENTO = 2;
        
  private String título;
  private int preco;
        
  public Filme(String título, int preco) {
    this.titulo = titulo;
    this.preco = preco;
  }
        
  public int getPreco() {
    return preco;
  }
        
  public void setPreco(int preco) {
    this.preco = preco;
  }
        
  public String getTitulo() {
    return titulo;
  }
}

Listagem 2. Classe que representa um Aluguel

public class Aluguel {
  private Filme filme;
  private int diasAluguel;
        
  public Aluguel(Filme filme, int diasAluguel) {
    this.filme = filme;
    this.diasAluguel = diasAluguel;
  }
   
  public Filme getFilme() {
    return filme;
  }
   
  public int getDiasAluguel() {
    return diasAluguel;
  }      
}

Listagem 3. Classe que representa um Cliente

public class Cliente {
   
    private String nome;
    private List<Aluguel> alugueis;
   
    public Cliente(String nome) {
      this.nome = nome;
      this.alugueis = new ArrayList<Aluguel>();
    }
   
    public void addAluguel(Aluguel aluguel) {
      this.alugueis.add(aluguel);
    }
   
    public String getNome() {
      return nome;
    }
   
public String exibirRegistroAlugueis() {
  double valorTotal = 0;
  int pontos = 0;
  StringBuilder dados = new StringBuilder();
  dados.append("Registro de Aluguéis do cliente: " + getNome() + "\n");
  for (Aluguel aluguel : this.alugueis) {
    double valor = 0;
   
    // Calcula o valor do aluguel
    switch (aluguel.getFilme().getPreco()) {
    case Filme.NORMAL:
    valor += 1.5;
    if (aluguel.getDiasAluguel() > 3)
      valor += (aluguel.getDiasAluguel() - 3) * 1.5;
        break;
      case Filme.INFANTIL:
      valor += 2;
    if (aluguel.getDiasAluguel() > 2)
      valor += (aluguel.getDiasAluguel() - 2) * 1.5;
        break;
      case Filme.LANCAMENTO:
      valor += aluguel.getDiasAluguel() * 3;
        break;
    }
   
    // Adiciona um ponto
    pontos++;
   
    // Bônus para mais de dois dias com um lançamento
    if (aluguel.getFilme().getPreco() == Filme.LANCAMENTO
      && aluguel.getDiasAluguel() > 1)
        pontos++;
   
    // Adiciona os dados desse aluguel
      dados.append("\t" + aluguel.getFilme().getTitulo() + "\t");
      dados.append(" = R$ " + String.valueOf(valor) + "\n");
                    
    valorTotal += valor;
      }
             
    // Rodapé
      dados.append("Total gasto com aluguéis: R$ " + String.valueOf(valorTotal) + "\n");
      dados.append("Pontos ganhos: " + String.valueOf(pontos));
             
      return dados.toString();
    }
}

Ao analisar o código destas três classes, o que mais chama a atenção é o método exibirRegistroAlugueis() da classe Cliente, devido ao seu tamanho. Geralmente métodos muito longos podem conter algumas falhas, como possuir códigos repetidos e/ou encapsular códigos que deveriam estar em outros métodos. Mas para ter certeza de que este método está ou não bem projetado, devemos entender o que ele está fazendo:

  1. É declarado e inicializado um StringBuilder que conterá o texto com os dados dos aluguéis;
  2. Para cada aluguel:
    • É calculado o valor;
    • É somado mais um ponto;
    • Caso haja um bônus devido ao tipo do filme e ao tempo de aluguel, o cliente recebe um ponto extra;
    • As informações do aluguel do filme são adicionadas ao StringBuilder;
    • É realizada uma soma do valor total do aluguel com os outros aluguéis para exibir um total no final da listagem.
  3. Por fim, é adicionado um rodapé no StringBuilder contendo os totalizadores dos aluguéis (total de pontos e valor total).

Agora que sabemos exatamente o que este método está fazendo é possível perceber que ele não foi bem projetado. Ele está fazendo muito mais coisas do que realmente deveria – exibir um registro dos aluguéis.

O cálculo do valor do aluguel não é de responsabilidade desse método, então, isso deveria ser implementado em outro lugar (um novo método), assim como os pontos que aquele aluguel rendeu para o cliente também deveriam ser codificados em outro lugar (mais um novo método).

Como se pode notar, estes cálculos deveriam estar encapsulados em métodos diferentes, pois não é somente quando queremos exibir um registro dos aluguéis que precisamos realizar tais cálculos.

Imagine que o gerente da locadora solicitou duas novas funcionalidades para o sistema: exibir o registro dos aluguéis em HTML e a adição de novas faixas de preços. Do jeito que o código está, é praticamente impossível reutilizá-lo para implementar a funcionalidade de mostrar o registro dos aluguéis em HTML. A única maneira de fazer isso seria escrever um novo método que duplique todo o código e alterá-lo onde for necessário para que a saída seja impressa em HTML. Isso é algo fácil e rápido de se fazer, mas quebra um dos maiores princípios da orientação a objetos: o reuso. Seguindo esta linha, com a adição de uma nova faixa de preço (ex: Filmes Clássicos) os dois métodos deveriam ser alterados para tratar essa nova situação.

Pensando nisso, o ideal é refatorar o método exibirRegistroAlugueis() para depois adicionar a nova funcionalidade que imprime a saída em HTML.

Como já vimos, o cálculo do valor do aluguel pode facilmente se transformar em um método. Essa refatoração é conhecida como Extrair Método (Extract Method), que consiste em copiar um pedaço de código para um novo método e substituí-lo pela chamada a esse novo método.

Para facilitar nosso trabalho, o Eclipse já implementa esse padrão de refatoração. Deste modo, basta selecionar o código que queremos extrair para um novo método e utilizar o atalho Alt + Shift + M ou clicar com o botão direito sobre o código selecionado e navegar até Refatoração > Extrair Método. Feito isso, será aberta uma janela onde podemos especificar como queremos que o método seja gerado. Aqui é preciso informar o nome do novo método, qual será o modificador de acesso (private, public, ...) e ajustar os parâmetros que o Eclipse sugeriu, como demonstrado na Figura 2.

Figura 2. Extraindo um método utilizando o assistente do Eclipse

Ao confirmar a operação o Eclipse já cria o novo método logo abaixo do método exibirRegistroAlugueis() e já o invoca onde é necessário. A Listagem 4 mostra o código após a confirmação da extração do método.

Listagem 4. Código gerado pela refatoração Extração de Método do Eclipse

public class Cliente {
      // ...
      public String exibirRegistroAlugueis() {
            double valorTotal = 0;
            int pontos = 0;
            StringBuilder dados = new StringBuilder();
            dados.append("Registro de Aluguéis do cliente: " + getNome() + "\n");
            for (Aluguel aluguel : this.alugueis) {
  
                    // Calcula o valor do aluguel
                    double valor = calculaValorAlguel(aluguel, valor);
  
                   // Adiciona um ponto
                   pontos++;
  
                   // Bônus para mais de dois dias com um lançamento
                   if (aluguel.getFilme().getPreco() == Filme.LANCAMENTO
                                && aluguel.getDiasAluguel() > 1)
                          pontos++;
  
                   // Adiciona os dados desse aluguel
                   dados.append("\t" + aluguel.getFilme().getTitulo() + "\t");
                   dados.append(" = R$ " + String.valueOf(valor) + "\n");
                    
                   valorTotal += valor;
            }
             
            // Rodapé
            dados.append("Total gasto com aluguéis: R$ " + String.valueOf(valorTotal) + "\n");
            dados.append("Pontos ganhos: " + String.valueOf(pontos));
             
            return dados.toString();
      }
  
      private double calculaValorAlguel(Aluguel alugue) {
            double valor = 0;
            switch (aluguel.getFilme().getPreco()) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (aluguel.getDiasAluguel() > 3)
                          valor += (aluguel.getDiasAluguel() - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (aluguel.getDiasAluguel() > 2)
                          valor += (aluguel.getDiasAluguel() - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += aluguel.getDiasAluguel() * 3;
                   break;
            }
            return valor;
      }
}

Analisando o método calculaValorAluguel() conseguimos identificar que ele utiliza informações dos objetos Aluguel (diasAluguel) e Filme (preco) e não utiliza nenhuma informação do Cliente. Sendo assim, não há necessidade desse método estar localizado na classe Cliente.

Para solucionar este problema, aplicaremos a refatoração Mover Método (Move Method) para mover esse cálculo para a classe Aluguel. Para utilizar o assistente do Eclipse basta selecionar todo o método, clicar com o botão direito e navegar até Refatoração > Mover... ou utilizar o atalho Alt + Shift + V. O assistente já sugere que o método seja movido para a classe Aluguel e nosso único trabalho é dar um novo nome para o método. A partir de agora ele será chamado getValor(). As Listagens 5 e 6 mostram como ficaram as classes Aluguel e Cliente após o método ser movido.

Listagem 5. Classe Aluguel com o novo método getValor()

public class Aluguel {
      // ...
      public double getValor() {
            double valor = 0;
            switch (getFilme().getPreco()) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (getDiasAluguel() > 3)
                          valor += (getDiasAluguel() - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (getDiasAluguel() > 2)
                          valor += (getDiasAluguel() - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += getDiasAluguel() * 3;
                   break;
            }
            return valor;
      }
}
Listagem 6. Classe Cliente após o método calculaValorAluguel() ser movido

public class Cliente {
      // ...
      public String exibirRegistroAlugueis() {
            double valorTotal = 0;
            int pontos = 0;
            StringBuilder dados = new StringBuilder();
            dados.append("Registro de Aluguéis do cliente: " + getNome() + "\n");
            for (Aluguel aluguel : this.alugueis) {
  
                   // Calcula o valor do aluguel
                   double valor += aluguel.getValor();
  
                   // Adiciona um ponto
                   pontos++;
  
                   // Bônus para mais de dois dias com um lançamento
                   if (aluguel.getFilme().getPreco() == Filme.LANCAMENTO
                                && aluguel.getDiasAluguel() > 1)
                          pontos++;
  
                   // Adiciona os dados desse aluguel
                   dados.append("\t" + aluguel.getFilme().getTitulo() + "\t");
                   dados.append(" = R$ " + String.valueOf(valor) + "\n");
                    
                   valorTotal += valor;
            }
             
            // Rodapé
            dados.append("Total gasto com aluguéis: R$ " + String.valueOf(valorTotal) + "\n");
            dados.append("Pontos ganhos: " + String.valueOf(pontos));
             
            return dados.toString();
      }
}

Continuando a análise do método exibirRegistroAlugueis() da classe Cliente, podemos aplicar as mesmas refatorações que aplicamos no cálculo do valor do aluguel no cálculo dos pontos: Extrair Método, pois assim poderemos realizar o cálculo apenas invocando este método; e Mover Método, pois para realizar tal cálculo somente as informações do aluguel são utilizadas. Sendo assim, podemos movê-lo para a classe Aluguel.

Deste modo, após estas refatorações, as classes Cliente e Aluguel são exibidas na Listagem 7.

Listagem 7. Classes Cliente e Aluguel após extrair e mover o código referente ao cálculo dos pontos

public class Aluguel {
      // ...
      public double getValor() {
            double valor = 0;
            switch (getFilme().getPreco()) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (getDiasAluguel() > 3)
                          valor += (getDiasAluguel() - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (getDiasAluguel() > 2)
                          valor += (getDiasAluguel() - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += getDiasAluguel() * 3;
                   break;
            }
            return valor;
      }
  
      public int getPontos() {
            int pontos = 1;
       
            // Bônus para mais de dois dias com um lançamento
            if (getFilme().getPreco() == Filme.LANCAMENTO && getDiasAluguel() > 1)
                   pontos++;
            return pontos;
      }
}
  
public class Cliente {
      // ...
      public String exibirRegistroAlugueis() {
            double valorTotal = 0;
            int pontos = 0;
            StringBuilder dados = new StringBuilder();
            dados.append("Registro de Aluguéis do cliente: " + getNome() + "\n");
            for (Aluguel aluguel : this.alugueis) {
                   double valor = 0;
  
                   // Calcula o valor do aluguel
                   valor += aluguel.getValor();
  
                   // Calcula os pontos
                   pontos += aluguel.getPontos();
  
                   // Adiciona os dados desse aluguel
                   dados.append("\t" + aluguel.getFilme().getTitulo() + "\t");
                   dados.append(" = R$ " + String.valueOf(valor) + "\n");
                    
                   valorTotal += valor;
            }
             
            // Rodapé
            dados.append("Total gasto com aluguéis: R$ " + String.valueOf(valorTotal) + "\n");
            dados.append("Pontos ganhos: " + String.valueOf(pontos));
             
            return dados.toString();
      }
}

A Figura 3 mostra como está o diagrama de classes da nossa aplicação após as refatorações.

Figura 3. Diagrama de classes após mover os métodos que calculam o valor do aluguel e dos pontos

Continuando com as nossas refatorações, vamos analisar agora o método getValor() da classe Aluguel. Aqui, observamos que não é muito interessante a instrução switch baseada no preço do filme. O correto seria fazer um switch utilizando informações próprias e não de outros objetos. Para evitar isso podemos mover esse método para a classe Filme, no entanto, para isso funcionar, devemos enviar como parâmetro a quantidade de dias do aluguel.

Sendo assim, chegamos a um impasse: por que passar a quantidade de dias do aluguel para o método na classe Filme ao invés de recuperar o preço do filme na classe Aluguel? Porque foi proposta a adição de novas faixas de preço pelo gerente da locadora. Por isso, quando o desenvolvedor for adicionar o novo preço, deve-se ter em mente que o melhor é fazer o mínimo de mudanças possíveis no resto do sistema. O cálculo do preço estando junto com o filme facilita na hora de adicionar uma nova faixa de preço e sua regra de cálculo, pois apenas uma classe será alterada.

Juntamente com o cálculo do valor do aluguel, vamos mover também o cálculo dos pontos para a classe Filme, pensando em facilitar futuras mudanças, como o que foi falado há pouco.

Só há uma novidade nessa refatoração: mesmo movendo o código dos métodos getValor() e getPontos() para novos métodos na classe Filme, a classe Aluguel ainda deve continuar tendo estes métodos, pois a classe Cliente já faz chamadas a eles. Se movêssemos completamente os métodos (apagando-os da classe Aluguel), a classe Cliente só conseguiria calcular o valor através da instrução aluguel.getFilme().getValor(), o que não seria problema algum, mas devemos ter em mente que em outro lugar do sistema pode existir códigos que chamam os métodos getValor() e getPontos() da classe Aluguel, e removê-los causaria um ou vários erros. Para evitar tais erros é melhor deixar os métodos na classe Aluguel e dentro deles apenas fazer uma chamada aos novos métodos na classe Filme. Utilizando o assistente do Eclipse para mover os métodos, basta selecionar a opção Manter método original, como ilustrado na Figura 4.

Figura 4. Opção do assistente para não apagar o método antigo

A Listagem 8 mostra como fica a classe Aluguel e a Listagem 9 a classe Filme após movermos os métodos de calcular o valor e os pontos para a classe Filme.

Listagem 8. Classe Aluguel após mover os métodos getValor() e getPontos()

public class Aluguel {
      // ...
      public double getValor() {
            return filme.getValor(diasAluguel);
      }
  
      public int getPontos() {
            return filme.getPontos(diasAluguel);
      }
}

Listagem 9. Classe Filme após mover os métodos getValor() e getPontos()

public class Filme {
       
      public static final int NORMAL = 0;
      public static final int INFANTIL = 1;
      public static final int LANCAMENTO = 2;
       
      // ...
  
      public double getValor(int diasAluguel) {
            double valor = 0;
            switch (preco) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (diasAluguel > 3)
                          valor += (diasAluguel - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (diasAluguel > 2)
                          valor += (diasAluguel - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += diasAluguel * 3;
                   break;
            }
            return valor;
      }
  
      public int getPontos(int diasAluguel) {
            int pontos = 1;
       
            // Bônus para mais de dois dias com um lançamento
            if (preco == Filme.LANCAMENTO && diasAluguel > 1)
                   pontos++;
             
            return pontos;
      }      
}

Agora que a classe Filme está encarregada da lógica para calcular o valor e os pontos, fica mais fácil para futuras mudanças serem feitas.

No entanto, ainda podemos melhorar esses métodos, pois cada filme tem um comportamento diferente. O aluguel de um filme do tipo Normal, por exemplo, custa R$ 1,50 até o terceiro dia de aluguel, e após isso o valor aumenta R$ 1,50, passando a custar R$ 3,00. Já o aluguel de um filme do tipo Lançamento custa R$ 3,00 por dia. Uma vez que cada filme possui um comportamento diferente, poderíamos utilizar subclasses para implementá-los, como mostra a Figura 5.

Figura 5. Usando herança na classe Filme

É isso o que sugere o padrão de refatoração Trocar condicional por polimorfismo (Replace Conditional with Polymorphism). Substituir instruções condicionais (if e switch) pelo polimorfismo dos métodos. Dessa forma, cada tipo de filme que tiver um comportamento diferente poderá sobrescrever o método cujo comportamento varia (neste caso o método getValor(int)) e implementar o seu comportamento.

Infelizmente esta solução não é viável para esta aplicação pois um filme pode trocar o seu tipo com o decorrer do tempo, mas um objeto não pode trocar sua classe com o decorrer do tempo. Isto é, um filme que possui o tipo Lançamento pode ter o tipo facilmente alterado para Normal depois de algum tempo, já um objeto FilmeLancamento não pode simplesmente se tornar um objeto FilmeNormal depois de algum tempo.

Contudo, ainda existe uma outra refatoração que depois de empregada irá possibilitar que utilizemos a refatoração Trocar condicional por polimorfismo. Tal refatoração é conhecida como Trocar o tipo com State (Replace Type Code with State).

Esta refatoração implica em adotar o padrão de projeto State para adicionar uma classe responsável pelos diferentes comportamentos do objeto, nesse caso o preço do filme. A Figura 6 ilustra a aplicação do padrão State na classe Filme.

Padrão de Projeto State: O padrão de projeto State permite que um objeto mude o seu comportamento quando seu estado interno muda. Ele encapsula os comportamentos dos possíveis estados em classes diferentes e apenas delega as tarefas para a classe que representa o estado atual.
Figura 6. Aplicando o padrão de projeto State na classe Filme

Criar uma nova classe para os preços dos filmes torna possível o uso da refatoração trocar condicional por polimorfismo, pois delega o cálculo do valor do aluguel para o próprio preço. Com isso, cada preço que tiver um comportamento diferente deve simplesmente sobrescrever o método getValor() e implementar o seu comportamento. A Listagem 10 mostra a classe abstrata Preco que define o método getPreco() e as subclasses que a estendem.

Com os preços criados podemos aplicar a refatoração Trocar o tipo com State na classe Filme para deixar o código de acordo com o diagrama da Figura 6, porém, para aplicar tal refatoração precisaremos mudar o tipo do atributo preco da classe Filme para Preco. Como já existem alguns métodos dentro da classe Filme que acessam o preço diretamente (this.preco), se mudarmos o tipo alguns erros aparecerão. Para evitar tais erros vamos primeiro encapsular o atributo preco nos famosos métodos getters e setters, para somente depois mudar o seu tipo. Desse jeito podemos fazer o setter receber um int e criar um novo Preco de acordo com o valor do parâmetro e o getter retornar um int de acordo com o que foi projetado na Figura 6. Essa refatoração é conhecida como Auto Encapsular Campo (Self Encapsulate Field) e é demonstrada na Listagem 11.

Listagem 10. Classe Preco e suas subclasses

public abstract class Preco {
       
      abstract public int getPreco();
       
}
  
public class PrecoNormal extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.NORMAL;
      }
  
}
  
public class PrecoInfantil extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.INFANTIL;
      }
  
}
  
public class PrecoLancamento extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.LANCAMENTO;
      }
  
}
Listagem 11. Classe Filme após aplicar a refatoração Auto Encapsular Campo no atributo preco

public class Filme {
       
      public static final int NORMAL = 0;
      public static final int INFANTIL = 1;
      public static final int LANCAMENTO = 2;
       
      private String titulo;
      private int preco;
       
      public Filme(String titulo, int preco) {
            this.titulo = titulo;
            setPreco(preco);
      }
       
      public int getPreco() {
            return preco;
      }
       
      public void setPreco(int preco) {
            this.preco = preco;
      }
       
      public String getTitulo() {
            return titulo;
      }
  
      public double getValor(int diasAluguel) {
            double valor = 0;
            switch (getPreco()) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (diasAluguel > 3)
                          valor += (diasAluguel - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (diasAluguel > 2)
                          valor += (diasAluguel - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += diasAluguel * 3;
                   break;
            }
            return valor;
      }
  
      public int getPontos(int diasAluguel) {
            int pontos = 1;
       
            // Bônus para mais de dois dias com um lançamento
            if (getPreco() == Filme.LANCAMENTO && diasAluguel > 1)
                   pontos++;
             
            return pontos;
      }      
}

Agora podemos, de fato, aplicar a refatoração Trocar o tipo com State. Para isso, basta trocar o tipo do atributo preco da classe Filme para Preco, alterar o setter para que crie um novo objeto Preco a partir do int que vem por parâmetro, e modificar o getter para retornar o método getPreco() do atributo preco. Tal refatoração é demonstrada na Listagem 12.

Listagem 12. Classe Filme após a refatoração Trocar Tipo com State

public class Filme {
  
       // ... 
       private Preco preco;
  
      public int getPreco() {
            return preco.getPreco();
      }
  
      public void setPreco(int preco) {
            switch (preco) {
            case Filme.NORMAL:
                   this.preco = new PrecoNormal();
                   break;
            case Filme.INFANTIL:
                   this.preco = new PrecoInfantil();
                   break;
            case Filme.LANCAMENTO:
                   this.preco = new PrecoLancamento();
                   break;
            default:
                   throw new IllegalArgumentException("Código de Preço inválido");
            }
      }
  
      // ...
  
}

Agora que a classe Filme já teve o atributo preco refatorado, podemos mover o método getValor() para a classe Preco (Listagem 13) e depois aplicar a refatoração Trocar condicional por polimorfismo. Isto é importante porque cada preço tem um comportamento diferente em relação ao valor, sendo assim, cada classe implementa tal comportamento no seu próprio método getValor(). A Listagem 14 demostra o resultado desta refatoração.

Listagem 13. Classe Preco e classe Filme após mover o método getValor()

public abstract class Preco {
       
      abstract public int getPreco();
  
      public double getValor(int diasAluguel) {
            double valor = 0;
            switch (getPreco()) {
            case Filme.NORMAL:
                   valor += 1.5;
                   if (diasAluguel > 3)
                          valor += (diasAluguel - 3) * 1.5;
                   break;
            case Filme.INFANTIL:
                   valor += 2;
                   if (diasAluguel > 2)
                          valor += (diasAluguel - 2) * 1.5;
                   break;
            case Filme.LANCAMENTO:
                   valor += diasAluguel * 3;
                   break;
            }
            return valor;
      }      
}
  
public class Filme {
  
      // ...
  
      public double getValor(int diasAluguel) {
            return preco.getValor(diasAluguel);
      }
  
      // ...
}
Listagem 14. Subclasses de Preco implementando seus próprios comportamentos

public class PrecoNormal extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.NORMAL;
      }
  
      @Override
      public double getValor(int diasAluguel) {
            double valor = 1.5;
            if (diasAluguel > 3)
                   valor += (diasAluguel - 3) * 1.5;
            return valor;
      }      
}
  
public class PrecoInfantil extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.INFANTIL;
      }
       
      @Override
      public double getValor(int diasAluguel) {
            double valor = 2;
            if (diasAluguel > 2)
                   valor += (diasAluguel - 2) * 1.5;
            return valor;
      }
}
  
public class PrecoLancamento extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.LANCAMENTO;
      }
       
      @Override
      public double getValor(int diasAluguel) {
            return diasAluguel * 3;
      }
}

Como todas as subclasses de Preco vão implementar o método getValor(), podemos torná-lo abstrato, de acordo com a Listagem 15.

Listagem 15. Classe Preco com o método getValor() abstrato

public abstract class Preco {
      abstract public int getPreco();
      abstract double getValor(int diasAluguel);
}

Podemos aplicar as mesmas refatorações (mover método e trocar condicional por polimorfismo) para o método getPontos(), mas como não são todas as faixas de preço que têm um comportamento diferente em relação aos pontos, deixaremos o comportamento padrão na classe Preco, como a Listagem 16 demonstra.

Listagem 16. Classes que sofreram alteração após “Mover Método” e “Trocar a Condicional por Polimorfismo” no método getPontos()

public class Filme {
       
      // ...
       
      public int getPontos(int diasAluguel) {
            return preco.getPontos(diasAluguel);
      }
  
      // ... 
}
  
public abstract class Preco {
       
      abstract public int getPreco();
  
      abstract public double getValor(int diasAluguel);
  
      public int getPontos(int diasAluguel) {
            return 1;
      }
}
  
public class PrecoLancamento extends Preco {
  
      @Override
      public int getPreco() {
            return Filme.LANCAMENTO;
      }
       
      @Override
      public double getValor(int diasAluguel) {
            return diasAluguel * 3;
      }
       
      @Override
      public int getPontos(int diasAluguel) {
            if (diasAluguel > 1)
                   return 2;
            else
                   return super.getPontos(diasAluguel);
      }
}

A Figura 7 ilustra o diagrama da aplicação após a última refatoração.

Figura 7. Diagrama de classes após as refatorações

Conclusão

Mesmo este sendo um simples exemplo, conseguimos demonstrar várias refatorações conhecidas e melhorar bastante o código da pequena aplicação da locadora de vídeos.

Apesar de todas as mudanças que fizemos no código terem sido retrabalho (não adicionamos nenhuma funcionalidade nova), ele não foi desnecessário, pois ao final, a estrutura interna do aplicativo ficou muito mais orientada a objetos e fácil de entender.

Ao comparar a Figura 1 com a Figura 7 podemos notar que após as refatorações as classes da nossa aplicação passaram a utilizar: herança (Preco e subclasses), polimorfismo (Filme possui um atributo do tipo Preco e não de uma de suas subclasses), e sobrescrita de métodos (getValor() e getPontos()). Se observarmos as classes mais a fundo, encontraremos o encapsulamento do atributo preco da classe Filme e uma melhor localização para os métodos, o que comprova que este retrabalho não foi desnecessário.

Após essas mudanças, quando o desenvolvedor ler as novas funcionalidades que o gerente da locadora pediu para serem implementadas, ele vai saber o que fazer rapidamente.

Para adicionar uma nova faixa de preços, por exemplo, agora basta criar uma subclasse de Preco, implementar o método getValor() e, o getPontos() (caso a nova faixa de preços possua um comportamento diferente do padrão). Para finalizar, na classe Filme, adicionar mais uma constante referente ao preço e alterar o método setPreco(int) para tratar a nova classe.

Para imprimir o registro dos aluguéis em HTML o desenvolvedor pode copiar o método exibirRegistroAlugueis() da classe Cliente e criar um novo método exibirRegistroAlugueisHTML(), mudando apenas as linhas necessárias para exibir a saída em HTML. Algumas pessoas dirão que é inaceitável duplicar código, mas nesse caso não tem problema, pois caso alguma regra de negócio seja alterada, os métodos não vão precisar sofrer nenhuma alteração.

Existem muitas outras refatorações além das demonstradas neste artigo, portanto o seu estudo é altamente recomendável. Quanto mais o desenvolvedor conhecer e dominar as refatorações, mais fácil e menos demorado será a aplicação das mesmas, assim como melhor será o resultado para a qualidade do sistema.


Saiu na DevMedia!

  • SQL nível Jedi: Subqueries:
    A utilização de JOINS para consultar em mais de uma tabela é uma prática comum entre os desenvolvedores, porém em certas ocasiões não é o suficiente para trazer os resultados desejados. Aqui você saberá quando e como utilizar subqueries. Confira!
  • Séries:
    Séries sobre programação que abordam os principais temas da área: Tecnologias, Padrões de Arquitetura, Levantamento de Requisitos e muitos outros. Sempre com cursos e projetos completos que te ajudam a evoluir como programador.

Saiba mais sobre Arquitetura de Software ;)

  • Guias de Arquitetura de Software:
    DescrAprenda os Conceitos e a Importância da Arquitetura de Software. Nestas guias você aprenderá como estruturar seu projeto desde sua definição até a sua aplicabilidade.ipton
  • MVC e Regras de negócio:
    Em uma arquitetura MVC, temos três camadas com diferentes responsabilidades. Em qual destas camadas deveria estar a regra de negócio da aplicação? Saiba isso e muito mais nesta série.
  • Entre de cabeça no REST:
    Aprenda como criar e consumir APIs.Por que eu criaria/usaria uma API? Desvende essa e outras dúvidas nessa série completa sobre consumo de APIs RESTful. Visite agora!

Artigos relacionados