19 de jan. de 2014

Refactoring 1 - Decompondo e Redistribuindo

É claro que o centro das atenções é o imenso método relatório. Ao se deparar com métodos muito extensos, o melhor a se fazer é decompor o método em pequenas partes. Pequenas partes de código tende a deixas as coisas mais gerenciáveis, são mais fáceis de trabalha-los e movimenta-los.

A primeira fase do refactoring é dividir o método e mover as partes para classes que se adaptam melhor. O objetivo principal é facilitar a criação do método RealtorioHTML com a menor duplicação de código possível.

O primeiro passo é encontrar um amontoado lógico de código e utilizar a Extração de Método. Uma parte de código óbvia são as sequências de if-else. Essa para ser uma boa parte de código para ser isolada e ter seu próprio método.

Quando extraímos um método, assim como em qualquer outro refactoring, precisamos saber o que pode dar errado. Se fizermos uma má extração, poderemos inserir um bug ao programa. Então, antes de fazer o refactoring, precisamos pensar em como fazer isso com segurança. Para isso precisamos seguir os itens de segurança.

Primeiro precisamos localizar e identificar no fragmento de código todas as variáveis locais e parâmetros. No trecho de código que vamos extrair identificamos as seguintes variáveis: Locacao e TotalLocacao. A variável Locacao não é modificada pelo código, mas Contador e TotalLocacao são modificadas. Para qualquer variável que não é modificada pelo código eu posso defini-la no novo método como um parâmetro. Variáveis que sofrem modificação precisam de um cuidado maior. Se houver somente uma variável que sofre modificação, podemos retorna-la como resultado do método. A variável Contador, apesar de sofre modificação durante o código, ela não faz parte da regra de negócio, ela é apenas um artifício para navegar entre os itens do objeto Locacao, portanto não há necessidade de retorna-la fora do método. Sendo assim, posso definir a variável TotalLocacao como retorno do método.

A seguir mostramos o código antes e depois do refactoring. Destacaremos em negrito as modificações.

Antes
Depois
function TCliente.Relatorio: string;
var
  Total, TotalLocacao: Double;
  PontosCliente: Integer;
  Resultado: String;
  Contador: Integer;
  Locacao: TLocacao;
begin
  Total := 0;
  PontosCliente := 0;
  Resultado := 'Registro de locação de ' + Nome + #13;
  // Determina total de cada locação
  for Contador := 0 to Locacoes.Count - 1 do
  begin
    Locacao := Locacoes.Items[Contador];
    TotalLocacao := 0;
    if Locacao.Filme.CodigoPreco = TFilme.Regular then
    begin
      TotalLocacao := TotalLocacao + 2;
      if (Locacao.DiasAluguel > 2) then
        TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 2) * 1.5);
    end
    else if Locacao.Filme.CodigoPreco = TFilme.Lancamento then
    begin
      TotalLocacao := TotalLocacao + (Locacao.DiasAluguel * 3);
    end
    else if Locacao.Filme.CodigoPreco = TFilme.Infantil then
    begin
      TotalLocacao := TotalLocacao + 1.5;
      if (Locacao.DiasAluguel > 3) then
        TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 3) * 1.5);
    end;
    // Adiciona pontos de clientes
    PontosCliente := PontosCliente + 1;
    // Gera bonus porlocação de lançamento por dois dias
    if (Locacao.Filme.CodigoPreco = TFilme.Lancamento) and (Locacao.DiasAluguel > 1) then
      PontosCliente := PontosCliente + 1;
    //Mostra dados dessa locação
    Resultado := Resultado +  '|' + Locacao.Filme.Titulo + '|' +
    FloatToStr(TotalLocacao) + #13;
    Total := Total + TotalLocacao;
  end;
  //Adiciona Rodapé
  Resultado := Resultado +  'O total é: ' + FloatToStr(Total) + #13;
  Resultado := Resultado +  'Você ganhou: ' + FloatToStr(PontosCliente) + ' pontos';
  Result := Resultado;
end;
function TCliente.Relatorio: string;
var
  Total, TotalLocacao: Double;
  PontosCliente: Integer;
  Resultado: String;
  Contador: Integer;
  Locacao: TLocacao;
begin
  Total := 0;
  TotalLocacao := 0;
  PontosCliente := 0;
  Resultado := 'Registro de locação de ' + Nome + #13;
  // Determina total de cada locação
  for Contador := 0 to Locacoes.Count - 1 do
  begin
    Locacao := Locacoes.Items[Contador];
    TotalLocacao := TotalLocacaoItem(Locacao);
    // Adiciona pontos de clientes
    PontosCliente := PontosCliente + 1;
    // Gera bonus porlocação de lançamento por dois dias
    if (Locacao.Filme.CodigoPreco = TFilme.Lancamento) and (Locacao.DiasAluguel > 1) then
      PontosCliente := PontosCliente + 1;
    //Mostra dados dessa locação
    Resultado := Resultado +  '|' + Locacao.Filme.Titulo + '|' +
    FloatToStr(TotalLocacao) + #13;
    Total := Total + TotalLocacao;
  end;
  //Adiciona Rodapé
  Resultado := Resultado +  'O total é: ' + FloatToStr(Total) + #13;
  Resultado := Resultado +  'Você ganhou: ' + FloatToStr(PontosCliente) + ' pontos';
  Result := Resultado;
end;

function TCliente.TotalLocacaoItem(Locacao: TLocacao): Double;
var
  TotalLocacao: Double;
begin
    TotalLocacao := 0;
    if Locacao.Filme.CodigoPreco = TFilme.Regular then
    begin
      TotalLocacao := TotalLocacao + 2;
      if (Locacao.DiasAluguel > 2) then
        TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 2) * 1.5);
    end
    else if Locacao.Filme.CodigoPreco = TFilme.Lancamento then
    begin
      TotalLocacao := TotalLocacao + (Locacao.DiasAluguel * 3);
    end
    else if Locacao.Filme.CodigoPreco = TFilme.Infantil then
    begin
      TotalLocacao := TotalLocacao + 1.5;
      if (Locacao.DiasAluguel > 3) then
        TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 3) * 1.5);
    end;
    Result := TotalLocacao;
end;



Quando fazemos uma mudança desse nível, temos que compilar e executar o teste. Após executar, todos os teste rodaram sem erros.

Digamos que na criação do método TotalLocacaoItem, tivéssemos colocar o retorno do tipo Integer ao invés de Double. Nesse caso, seria fácil pegar o problema no momento da compilação que estaria acusando erro de tipo de retorno. Esse é a essência do processo de refactoring pois o seria fácil e rápido encontrar o motivo do problema.

Então fica a dica: Refatore o programa em pequenos passos. Se você cometer um erro, será fácil encontrar o bug.

Na ferramenta Delphi XE3 temos o menu Refactor. (Não sei a partir de qual versão do Delphi já possui esse recurso). Se utilizarmos o item Extract Method (Shift+Ctrl+M), ele fará esse trabalho de extrair o método, gerando um novo método com o código selecionado. Conforme as imagens abaixo:





Gerando o código abaixo:

function TCliente.Relatorio: string;
var
  Total, TotalLocacao: Double;
  PontosCliente: Integer;
  Resultado: String;
  Contador: Integer;
  Locacao: TLocacao;
begin
  Total := 0;
  PontosCliente := 0;
  Resultado := 'Registro de locação de ' + Nome + #13;
  // Determina total de cada locação
  for Contador := 0 to Locacoes.Count - 1 do
  begin
    Locacao := Locacoes.Items[Contador];
    TotalLocacaoItem(TotalLocacao, Locacao);
    // Adiciona pontos de clientes
    PontosCliente := PontosCliente + 1;
    // Gera bonus porlocação de lançamento por dois dias
    if (Locacao.Filme.CodigoPreco = TFilme.Lancamento) and (Locacao.DiasAluguel > 1) then
      PontosCliente := PontosCliente + 1;
    //Mostra dados dessa locação
    Resultado := Resultado +  '|' + Locacao.Filme.Titulo + '|' +
    FloatToStr(TotalLocacao) + #13;
    Total := Total + TotalLocacao;
  end;
  //Adiciona Rodapé
  Resultado := Resultado +  'O total é: ' + FloatToStr(Total) + #13;
  Resultado := Resultado +  'Você ganhou: ' + FloatToStr(PontosCliente) + ' pontos';
  Result := Resultado;
end;

procedure TCliente.TotalLocacaoItem(var TotalLocacao: Double; Locacao: TLocacao);
begin
  TotalLocacao := 0;
  if Locacao.Filme.CodigoPreco = TFilme.Regular then
  begin
    TotalLocacao := TotalLocacao + 2;
    if (Locacao.DiasAluguel > 2) then
      TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 2) * 1.5);
  end
  else if Locacao.Filme.CodigoPreco = TFilme.Lancamento then
  begin
    TotalLocacao := TotalLocacao + (Locacao.DiasAluguel * 3);
  end
  else if Locacao.Filme.CodigoPreco = TFilme.Infantil then
  begin
    TotalLocacao := TotalLocacao + 1.5;
    if (Locacao.DiasAluguel > 3) then
      TotalLocacao := TotalLocacao + ((Locacao.DiasAluguel - 3) * 1.5);
  end;
end;

Note que nesse caso, o método gerado, TotalLocacaoItem, não possui retorno. A ferramenta seta o parâmetro TotalLocacao como var e  retorna nele mesmo o resultado da função. Esse recurso normalmente é usado quando precisamos que o método retorne mais de um valor, em todo o caso, no nosso exemplo o uso desse recurso não afeta em nada o refactoring, afinal o mais importante foi realizado, ou seja, extraiu o método TotalLocacaoItem do método Relatorio.

E os testes continuam rodando sem erros:


Continua no próximo post...

Nenhum comentário:

Postar um comentário