22 de mar. de 2013

Padrão de implementação VARARGS (funções variádicas) e sua relação com o princípio Zero One Infinity

É uma situação comum termos de escrever um método (ou função) que aceite um número variável de argumentos.

Existem algumas técnicas para atingir isso, por exemplo, usando sobrecarga ou varargs.

Para tratar destas técnicas vou começar com um estudo de caso baseado em uma API bem simples: considere uma classe utilitária com um método utilizado para gerar o caminho de pão, ou popular breadcrumb. Este método recebe um caractere separador a seguir os níveis 1, 2 e 3. Um separador e o nível 1 são argumentos obrigatórios, já o nível 2 e 3 são opcionais. Exemplo de uso da API:

// Imprime: > Home
System.out.println(WebHelper.caminhoPao('>', "Home"));
// Imprime: > Home > Eletrônicos
System.out.println(WebHelper.caminhoPao('>', "Home", "Eletrônicos"));
// Imprime: > Home > Eletrônicos > TV's
System.out.println(WebHelper.caminhoPao('>', "Home", "Eletrônicos", "TV's"));

Um modo de implementar, mesmo sendo uma implementação ingênua com sobrecarga de método, poderia parecer-se com isso:

class WebHelper {
     
    public static String caminhoPao(char separador, String nivel1) {
        return separador + " " + nivel1;
    }
     
    public static String caminhoPao(char separador, String nivel1, String nivel2) {
        StringBuilder caminho = new StringBuilder(separador + " " + nivel1);
        return caminho.append(" ")
                      .append(separador)
                      .append(" ")
                      .append(nivel2)
                      .toString();
    }
     
    public static String caminhoPao(char separador, String nivel1, String nivel2, String nivel3) {
        StringBuilder caminho = new StringBuilder(separador + " " + nivel1);
        return caminho.append(" ")
                      .append(separador)
                      .append(" ")
                      .append(nivel2)
                      .append(" ")
                      .append(separador)
                      .append(" ")
                      .append(nivel3)
                      .toString();
    }
}

Esta implementação problemas mais e menos evidentes (problemas de projeto, não de implementação).

O problema mais evidente é a duplicação, ou seja, o método viola o Princípio de Projeto DRY: não há reaproveitamento de código, os métodos tem o mesmo propósito, mas implementam toda a funcionalidade novamente. Como seria se precisássemos de quatro níveis?

O problema menos evidente é a rigidez, a falta de flexibilidade para o caso de, no futuro, precisarmos de mais níveis. Esta rigidez também viola um Princípio de Projeto pouco conhecido, o ZOI, Zero One Infinity Rule, em português Regra do Zero Um Infinito.

O ZOI afirma que não deve ser imposto qualquer limite arbitrário de instâncias de qualquer entidade. Em outras palavras, uma entidade deve ser totalmente proibida, ou uma instância pode ser aceita, ou então qualquer número de instâncias. É a típica sensação de, por exemplo, se são aceitos dois argumentos, então por que não três? E por que não quatro? Deveria haver um limite?


<off-topic>Isaac Asimov em seu livro "Os próprios Deuses" afirma que "o número 2 é ridículo e não deveria existir", se referindo a universos no sentido de, se tu aceitas que existe 2 universos, ou seja, que não existe um único universo, então tens que aceitar a possibilidade de existir n universos.</off-topic>


Toda esta explicação inicial é para embasar e dar motivação a aplicação de VARARGS, que é um modo mais elegante de permitir um número variável de argumentos em um método/função, consolidando uma Função Variádica. Cada linguagem tem uma notação especial para definir Funções (ou Métodos) Variádicas, em Python é usado *args, já em Java é usado Tipo... args. A seguir a implementação em Java:

class WebHelper {
     
    public static String caminhoPao(char separador, String nivel1, String... demaisNiveis) {
 
        StringBuilder caminho = new StringBuilder(separador + " " + nivel1);
                 
        for (String nivel : demaisNiveis) {
            caminho.append(" ")
                    .append(separador)
                    .append(" ")
                    .append(nivel);
        }
         
        return caminho.toString();
    }
}

Com essa assinatura no método fazemos com que o separador e o nivel1 sejam argumentos obrigatórios sendo opcional o último argumento, ou seja, passar os níveis 2, 3 e assim por diante. A variável definida no parâmetro variável é um array do tipo especificado e pode ser iterado para tratar os valores recebidos. Com a implementação acima é possível fazer qualquer uma das chamadas abaixo sem problemas:

// demaisNiveis = [] e Imprime: > Home
System.out.println(WebHelper.caminhoPao('>', "Home"));
// demaisNiveis = [] e Imprime: > Home > Eletrônicos
System.out.println(WebHelper.caminhoPao('>', "Home", "Eletrônicos"));
// demaisNiveis = ["Eletrônicos", "TV's"] e Imprime: > Home > Eletrônicos > TV's
System.out.println(WebHelper.caminhoPao('>', "Home", "Eletrônicos", "TV's"));
// demaisNiveis = ["Eletrônicos", "TV's", "LED"] e Imprime: > Home > Eletrônicos > TV's > LED
System.out.println(WebHelper.caminhoPao('>', "Home", "Eletrônicos", "TV's", "LED"));


Espero que tenha sido útil a informação e qualquer comentário, sugestão, dúvida fique a vontade para comentar.

Nenhum comentário: