segunda-feira, 15 de outubro de 2012

Melhorando o cenário de testes com padrões de projeto Builder e Fluent Interface

Como sabemos, testar garante que nosso código tenha mais qualidade, seja mais conciso e elegante e também conhecemos a importância de testar nossas aplicações. Porém testar gera mais código e devemos dar ao código de teste a mesma atenção que damos ao código de produção, para que os testes sejam fáceis de entender e não se tornem difíceis de serem mantidos. Pois caso isso aconteça a equipe pode ficar desmotivada para realizar os testes.

Imagine que estamos desenvolvendo um sistema para uma loja de venda de computadores. Este sistema deve sugerir computadores que possam agradar determinado cliente.

Um possível modelo para este domínio pode ser visto no diagrama (diagrama informal, ok?!) abaixo:
Nosso computador possui apenas duas portas USB, uma dianteira e outra traseira, cor, possui vários componentes e também informa se é de marca famosa, pois possuímos clientes que compram apenas produtos de primeira linha.

Cada componente possui nome, descrição e cor.

Cor por sua vez possuí um nome e valores correspondente a tabela de cores RGB (red, green, blue).

Vamos ao código do domínio:


public class Computador {
 private USB usbDianteira;

 private USB usbTraseira;

 private Collection< Componente > componentes;

 private boolean marcaFamosa;

 public Computador(USB usbDianteira, USB usbTraseira, Collection< Componente > componentes, boolean marcaFamosa) {
  this.usbDianteira = usbDianteira;
  this.usbTraseira = usbTraseira;
  this.componentes = componentes;
  this.marcaFamosa = marcaFamosa;
 }

 // getters e setters necessários

}


public class USB {
 private String versao;

 public USB(String versao) {
  this.versao = versao;
 }

 // getters e setters necessários
}


public class Componente {
 private String nome;

 private String descricao;

 private Cor cor;

 public USB(String nome, String descricao, Cor cor) {
  this.nome = nome;
  this.descricao = descricao;
  this.cor = cor;
 }

 // getters e setters necessários
}


public class Cor {
 private String nome;

 private int r;

 private int g;

 private int b;

 public USB(String nome, int r, int g, int b) {
  this.nome = nome;
  this.r = r;
  this.g = g;
  this.b = b;
 }

 // getters e setters necessários
}


Agora que temos nosso modelo pronto, iremos testar a classe que verifica se devemos sugerir determinado computador ao cliente ou não. Esta classe recebe o Cliente no construtor e podemos chamar o método deveSugerir, que recebe um computador e retorna um boolean.

public class Aconselhador {
 private Cliente client;
 
 public Aconselhador(Cliente cliente) {
  this.cliente = cliente;
 }

 public boolean deveReceberComoSugestao(Computador computador) {
  // faz uma lógica baseada no cliente no computador a ser sugerido
 }
}
Vamos testar nossa classe, para fins didáticos vamos testar apenas um caso da classe, porém em seu código, você deve testar TODAS as possibilidades. (Veja o post anterior para aprender como visualizar a cobertura de testes em seu código).

@Test
public void deveSugrirComputadorDeMarcaEComMuitosComponentesParaClienteExigente() {
 Cliente cliente = new Cliente("Renan");

 USB usbDianteira = new USB("2.0");
 USB usbTraseira = new USB("1.0");

 Cor corDoMonitor = new Cor("preto", 0, 0, 0);
 Componente monitor = new Componente("nome do monitor", "descricao monitor", corDoMonitor);

 Cor corDoMouse = new Cor("preto", 0, 0, 0);
 Componente mouse = new Componente("descricao do mouse", "nome mouse", corDoMouse);

 Cor corDoTeclado = new Cor("branco", 100, 100, 100);
 Componente teclado = new Componente("nome do teclado", "descricao teclado", corDoMonitor);

 Cor corDaWebCam = new Cor("cinza", 140, 120, 110);
 Componente webCam = new Componente("nome da webcam", "descricao webcam", corDaWebCam);

 List< Componente > componentes = new ArrayList<>();
 componentes.add(monitor);
 componentes.add(teclado);
 componentes.add(webCam);

 Computador computador = new Computador(usbTraseira, usbDianteira, componentes, true);

 assertTrue(new Aconselhador(cliente).deveReceberComoSugestao(computador));
}

Vamos analisar o código e fazer algumas considerações. A primeira coisa a notar é que quanto mais Componentes nosso Computador possuir, mais poluído fica nosso código. Também sem uma explicação prévia, fica difícil saber oque é o cada argumento inteiro passado para o construtor da classe Cor, o mesmo acontece para o argumento boolean passado para o construtor de Computador. Note que apenas lendo o código de teste não temos a mínima ideia do que significa cada um desses argumentos.

Note que instanciar tantos objetos, com tantas relações se torna propenso a erros.

Nosso Computador recebe uma Collection de Componentes, logo podemos declarar um Componente porém podemos esquecer de passá-lo para a lista de Componentes passada ao Computador. Como aconteceu com o componente "mouse", outro erro é que o objeto corDoMonitor foi passado como parâmetro para o construtor do teclado, repare o código.

Por fim, nosso Computador recebe dois argumentos do tipo USB, porém oque significa o primeiro? E o segundo? Qual deles representa a USB dianteira, e qual da USB traseira? No código acima, invertemos a ordem das USB.

Veja que este é um ponto bem propício a erros: é difícil saber a ordem quando temos dois ou mais argumentos do mesmo tipo sendo recebidos. O mesmo acontece com a classe cor, qual a ordem de tons ela recebe? Vermelho, verde, azul? Apesar de fugir da convenção nada impediria ser Verde, vermelho, azul, por exemplo. E também nos atributos String recebidos pela classe Componente.

É impossível apenas ler o código que cria o cenário de teste e entender todas as informações.

Porém podemos reverter essa situação com a aplicação dos padrões de projeto Builder e Fluent Interface.

O Builder torna a criação dos objetos mais fácil. Nos ajuda através de métodos com nomes significativos a entender oque estamos declarando e elimina as ambiguidades que podem ocorrer na passagem de parâmetros com o mesmo tipo, como no caso dos parâmetros USB na classe Computador e int na classe Cor.

Com Fluent Interface é possível dar mais fluidez à sintaxe e em alguns casos economizar boas linhas de código.

Vamos aos exemplos práticos. Começamos criando a classe ComputadorBuilder:

public class ComputadorBuilder {
 private USB usbDianteira;

 private USB usbTraseira;

 private List< Componente > componentes = new ArrayList<>();

 private boolean possuiMarcaFamosa;

 public ComputadorBuilder comUSBDianteira(USB usbDianteira) {
  this.usbDianteira = usbDianteira;
  return this;
 }

 public ComputadorBuilder comUSBTraseira(USB usbTraseira) {
  this.usbTraseira = usbTraseira;
  return this;
 }

 public ComputadorBuilder comMarcaFamosa() {
  this.possuiMarcaFamosa = false;
  return this;
 }

 public ComputadorBuilder comComponente(Componente componente) {
  this.componentes.add(componente)
  return this;
 }

 public Computador build() {
  return new Computador(this.usbDianteira, this.usbTraseira, this.componentes, possuiMarcaFamosa);
 }
}


Veja que todos os métodos começam com a palavra "com", convencionamos assim, para que fique mais fácil autocompletar o nome dos métodos através da ide.

Vamos ao builder da classe Componente:

public class ComponenteBuilder {

 private String nome;

 private String descricao;

 private Cor cor;
 
 public ComponenteBuilder comNome(String nome) {
  this.nome = nome;
  return this;
 }

 public ComponenteBuilder comNome(String descricao) {
  this.descricao = descricao;
  return this;
 }

 public ComponenteBuilder comCor(Cor cor) {
  this.cor = cor;
  return this;
 }

 public Componente build() {
  return new Componente(this.nome, this.descricao, this.cor);
 }
}

E por fim o builder da classe Cor:

public class CorBuilder {

 private String nome;

 private int r;

 private int g;

 private int b;

 public CorBuilder comNome(String nome) {
  this.nome = nome;
  return this;
 }

 public CorBuilder comR(int r) {
  this.r = r;
  return this;
 }

 public CorBuilder comR(int g) {
  this.g = g;
  return this;
 }

 public CorBuilder comB(int b) {
  this.b = b;
  return this;
 }

 public Cor build() {
  return new Cor(nome, r, g, b);
 }
}

Agora vamos refatorar nosso teste, note que podemos encadear a chamada de nossos builders, criando assim uma fluidez ainda maior.

@Test
public void deveSugrirComputadorDeMarcaEComMuitosComponentesParaClienteExigente() {
 Computador computador = new ComputadorBuilder()
   .comUSBDianteira(new USB("2.0"))
   .comUSBTraseira(new USB("1.0"))

   .comComponente(new ComponenteBuilder()
    .comNome("nome do monitor")
    .comDescricao("descricao monitor")
    .comCor(new CorBuilder()
     .comNome("preto")
     .comR(0)
     .comG(0)
     .comB(0)
     .build()
    )
    .build()
   )

   .comComponente(new ComponenteBuilder()
    .comNome("nome do mouse")
    .comDescricao("descricao mouse")
    .comCor(new CorBuilder()
     .comR(0)
     .comG(0)
     .comB(0)
     .build()
    )
    .build()
   )


   .comComponente(new ComponenteBuilder()
    .comNome("nome do teclado")
    .comDescricao("descricao teclado")
    .comCor(new CorBuilder()
     .comR(100)
     .comG(100)
     .comB(100)
     .build()
    )
    .build()
   )


   .comComponente(new ComponenteBuilder()
    .comNome("nome webcam")
    .comDescricao("descricao webcam")
    .comCor(new CorBuilder()
     .comR(140)
     .comG(120)
     .comB(110)
     .build()
    )
    .build()
   )


  .build();

 Cliente cliente = new Cliente("renan");

 assertTrue(new Aconselhador(cliente).deveReceberComoSugestao(computador));
}

Apesar de nosso teste ter ficado com um número de linhas bem maior depois de refatorado, ganhamos muita expressividade, qualquer pessoa que ler o código entenderá o significado de cada argumento passado. E também ganhamos mais velocidade na hora de criar os objetos, decorrente da expressividade.

Bônus:
Padrões de projeto são como receitas, podemos segui-los, porém podemos adicionar nossos ingredientes a gosto, ou seja, não existe uma única forma de implementar um padrão. Note que usamos duas instâncias da classe Cor idênticas. Apesar de não estar descrito na do padrão builder, podemos criar um método estático para retornar um objeto com características fixas que usamos com frequência.

Algo como:
public static Cor corPreta() {
 return new Cor("preto", 0, 0, 0);
}