Blog da Caelum Blog da Caelum
Search
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.

arrow6 Responses

  1. Flavio Sakamura
    14 mos, 1 wk ago

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

  2. 14 mos, 1 wk ago

    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. 14 mos, 1 wk ago

    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. 14 mos, 1 wk ago

    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. 14 mos, 1 wk ago

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

  6. 14 mos, 1 wk ago

    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!

Leave A Comment