Possibilitando o overload de métodos em Javascript

Em muitas linguagens, como Java, é comum fazer o overload de métodos e que uns invoquem os outros:

public class NotaFiscal {
  public void add(Produto produto) {
    add(produto, 1);
  }
  public void add(Produto produto, int quantidade) {
    // ...
  }
}

Para identificar qual dos dois métodos será invocado depende do número de parâmetros passados (e os tipos). Já em Javascript, definir uma função com o mesmo nome a sobrescreve:

function adiciona(produto) {
  adiciona(produto, 1);
}
function adiciona(produto, categoria) {
 // essa funcao "sobrescreve" a anterior
}

Como trabalhar com variações de métodos com o mesmo nome, mas com número de argumentos (aridade) distintos? John Resig, criador do jQuery, sugeriu uma implementação simples e rápida:

    function addMethod(object, name, fn) {
      var old = object[name];
      object[name] = function() {
        if (fn.length == arguments.length) return fn.apply(this, arguments);
        else if (typeof old == 'function') return old.apply(this, arguments);
      };
    }

Ela é uma implementação de chain of responsibility trabalhando com funções ao invés de objetos. Nesse caso, cada chamada de addMethod armazena a última função criada e define uma nova função que, quando invocada, invocará a nova ou a anterior, de acordo com o número de parâmetros. Chain of responsibility se encaixou perfeitamente aqui. O uso seria:

function Users() {
  addMethod(this, "add", function() {});
  addMethod(this, "add", function(name) {});
  addMethod(this, "add", function(first, last) {});
}

new Users().add();
new Users().add("1");
new Users().add("1", "2");

Dessa forma também podemos implementar argumentos opcionais, adicionando funções que recebem menos parâmetros e delegam para a versão que recebe mais, passando os valores default.

Scott Olson sugeriu, no mesmo post, um código 3 vezes mais rápido que a implementação de Resig:

   function addMethod2(object, name, fn) {
      object._store = object._store || {};
      object._store[name] = object._store[name] || {};
      object._store[name][fn.length] = fn;
      object[name] = function() {
        if (this._store[name][arguments.length]) 
          return this._store[name][arguments.length].apply(this, arguments);
      };
    }

A vantagem da primeira abordagem é a de não adicionar um novo atributo ao objeto. Mas existem outras opções, em uma das madrugradas de programação do pessoal da Caelum, escrevemos uma variação que recebe uma array de funções, cada uma com um número diferente de argumentos. Iterando por essa array podemos transformá-la em um map, com a aridade como chave. Devolvemos uma função final que acessa a função desejada:

function functions(fs) {
	var self = {};
	fs.forEach(function(f) {
		self[f.length] = f
	});
	return function() {
		self[arguments.length].apply(this, arguments);
	};
}

Para utilizar, fazemos:

function Users() {
  this.add = functions([
    function() {});
    function(name) {});
    function(first, last) {}
  ]);
}

Uma solução que parece bem mais elegante, já que não há necessidade de uma chain of responsability aqui, pois não há prioridade: o mapeamento é direto aridade->função. Além disso, há um ganho colateral de performance.

Ainda há a pergunta: vale a pena definir métodos dessa forma diferente apenas para utilizar esse recurso? O mesmo pode ser igualmente aplicado a Ruby, com parecidas vantagens e desvantagens.

9 Comentários

  1. Flavio Sakamura 19/03/2012 at 12:03 #

    Gostei da solução para contornar o problema! Mas vou esperar a linguagem ter esse recurso um dia 🙂

  2. Caio Ribeiro Pereira 20/03/2012 at 17:54 #

    Parabéns pelo post, overload em Javascript é muito raro desenvolver no front-end, talvez agora com tecnologias como Node.js e Rhino que permite trabalhar Javascript no back-end o overload seja utilizado.

    A última solução de overload no post ficou muito enxuta e fácil de entender e de usar.

  3. Paulo Silveira 20/03/2012 at 18:36 #

    Também acho a solução interesante, um desafio curioso. Mas usar isso me parece uma maneira complicada de adicionar um recurso a linguagem, que ficará estranha aos olhos de outros programadores. Como o Caio disso, talvez se enquadre bem melhor no server-side. E olha que a ideia foi proposta pelo criador do jQuery.

  4. Nando Vieira 20/03/2012 at 19:08 #

    Fala Guilherme! Existem alguns erros (sintaxe e funcional) nesse código. Acredito que pelo fato de você tentar extrair em um exemplo mais conciso.

    O +this+ da função “functions”, nunca será a instância de User. Será sempre o objeto global. Duas soluções: executar a função +functions+ como call forçando o escopo, ou passar como parâmetro.

    Particularmente, não gosto de definir funções dentro do construtor, já que os objetos nunca herdarão o prototype (pelo menos as funções definidas ali).

    Além disso, dá para simplificar o modo como os métodos com overload são definidos.

    Meus dois centavinhos chorados: https://gist.github.com/2141765

    Abraços!

  5. Nando Vieira 20/03/2012 at 19:21 #

    Meh. Falei besteira quanto ao this. Mas o resto permanece! 😀

  6. Guilherme Silveira 20/03/2012 at 19:35 #

    Opa Nando!

    Sobre o prototype, faz sentido o que você comentou. Como a primeira sugestão do Resig era sem prototype e a comparação também, segui com ela pra examinar no jperf. No post do Resig ele coloca a variação do prototype logo depois.

    Boa sugestão a da array! No meio da brincadeira nós também implementamos como array e a velocidade era vergonhosa (tipo 10 vezes mais rápido que o hash), mas a limitação é que você precisa passar funções para 1 a n parâmetros… como a idéia era só mostrar soluções simples não coloquei no jperf ela.

    Aliás faltou o link pro jperf no post, desculpa: http://jsperf.com/jsoverloading

    Ignorando a questão da array e a vantagem/desvantagem dela é o mesmo código que o do post + o delta do Resig.
    Não bem o mesmo, pois context é um nome bonito, self não é no meu caso, eu devia mudar.

    Fizemos diversas variacoes (com pre-caching) que ficavam mais rápido que hash e suportando array, mas estava ficando mais complexo.. e a idéia seria só mostrar algumas coisas legais que podemos fazer na linguagem.

    Abraço!

  7. Gamarra 16/10/2013 at 15:39 #

    Apesar do post ser antigo vou dar uma pincelada postando meu ponto de vista (que pode não ser o melhor).

    Em casos como este, onde a linguagem por si só não oferece o recurso, ficar criando alternativas “estranhas” pode tornar a leitura do código confusa e talvez o “ganho” não compense.

    Overload pode ser entendido como a criação de dois métodos distintos cujo uma das características (o nome) é a mesma, mas que não deixam de ser dois métodos distintos.

    Em casos como este eu prefiro utilizar nomes distintos e simplificar o cógido.

    function adicionaProduto(produto) {
    adicionaProdutoComCategoria(produto, 1);
    }

    function adicionaProdutoComCategoria(produto, categoria) {
    //código feliz
    }

  8. Fernando 11/11/2013 at 15:02 #

    Cara, acho minha solução mais elegante, rápida e compacta

    https://github.com/myfingersarebroken/simpleOverload

    XD

  9. Guilherme Silveira 12/11/2013 at 10:11 #

    Oi Fernando, faz sentido sim, todas essas nossas abordagens (inclusive a do Gamarra) é a de tentarmos implementar invocação de acordo com tipos na linguagem. Em umas maneiras (como na minha) fazemos implicitamente, na sua explicitamente através do tipo (string, int) e na do gamarra explicitamente atraves do tipo proprio (produto, categoria)

Deixe uma resposta