10 de mar. de 2010

Refactorings: Extrair método e substituir variável temporária por consulta

Salve todos,

Como desenvolvedor, penso que uma das competências necessárias é saber refatorar o código. Isto se aprende com o tempo, e ler ajuda bastante, li o livroRefatoração: Aperfeiçoando o projeto de código existente, do Martin, e ele é muito bom, claro e coeso, demonstrando algumas refatorações consolidadas.


Então, como tenho um tempo livre, resolvi compartilhar algumas partes e expor para discussão aqui na lista.

Primeiro, quando refatorar?

Pode ser ...

- Quando corrige um erro;
- Quando se está adicionando uma nova funcionalidade;
- Quando está se revisando o código;

Ou, como diz Martin, na edição de seu código, na terceira vez que tiver que mexer em algo, refatore.

Segundo, é necessário refatorar?

Depende, Kent Beck diz que deve-se refatorar quando detectar mau cheiro no código. Mau cheiro, não quer dizer que algo não funcione, mas sim aquele código em que tu olhas atravessado e pensa "Dá para melhorar isso".

Abaixo, uma lista resumida de alguns maus cheiros clássicos:

- Nomes obscuros de variáveis;
- Código duplicado;
- Método muito longo;
- Classe muito longa;
- Muitos parâmetros no método;
- Construtor com nulos;
- Muitas variáveis de instância;
- Campos temporários;
- Comentários no código (a.k.a. "desodorante")

Um dos princípios da refatoração é deixar o código legível e por consequência mais manutenível. Uma refatoração que aplico com frequência, é o extrair método, que uso para substituir comentários ou reusar parte de um método para outros métodos, e alterar o nome de variáveis.

Inventei uma classe 'mock' (hehehe), por exemplo:

class PagamentoService {

   private ContabilService cs;

   public void encaminhaPagamentosCartao(Integer id) {

      if (id == null || id <= 0) throw new IllegalArgumentException("ID invalido");

      Venda e = em.find(Venda.class, id);

      if (venda == null) throw new BusinessException("Venda nao encontrada para o ID:" + id);

      //Lista de pagamentos      
      List pc = new ArrayList();

      for (Item i : e.itens) {
         boolean quitado = true;
         for (Pagamento p : i.pagamentos) {
            if (!p.faturado) quitado = false;
         }
         if (quitado) {
            for (Pagamento p : i.pagamentos) {
               if (p.isCartao) {
                  pc.add(p);
               }
            }
         }
      }

      for (Pagamento p : pc) {
         cs.send(p);
      }

   }

}

Para ficar mais legível, poderíamos deixá-la assim:


class PagamentoService {

   private ContabilService contabilService;

   public void encaminhaPagamentosCartao(Integer idVenda) {

      validaParametro(idVenda);
      
      Venda venda = localizaVenda(idVenda);

      enviaPagamentoParaContabilidade(venda);      

   }

   private void validaParametro(Integer idVenda) {
      if (idVenda == null || idVenda <= 0) throw new IllegalArgumentException("ID da Venda é invalido");
   }

   private Venda localizaVenda(Integet idVenda) {
      Venda e = em.find(Venda.class, id);

      

      if (venda == null) throw new BusinessException("Venda nao encontrada para o ID:" + id);
   }

   private void enviaPagamentoParaContabilidade(Venda venda) {

      for (Item item : venda.itens) {
         if (estaQuitado(item)) {
            for (Pagamento pagamento : item.pagamentos) {
               if (pagamento.isCartao) {
                  enviaParaContabilidade(pagamento);
               }
            }
         }
      }
   }

   private boolean estaQuitado(Item item) {      

      for (Pagamento pagamento : item.pagamentos) {
         if (!pagamento.faturado) return false;
      }
      return true;
   }

   private void enviaPagamento(Pagamento pagamento) {
      contabilService.send(pagamento);
   }

}

Geralmente a refatoração adiciona indireção, obviamente, mas vai ao encontro do paradigma orientado a objetos, que prega a utilização de pequenas partes reutilizáveis de código, sempre pensando no futuro. Neste exemplo, os comentários foram substituídos por métodos  (extrair método) que falam por si, as variáveis foram renomeadas e uma delas trocada por uma consulta (substituir variável temporária por consulta).

Como benefício, futuros métodos públicos podem utilizar os métodos privados para operações semelhantes. 

O difícil é saber até onde refatorar, dividir em pequenas peças, mas isto vem com a experiência. Após acostumar-se a refatorar, passa a não se ter mais medo de alterar o código, mas deve ser feito com cuidado para não introduzir bug's, e aí entra a importância dos testes. Geralmente a refatoração é associada com alguma prática de TDD.

Em outro tópico lanço outro estudo de caso e refatorações.

Para os analistas e DBA's de plantão, sugiro este ótimo livro: http://www.agiledata.org/essays/databaseRefactoring.html
Li alguns pequenos trechos, já que estou mais ativo no código, e acho que vale a pena.

Refactorings: Introduzir objeto parâmetro.

Salve

Direto ao ponto, imagine uma classe método que utilizem muitos parâmetros, por exemplo:

public class AluguelService {


    public List findAluguel(Date dataInicialContrato, Date dataFinalContrato,
                            Date dataInicialOcupacao, Date dataFinalOcupacao) {
        // pesquisa e regras de negócio aqui
    }

    public List findAluguelByStatus(Boolean status, 
                                    Date dataInicialContrato, Date dataFinalContrato,
                                    Date dataInicialOcupacao, Date dataFinalOcupacao) {
        // pesquisa e regras de negócio aqui
    }

    public List findAluguelByCliente(Integer idCliente, Date dataInicialContrato, 
                                     Date dataFinalContrato,
                                     Date dataInicialOcupacao, Date dataFinalOcupacao) {
        // pesquisa e regras de negócio aqui
    }

}


Esta classe é elegível a refatoração substituir parâmetros por objeto parâmetro: http://www.refactoring.com/catalog/introduceParameterObject.html

Neste caso, há sempre uma intervalo de tempo do contrato e ocupacao. Em primeiro lugar, poderia-se tratar os intervalos:


public class Intervalo {
    private Date dataInicial;
    private Date dataFinal;

    //Get's and Set's
}


E deixar a classe assim:


public class AluguelService {


    public List findAluguel(Intervalo contrato,
                            Intervalo ocupacao) {
        // pesquisa e regras de negócio aqui
    }

    public List findAluguelByStatus(Boolean status, Intervalo contrato,
                                    Intervalo ocupacao) {
        // pesquisa e regras de negócio aqui
    }

    public List findAluguelByCliente(Integer idCliente, Intervalo contrato,
                                     Intervalo ocupacao) {
        // pesquisa e regras de negócio aqui
    }

}


Acho que já deu "outra cara". Entretanto, se no domínio, o intervalo do contrato e ocupação andem sempre juntos, pode-se pensar em uni-los em um objeto parâmetro.


public class IntervaloAluguel {
    private Intervalo contrato;
    private Intervalo ocupacao;

    //Get's and Set's
}


Então, a classe ficaria assim:



public class AluguelService {


    public List findAluguel(IntervaloAluguel intervalo) {
        // faz pesquisa aqui
    }

    public List findAluguelByStatus(Boolean status, IntervaloAluguel intervalo) {
        // faz pesquisa aqui
    }

    public List findAluguelByCliente(Integer idCliente, IntervaloAluguel intervalo) {
        // faz pesquisa aqui
    }

}

Em uma prática mais Domain Driven Design poderia aplicar validações dos parâmetros nas classes parâmetro, por exemplo, tratar o IntervaloAluguel para que as datas sejam válidas, que a data de ocupacao esteja dentro do intervalo contrato, e assim por diante, deixando o a classe mais como um Value Object (não confundir com Transfer Object), se quiserem saber mais: http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson