27 de jan. de 2014

Movendo o cálculo da locação

Continuando nosso estudo sobre refactoring, podemo observar que o nosso novo método TotalLocacaoItem ficou fora do contexto. Nota-se que o método tem informações da locação, mas não tem informação sobre o cliente.

Isso nos lava a deduzir que o método está referenciado ao objeto errado. Na maioria dos casos um objeto precisa estar referenciado ao objeto que detém os dados que o método utiliza, portanto no nosso exemplo o método deveria estar no objeto Locacao.

type

  TCliente = class
  private
    FNome: string;
    FLocacoes: TLocacoes;
    procedure SetNome(const Value: string);
    function TotalLocacaoItem(Locacao: TLocacao): Double;
  public
    constructor Create(pNome: string);
    procedure AdicionaLocacao(Valor: TLocacao);
    function Relatorio: string;
    property Nome: string read FNome write SetNome;
    property Locacoes: TLocacoes read FLocacoes;
  end;


Para isso utilizamos o método Mover. Basta copiar o método para a classe Locacao, e faz os ajustes necessários. Depois é só compilar.




Para o exemplo que estamos trabalhando, após mover o método para classe lotação, é necessário retirar o parâmetro Locacao. E também, renomear  o método.

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


Agora podemos testar e ver como esse método é utilizado. Para fazer isso é só substituir o conteúdo do método TotalLocacaoItem, com a chamada do método criado na classe Lotacao.

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 := Locacao.TotalItem();
    // 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;


Agora podemos compilar, testar e certificar que está funcionando exatamente como antes.


19 de jan. de 2014

Renomeando variáveis

Mesmo com o facilitador do Delphi de extrair o método, eu mantive o primeiro modelo do método TotalLocacaoItem, onde o total de locação do item é resultado do método e sem utilizar os parâmetros do tipo var.

Agora analisando o método que foi extraído, percebemos há variáveis que podem ter seus nomes melhorados.

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

Após renomear a variável, compilamos e rodamos o teste. Ok, sem erros, não estragamos nada.

Vale apena o esforço para renomear variáveis. Sim, claro. Absolutamente. O bom código deve conseguir comunicar o que ele está fazendo de forma clara e precisa, e os nomes das variáveis são as chaves para garantir um código claro. Nunca devemos ter receio em modificar o nome das variáveis com objetivo de melhorar a qualidade do código. Utilize boas ferramentas de busca e substituição para facilitar o trabalho.

Dica: Qualquer tolo pode escrever um código que o computador entenda. Bons desenvolvedores escrevem códigos que humanos conseguem compreender.

Códigos que transmitem o seu propósito são muito importantes.


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...

18 de jan. de 2014

Lista de Testes - Completa

Conforme prometido segue a lista de testes completa. Bom, pelo menos acho que está completa.
Procurei atender todas as situações de condicionais do método Relatorio. Todos os esteste estão executando sem erros. Agora, posso iniciar o refactoring, e se algum teste começar a falhar durante o refactoring, é porque algo está errado, e estou inserindo um bug no programa.

unit uTesteLocacao;

interface

uses TestFramework, uCliente, uFilme, uAluguel;

type
  TTestLocacao = class(TTestCase)
  published
    procedure LocacaoFilmeInfantil1Dia;
    procedure LocacaoFilmeInfantil4Dias;
    procedure LocacaoFilmeLancamento1Dia;
    procedure LocacaoFilmeLancamento2Dias;
    procedure LocacaoFilmeRegular1Dia;
    procedure LocacaoFilmeRegular3Dias;
    procedure Locacao3FilmesDistintos5Dias;
  end;

implementation

{ TTestLocacao }

procedure TTestLocacao.Locacao3FilmesDistintos5Dias;
var
  FilmeInfantil: TFilme;
  FilmeRegular: TFilme;
  FilmeLancamento: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    FilmeInfantil := TFilme.Create('Tarzan', TFilme.Infantil);
    FilmeLancamento := TFilme.Create('O Hobbit', TFilme.Lancamento);
    FilmeRegular := TFilme.Create('Máquina Mortífera I', TFilme.Regular);
    Cliente := TCliente.Create('Alice');
    Locacao := TLocacao.Create(FilmeInfantil, 5);
    Cliente.AdicionaLocacao(Locacao);
    Locacao := TLocacao.Create(FilmeRegular, 5);
    Cliente.AdicionaLocacao(Locacao);
    Locacao := TLocacao.Create(FilmeLancamento, 1);
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|Tarzan|4,5' + #13 + '|Máquina Mortífera I|6,5' + #13 + '|O Hobbit|3' + #13 + 'O total é: 14' + #13 + 'Você ganhou: 3 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    FilmeInfantil.Free;
    FilmeRegular.Free;
    FilmeLancamento.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeInfantil1Dia;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('Tarzan', TFilme.Infantil);
    Locacao := TLocacao.Create(Filme, 1);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|Tarzan|1,5' + #13 + 'O total é: 1,5' + #13 + 'Você ganhou: 1 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeInfantil4Dias;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('Tarzan', TFilme.Infantil);
    Locacao := TLocacao.Create(Filme, 4);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|Tarzan|3' + #13 + 'O total é: 3' + #13 + 'Você ganhou: 1 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeLancamento1Dia;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('O Hobbit', TFilme.Lancamento);
    Locacao := TLocacao.Create(Filme, 1);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|O Hobbit|3' + #13 + 'O total é: 3' + #13 + 'Você ganhou: 1 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeLancamento2Dias;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('O Hobbit', TFilme.Lancamento);
    Locacao := TLocacao.Create(Filme, 2);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|O Hobbit|6' + #13 + 'O total é: 6' + #13 + 'Você ganhou: 2 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeRegular1Dia;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('Máquina Mortífera I', TFilme.Regular);
    Locacao := TLocacao.Create(Filme, 1);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|Máquina Mortífera I|2' + #13 + 'O total é: 2' + #13 + 'Você ganhou: 1 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

procedure TTestLocacao.LocacaoFilmeRegular3Dias;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  try
    Filme := TFilme.Create('Máquina Mortífera I', TFilme.Regular);
    Locacao := TLocacao.Create(Filme, 3);
    Cliente := TCliente.Create('Alice');
    Cliente.AdicionaLocacao(Locacao);
    Resultado := Cliente.Relatorio();
    Esperado := 'Registro de locação de Alice' + #13 + '|Máquina Mortífera I|3,5' + #13 + 'O total é: 3,5' + #13 + 'Você ganhou: 1 pontos';
    Check(Resultado = Esperado, 'Falha no teste: Esperado: ' + Esperado + ' Realizado:' + Resultado);
  finally
    Filme.Free;
    Cliente.Free;
    Locacao.Free;
  end;
end;

initialization

TestFramework.RegisterTest(TTestLocacao.Suite);

end.

17 de jan. de 2014

Primeiro passo do refactoring - Testes


Sempre que for fazer o refactoring, o primeiro passo sempre é o mesmo. É necessário construir um conjunto de testes sólidos para  a seção de código que será trabalhada. Os teste são fundamentais, pois mesmo que sigamos o refactoring de forma estruturada, ainda há o risco de inserir bugs no programa, afinal somos humanos e podemo cometer equívocos. Por isso precisamos de testes sólidos.

Vamos a implementação de testes automatizados. O refactoring é uma ferramenta do TDD, e é por isso que estamos tratando desse assunto primeiro. Quando formos discutir TDD, vocês perceberão que já sabem tudo de TDD por que aprenderam TDD estudando refactoring.

Em qualquer aplicação os testes são simples, rápidos de implementar e executar. É o conjunto deles que garantirá a efetividade da automatização de testes. Lembre-se para testar algo é preciso saber o que será testado, é preciso possuir um resultado ou valor esperado. O teste apenas vai executar algo, e consultar se o resultado esperado ocorreu ou não. O segredo do teste está a mensagem de retorno do resultado. Se o teste ocorreu conforme esperado, a mensagem pode ser um simples Ok. Mas se o teste deu errado, o próprio teste deve retornar dizendo o que deu errado. Qual o valor esperado que não foi encontrado, qual valor foi retornado no lugar do valor esperado. São essas informações que irão direcionar a solução para o problema detectado no teste. Por isso dizemos que o testes devem ser auto verificáveis. Se não fizermos dessa forma, perderemos tempo verificando números, consultando registros para saber onde o teste falhou.

O refactoring se apoia totalmente nos testes. Nós vamos nos apoiar nos teste para nos dizer quando introduzimos um bug no programa. Por isso é essencial ter bons testes, testes abrangentes. Vale a pena gastar um tempo na implementação dos testes, pois eles lhe darão a segurança necessária para realizar as mudanças no programa.

Vamos aos testes logo né!
Para testar, sugiro a criação de testes que simule a locação de cada tipo de filme e com quantidade de dias de locação variados. Como o retorno do método Relatorio é uma string,o teste se concentrará na validação da string gerada pelo método em comparação com a string que esperamos que seja gerada. Podemos executar e simular todas as situações de testes e capturar as string geradas, em seguida basta adiciona-las ao teste. Vale ressaltar que o nosso exemplo nesse momento está correto e sem bugs, gerado os dados da forma que o usuário deseja, portanto faz todo o sentido utilizar esses dados na validação dos testes.

Vou montar os testes, mas vou deixar uma amostra de alguns necessários para que possam basear.
Continuo no próximo post.
Até mais.

Segue abaixo alguns testes (Esses são somente alguns testes, serão necessários mais testes, para ficar sólido e seguro):



unit uTesteLocacao;

interface

uses TestFramework, uCliente, uFilme, uAluguel;

type
  TTestLocacao = class(TTestCase)
  published
    procedure LocacaoFilmeInfantil1Dia;
    procedure LocacaoFilmeInfantil4Dias;
    procedure LocacaoFilmeLancamento1Dia;
    procedure LocacaoFilmeLancamento2Dias;
  end;

implementation

{ TTestLocacao }

procedure TTestLocacao.LocacaoFilmeInfantil1Dia;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  Filme := TFilme.Create('Tarzan', TFilme.Infantil);
  Locacao := TLocacao.Create(Filme,1);
  Cliente := TCliente.Create('Alice');
  Cliente.AdicionaLocacao(Locacao);
  Resultado := Cliente.Relatorio();
  Esperado := 'Registro de locação de Alice'+#13+
              '|Tarzan|1,5' + #13 +
              'O total é: 1,5'+#13+
              'Você ganhou: 1 pontos';
  Check(Resultado = Esperado, 'Falha no teste: Esperado: '+ Esperado + ' Realizado:' + Resultado);
end;


procedure TTestLocacao.LocacaoFilmeInfantil4Dias;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  Filme := TFilme.Create('Tarzan', TFilme.Infantil);
  Locacao := TLocacao.Create(Filme,4);
  Cliente := TCliente.Create('Alice');
  Cliente.AdicionaLocacao(Locacao);
  Resultado := Cliente.Relatorio();
  Esperado := 'Registro de locação de Alice'+#13+
              '|Tarzan|3' + #13 +
              'O total é: 3'+#13+
              'Você ganhou: 1 pontos';
  Check(Resultado = Esperado, 'Falha no teste: Esperado: '+ Esperado + ' Realizado:' + Resultado);
end;


procedure TTestLocacao.LocacaoFilmeLancamento1Dia;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  Filme := TFilme.Create('O Hobbit', TFilme.Lancamento);
  Locacao := TLocacao.Create(Filme,1);
  Cliente := TCliente.Create('Alice');
  Cliente.AdicionaLocacao(Locacao);
  Resultado := Cliente.Relatorio();
  Esperado := 'Registro de locação de Alice'+#13+
              '|O Hobbit|3' + #13 +
              'O total é: 3'+#13+
              'Você ganhou: 1 pontos';
  Check(Resultado = Esperado, 'Falha no teste: Esperado: '+ Esperado + ' Realizado:' + Resultado);
end;

procedure TTestLocacao.LocacaoFilmeLancamento2Dias;
var
  Filme: TFilme;
  Cliente: TCliente;
  Locacao: TLocacao;
  Resultado: String;
  Esperado: String;
begin
  Filme := TFilme.Create('O Hobbit', TFilme.Lancamento);
  Locacao := TLocacao.Create(Filme,2);
  Cliente := TCliente.Create('Alice');
  Cliente.AdicionaLocacao(Locacao);
  Resultado := Cliente.Relatorio();
  Esperado := 'Registro de locação de Alice'+#13+
              '|O Hobbit|6' + #13 +
              'O total é: 6'+#13+
              'Você ganhou: 2 pontos';
  Check(Resultado = Esperado, 'Falha no teste: Esperado: '+ Esperado + ' Realizado:' + Resultado);
end;

initialization
  TestFramework.RegisterTest(TTestLocacao.Suite);

end.




16 de jan. de 2014

Introdução ao Refactoring - Criando um cenário

Como mencionei anteriormente, nossa intenção e demonstrar o refactoring através de exemplos. Vamos procurar demonstrar como o refactoring funciona e demonstrar como ocorre o processo.

Vamos pegar um exemplo pequeno, pois se utilizarmos um exemplo muito extenso, será necessário posts e posts para concluir. Realmente não faz sentido aplicar refactoring em programas muito pequenos, mas vamos interpretar que o exemplo a ser demonstrado é apenas uma pequena parte de um grande programa.

O programa é muito simples. É um programa de videolocadora que calcula e imprime um relatório de um aluguel de video realizado por um cliente. O programa diz qual filme o cliente alugou e por quanto tempo. Ele então calcula o preço, que dependerá o tempo de aluguel e o tipo de filme.
Há três tipos de filmes: regulares, infantil e lançamentos. Além de calcular o preço, o relatório exibe os pontos do cliente, que varia com o tipo do filme alugado.

Algumas classes representam vários elementos da videolocadora, segue algumas:



Em seguida o código de cada classe.

Filme
Filme é um simples classe de dados

unit uFilme;

interface

type
  Filme = class
  private
    FTitulo: string;
    FCodigoPreco: integer;
    procedure SetTitulo(const Value: string);
    procedure SetCodigoPreco(const Value: integer);

  public
    const Infantil = 2;
    const Regular = 0;
    const Lancamento = 1;
    constructor Create(titulo: string; precoCodigo: integer);
    property Titulo:string read FTitulo write SetTitulo;
    property CodigoPreco: integer read FCodigoPreco write SetCodigoPreco;

  end;


implementation

{ Filme }

constructor Filme.Create(titulo: string; precoCodigo: integer);
begin

end;

procedure Filme.SetCodigoPreco(const Value: integer);
begin
  FCodigoPreco := Value;
end;

procedure Filme.SetTitulo(const Value: string);
begin
  FTitulo := Value;
end;

end.

Aluguel
Aluguel representa o aluguel de filmes pelo cliente

unit uAluguel;

interface

uses uFilme;

type
  TAluguel = class
  private
    FFilme: TFilme;
    FDiasAluguel: integer;
    procedure SetFilme(const Value: TFilme);
    procedure SetDiasAluguel(const Value: integer);
  public
    constructor Create(pFilme: TFilme; pDiasAluguel: Integer);
    property Filme: TFilme read FFilme write SetFilme;
    property DiasAluguel: integer read FDiasAluguel write SetDiasAluguel;
  end;

implementation

{ TAluguel }

constructor TAluguel.Create(pFilme: TFilme; pDiasAluguel: Integer);
begin
  Filme := pFilme;
  DiasAluguel := pDiasAluguel;
end;

procedure TAluguel.SetDiasAluguel(const Value: integer);
begin
  FDiasAluguel := Value;
end;

procedure TAluguel.SetFilme(const Value: TFilme);
begin
  FFilme := Value;
end;

end.


Cliente
A classe cliente representa o cliente da loja. Assim como as outras classes ela possui dados e métodos assessores.

unit uCliente;

interface

uses uAluguel, System.Classes;

type

TLocacoes = TList;


type

  TCliente = class
  private
    FNome: string;
    FLocacoes: TLocacoes;
    procedure SetNome(const Value: string);

  public
    constructor Create(pNome:string);
    procedure AdicionaLocacao(Valor: TLocacao);
    property Nome:string read FNome write SetNome;
    property Locacoes: TLocacoes read FLocacoes;
  end;

implementation

{ TCliente }

procedure TCliente.AdicionaLocacao(Valor: TLocacao);
begin
  FLocacoes.Add(TLocacao.Create(Valor.Filme,Valor.DiasAluguel));
end;

constructor TCliente.Create(pNome: string);
begin
  Nome := pNome;
end;


procedure TCliente.SetNome(const Value: string);
begin
  FNome := Value;
end;

end.


A classe cliente possui um método que produz o relatório. A figura abaixo mostra as interações desse método. O código do método vem logo depois do diagrama.





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 +  '\t' + Locacao.Filme.Titulo + '\t' +
    FloatToStr(TotalLocacao) + '\n';
    Total := Total + TotalLocacao;
  end;
  //Adiciona Rodapé
  Resultado := Resultado +  'O total é: ' + FloatToStr(Total) + '\n';
  Resultado := Resultado +  'Você ganhou: ' + FloatToStr(PontosCliente) + ' pontos';
  Result := Resultado;
end;


Comentários sobre o código do programa

Qual sua opinião sobre o código programa? Você pode dizer que ele não está bem implementado, e que certamente não está orientado a objetos. Para um simples programa como esse, isso realmente não importa. Não existe nada de errado com um programa feito rapidamente e bagunçado. Mas se esse código representa um fragmento de um sistema mais complexo, aí sim, temos sérios problema com esse programa.
A rotina relatório na classe cliente está muito extensa e faz muitas coisas. Muitas dessas coisas deveriam estar sob responsabilidade de outras classes.

Mesmo que o programa funcione bem. Isso não é apenas um julgamento de quem não gosta de códigos feios e mal feitos. Para o compilador não importa onde o código está mal feito e onde está bem feito. Mas se precisar realizar uma alteração no sistema, haverá um humano envolvido, e para humanos isso importa.
Um código mal organizado e complicado para se alterar. É complicado por que ele precisa saber onde as mudanças são necessárias. E se é complicado identificar onde precisa ser alterado, existe então uma chance muito grande do desenvolvedor cometar um equívoco e acabar gerando um bug no sistema.

Vamos supor que nesse caso o usuário deseje algumas mudanças no sistema. A primeira mudança que ele deseja é que o relatório seja emitido no formato html. Considere o impacto dessa mudança.
Como você pode observar no código é impossível reaproveitar qualquer comportamento do atual método relatorio para um relatório hmtl. A unica opção que você possui é copiar o método atual, e criar um novo método. Nada trabalhoso, no entanto, o comportamento ficará duplicado.

E se mudar as regras de cálculo? Você terá que ajustar o método Relatorio e o método RelatorioHTML, para que a mudança atenda os dois relatórios.
O problema do copiar colar, vem quando você precisa fazer alguma mudança. Se você está escrevendo um programa pequeno, e que não sofrerá mudanças, o copiar colar atende muito bem. Mas se o programa tem um tempo de vida longo, o copiar colar pode se tornar uma grave ameaça.

Isso leva a uma segunda solicitação de mudança. Os usuários quem mudar a forma de classificar os filmes, mas eles não se decidiram ainda de como será essa nova classificação. Eles tem várias solicitações de mudanças em mente. Essas mudanças afetam diretamente os preços das locações de filmes e o cálculo de pontuação de clientes. E você na sua experiência de desenvolvedor, sabe que os usuários vão continuar solicitando mudanças.

Você pode tentar fazer poucas modificações, que sejam possíveis, no código e depois de tudo funcionar conforme esperado. Relembrando a máxima da velha engenharia: "Se ainda não deu problema, não conserte." O programa pode realmente não dar problema, mas isso dói. Isso faz a sua vida mas difícil, pois você encontra dificuldades em fazer as mudanças que o usuário solicita. É nessa hora que entra o refactoring!

Dica: Quando você tem que adicionar um novo recurso ao programa, e o código do programa não está estruturado de forma conveniente para adicionar o novo recurso, primeiro refatore o programa de modo que a adição do recurso fique mais fácil, e só então adicione o novo recurso.