24 de mai. de 2013

Refatoração: substitua exceção por teste

Durante uma aula surgiu uma implementação feita por um aluno, créditos ao Matheus Cezar, onde se encaixa uma refatoração bem interessante: substituir exceção por teste.

A crônica é a seguinte: considere um método que retorne um elemento de uma coleção pelo índice ou nulo caso não exista o dado elemento. Um código cobaia pode ser visto a seguir.

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        // implementação aqui
    }
}

Existem vários modos de cumprir a API do método, uma delas é introduzindo um mau cheiro no código, utilizando a captura de exceções como desvio condicional. É bem simples, veja a seguir:

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
        
    public static void main(String[] args) {
        System.out.println(getOpcao(9));
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        try {
            return opcoes[numero];
        } catch (ArrayIndexOutOfBoundsException e) {
            return null;
        }
        
    }
}

Esta é uma implementação intuitiva a primeira vista, note que ela resolve o problema, ou seja, neste exemplo é impresso null, pois como não há o índice 9 o código opcoes.get(numero) lança a exceção ArrayIndexOutOfBoundsException que é capturada e suprimida, retornando null na situações excepcionais.

Existem um detalhe importante a respeito de exceções, elas introduzem um overhead, já que cada vez que acontecem, além do bloco condicional um objeto é criado para representar a exceção.

O que eu quero dizer é que esta implementação, baseada na captura da exceção, tem baixa performance se comparada com a realização de um teste, que é a refatoração proposta e inclusive documentada no livro do Fowler.

Escrevi um pequeno benchmark para instrumentar este código, veja a seguir:

public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        long inicio = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            getOpcao(9);
        }
        System.out.println((System.currentTimeMillis() - inicio) + "ms");
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        try {
            return opcoes.get[numero];
        } catch (ArrayIndexOutOfBoundsException e) {
            return null;
        }
        
    }
}
O tempo decorrido para executar 10000 (dez mil) consultas é de 110ms no meu modesto notebook equipado com um Intel Pentium Dual Core. A refatoração a seguir torna o código mais claro e também mais rápido, é a substituição de exceção por teste, veja a seguir:
public class Main {

    static String[] opcoes = {"zero", "um", "dois"};
    
    public static void main(String[] args) {
        long inicio = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            System.out.println(getOpcao(9));
        }
        System.out.println((System.currentTimeMillis() - inicio) + "ms");
    }
    
    // retornar a opção ou nulo caso não seja encontrada
    static String getOpcao(int numero) {
        if (numero >= 0 && numero < opcoes.length)        
            return opcoes[numero];        
        return null;
    }
}

Esta implementação executa em 5ms em média, bem melhor do que os 110ms anteriores, não? A mecânica é simples, em vez de confiar no catch nós fazemos um teste para verificar a validade do parâmetro.

É isso, deixe seu comentário se quiser.

Um comentário:

Felipe Bicca disse...

Muito bom o post... E esse Matheus é fera... Abraço