Front End

13 set, 2011

Delegação vs herança no JavaScript

Publicidade

Quando perguntaram o que ele
faria diferente se tivesse que escrever o Java do zero, James Gosling sugeriu
que ele iria jogar fora a
herança de classe
e escrever uma linguagem somente de delegação.

Usar a herança como um
veículo para a reutilização de código é um pouco parecido com pedir o Mclanche feliz porque você quer o brinquedo de plástico. Claro que o circulo é
uma forma, e um cachorro é um mamífero – mas uma vez que você acabou com os
exemplos dos livros, a maioria das nossas hierarquias se tornam arbitrárias e
tênues – construídas para manipulação de comportamento mesmo quando queremos
representar a realidade. Descendentes sucessivos são confrontados com o
crescente número de comportamentos irrelevantes ou inesperados devido à
reutilização de poucas delas.

Delegação é uma técnica que promove a
reutilização do código ao permitir a invocação da função de execução no
contexto de uma instância específica – independentemente da linha hierárquica da
instância e da função. O JavaScript tem um excelente suporte para Delegação na
forma de call e apply, o que nos permite injetar um objeto no valor this de
qualquer função. Isso permite o compartilhamento de código sem restrições,
livre das limitações de difícil controle de hierarquias anormais e super
complexas.

Irei demonstrar, pelo uso de
um exemplo, como call e apply podem promover uma abordagem limpa e funcional
para a reutilização de código. Então irei discutir como a especificação ES 5 (pdf) permite a reutilização de funções
embutidas ao formalizar o conceito de funções genéricas.

Delegação de uma função customizada

Suponha que precisamos de um
objeto retângulo para um app de desenho. Vamos criá-lo da maneira tradicional,
usando new e constructor.

01	var Rectangle = function(left, top, length, width, options) {
02 this.left = left;
03 this.top = top;
04 this.length = length;
05 this.width = width;
06 if (options) {
07 this.color = options.color;
08 this.border = options.border;
09 this.opacity = options.opacity;
10 //... etc.
11 }
12 }
13
14 var myRectangle = new Rectangle(10, 10, 30, 20, {color:'#FAFAFA', opacity:0.7});

Também precisaremos saber se o retângulo sobrepõe outro. Vamos
adicionar esta função ao protótipo:

01	Rectangle.prototype.overlaps = function(another) {
02 var r1x1 = this.left,
03 r1x2 = this.left + this.width,
04 r1y1 = this.top,
05 r1y2 = this.top + this.height,
06 r2x1 = another.left,
07 r2x2 = another.left + another.width,
08 r2y1 = another.top,
09 r2y2 = another.top + another.height;
10
11 return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2);
12 }
13
14 myRectangle.overlaps(myOtherRectangle);

Agora suponha que em outro
lugar do nosso app temos um dashboard que renderiza um monte de dashlets. Gostaríamos
de saber se esses dashlets se sobrepõem. Poderíamos usar herança – fazer com que
o protótipo do Dashlet seja herdado do retângulo. Mas as instâncias do dashlet
agora são sobrecarrecadas por um conjunto de atributos irrelevantes: opacidade, cor (e
outras funções típicas de desenho, como rotacionar, escalar e enviesar). Pense
em ofuscação. Pense em footprint de memória. Mais do que isso, se a herança for
nosso objetivo, podem existir candidatos mais apropriados para nos estendermos,
como ContentFrame ou Portlet.

Pense nisso… tudo que
realmente queremos fazer é ver se dois dahslets se sobrepõem. Considerando que
um dashlet tem atributos para esquerda, topo, largura e altura (mesmo se
tivermos que derivá-los), a delegação atende ao mesmo objetivo com um footprint
muito mais leve:

1	Rectangle.prototype.overlaps.call(dashlet1, dashlet2);

Podemos até comparar dois objetos literais dessa maneira. Aqui está o script
completo para que você possa testá-lo:

01	var Rectangle = function(left, top, length, width, options) {
02 //whatever...
03 }
04
05 Rectangle.prototype.overlaps = function(another) {
06 var r1x1 = this.left,
07 r1x2 = this.left + this.width,
08 r1y1 = this.top,
09 r1y2 = this.top + this.height,
10 r2x1 = another.left,
11 r2x2 = another.left + another.width,
12 r2y1 = another.top,
13 r2y2 = another.top + another.height;
14
15 return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2));
16 }
17
18 Rectangle.prototype.overlaps.call(
19 {left: 10, top: 10, width 12, height: 6},
20 {left: 8, top: 15, width 9, height: 16});
21 //true
22 Rectangle.prototype.overlaps.call(
23 {left: 10, top: 10, width 12, height: 6},
24 {left: 8, top: 25, width 9, height: 16});
25 //false;

Funções genéricas

Tudo isso é ótimo,
mas não seria bom injetar instâncias em funções embutidas também? Infelizmente,
muitas funções embutidas são designadas para lançar um TypeError
se o valor this não for do tipo especificado:

1	Date.prototype.getMilliseconds.apply({year:2010});
2 //TypeError: Date.prototype.getMilliseconds called on incompatible Object

Felizmente, a especificação EcmaScript 5 (pdf) formaliza o conceito
de funções genéricas. Essas funções, por design, permitem que o valor this seja
de qualquer tipo. Por exemplo, podemos invocar o método search da String no
contexto de um array.

1	var hasNumbers = "".search.call(['a','b','c'],/[0-9]/) > -1;

Eu cataloguei toda a lista de
funções genéricas no final do artigo. Primeiro, vamos ver alguns exemplos por
tipo:

Métodos genéricos de Array.prototype
toString, toLocaleString, concat, join, pop, push, reverse, shift, slice,
sort, splice, unshift, indexOf, lastIndexOf, every, some, forEach, map, filter,
reduce, reduceRight

A maioria dessas
funções irá converter this para um
objeto antes de invocá-lo, então se estivermos usando uma String como contexto,
essas funções que manipulam diretamente o argumento (p.e. push e shift) irão
surpreender o usuário ao retornar um objeto. No entanto, algumas das funções
genéricas do Array funcionam bem com Strings:

01	[].forEach.apply("javascript",[function(char) {console.log("give me a " + char.toUpperCase())}]);
02 //give me a J
03 //give me a A
04 //etc...
05
06 var increment = function(char) {return String.fromCharCode(char.charCodeAt(0)+1)};
07 var hiddenMammal = [].map.call('rhinocerous',increment).join(''); // "sijopdfspvt"
08
09 var myObj = {'0':'nil', '1':'one', length:2};
10 [].push.call(myObj,'two');
11 myObj; //{'0':'nil', '1':'one', '2':'two' length:3}

Métodos genéricos de String.prototype
charAt, charCodeAt, concat, indexOf, lastIndexOf, localeCompare, match,
replace, search, splice, split, substring, toLowerCase, toLocaleLowerCase,
toUpperCase, to LocaleLowerCase, trim, substr

A maioria dessas funções irá
converter o objeto this para uma String antes de invocar.
Dessa maneira, se estivermos injetando um Array como contexto, precisaremos
converter o resultado de volta para um Array no final, usando split.

01	"".trim.apply([" a","b "]).split(",");
02 //["a","b"]
03
04 "".toLowerCase.apply(["DIV","H1","SPAN"]).split(",");
05 //["div","h1","span"]
06
07 "".match.call(["a16","b44","b bar"],/[a-z][0-9]+/g);
08 //["a16", "b44"]
09
10 "".replace.call(
11 ['argentina','brazil','chile'],
12 /b./g, function(a){ return a.toUpperCase(); }
13 ).split(',');
14 //['Argentina',"Brazil","Chile"]

Métodos genéricos de Date.prototype
toJSON

Este método precisa que o
valor this tenha o método toISOString.

Object.prototype.toString
OK, não é estritamente uma
função genérica (uma vez que todo objeto de primeira classe é um objeto – um
erro de digitação nunca pode ser jogado em call ou aplly – a não ser que
estiver usando o ES 5 em modo estrito), no entanto, este é um grande candidato a
mostrar o poder da delegação.

Desde os primeiros dias do
JavaScript, os desenvolvedores lutam pela melhor maneira de determinar se um
objeto é um Array. A solução water-tight tem sido a principal opção
recentemente, e ela alavanca a habilidade de um Array entrar no método
toString
do Objeto:

1	function isArray(obj) {
2 return Object.prototype.toString.call(obj) == "[object Array]";
3 }

Meta Delegation (sort of)
Como o ES 5m a função apply
foi “generalizada”. O segundo argumento não precisa mais ser um Array.
Qualquer objeto que tiver propriedades de comprimento e índice pode ser usado
(por exemplo arguments ou presumivelmente uma string).

ES 5, 15.3.4.3: Na
edição 3, um TypeError é lançado se o segundo argumento passado para Function.prototype.apply
inão for um array object nem um arguments object. Na edição 5, o segundo
argumento deve ser qualquer tipo de objeto-array que tenha uma propriedade de
comprimento válida.

Infelizmentem os browsers não foram rápidos em adotá-lo.

Delegação via funções
“estáticas” (somente para Mozilla)

Dmitry Soshnikov ressalta que o mecanismo SpiderMonkey
suporta uma forma muito simples de delegação ao passar argumentos para a definição
autônoma da função. Legal! 

1	Array.map('abc', String.toUpperCase); //["A", "B", "C"]
2 String.toUpperCase(['a']); //"A"

Finalizando

A implementação de herança é
um conceito bacana – eu o respirei durante os 12 anos que programei em Smalltalk
e Java – mas devemos estar abertos para alternativas mais enxutas e versáteis
quando possível. A delegação de funções usando call e apply permite que as
utilidades do JavaScript escolham as melhores funcionalidades necessárias sem a
bagagem de uma hierarquia não intuitiva, inchada e imensamente complexa.

Apêndice: referência de funções genéricas

(Veja a 5ª edição do ECMA-262)
15.4.4.2 Array.prototype.toString ( )
15.4.4.3 Array.prototype.toLocaleString ( )
15.4.4.4 Array.prototype.concat ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.5 Array.prototype.join (separator)
15.4.4.6 Array.prototype.pop ( )
15.4.4.7 Array.prototype.push ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.8 Array.prototype.reverse ( )
15.4.4.9 Array.prototype.shift ( )
15.4.4.10 Array.prototype.slice (start, end)
15.4.4.11 Array.prototype.sort (comparefn)
15.4.4.12 Array.prototype.splice (start, deleteCount [ , item1 [ , item2 [ , …
] ] ] )
15.4.4.13 Array.prototype.unshift ( [ item1 [ , item2 [ , … ] ] ] )
15.4.4.14 Array.prototype.indexOf ( searchElement [ , fromIndex ] )
15.4.4.15 Array.prototype.lastIndexOf ( searchElement [ , fromIndex ] )
15.4.4.16 Array.prototype.every ( callbackfn [ , thisArg ] )
15.4.4.17 Array.prototype.some ( callbackfn [ , thisArg ] )
15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] )
15.4.4.19 Array.prototype.map ( callbackfn [ , thisArg ] )
15.4.4.20 Array.prototype.filter ( callbackfn [ , thisArg ] )
15.4.4.21 Array.prototype.reduce ( callbackfn [ , initialValue ] )
15.4.4.22 Array.prototype.reduceRight ( callbackfn [ , initialValue ] )
15.5.4.4 String.prototype.charAt (pos)
15.5.4.5 String.prototype.charCodeAt (pos)
15.5.4.6 String.prototype.concat ( [ string1 [ , string2 [ , … ] ] ] )
15.5.4.7 String.prototype.indexOf (searchString, position)
15.5.4.8 String.prototype.lastIndexOf (searchString, position)
15.5.4.9 String.prototype.localeCompare (that)
15.5.4.10 String.prototype.match (regexp)
15.5.4.11 String.prototype.replace (searchValue, replaceValue)
15.5.4.12 String.prototype.search (regexp)
15.5.4.13 String.prototype.slice (start, end)
15.5.4.14 String.prototype.split (separator, limit)
15.5.4.15 String.prototype.substring (start, end)
15.5.4.16 String.prototype.toLowerCase ( )
15.5.4.17 String.prototype.toLocaleLowerCase ( )
15.5.4.18 String.prototype.toUpperCase ( )
15.5.4.19 String.prototype.toLocaleUpperCase ( )
15.5.4.20 String.prototype.trim ( )
15.9.5.44 Date.prototype.toJSON ( key )
B.2.3 String.prototype.substr (start, length)

Leitura complementar

Allen Holub em JavaWorld Why Extends is
Evil

Bill Venners: A Conversation
with Java’s Creator, James Gosling

Nick Fitzgerald: OOP The Good
Parts: Message Passing, Duck Typing, Object Composition, and not Inheritance
– um excelente post no qual Nick bate
mais um pouco na herança e delineia três alternativas adicionais.

?

Texto
original disponível em http://javascriptweblog.wordpress.com/2010/12/22/delegation-vs-inheritance-in-javascript/