Desenvolvimento

7 jun, 2011

A herança é má e deve ser destruída

Publicidade

Quando eu construí o Animator.js,
recebi muitas críticas por promover que a herança não é uma coisa boa.
Esperando evitar uma guerra, mudei minha posição para ‘a herança é muitas
vezes útil, mas é muito mais ultrapassada’. Nos últimos meses, eu tenho tentado
descobrir como exatamente ela deveria ser usada, e concluí – pelo menos para os
tipos de sistemas que desenvolvedores GUI constroem – que a resposta é nunca.

Tenho trabalhado em uma porta Flash para o Animator.js,
que teve sua performance melhorada. É
um componente complexo de software, com cerca de 30 classes, mas ele usa Strategy Pattern no
lugar da herança. Eu me apaixonei pela minha posição anti-herança, e quero um
tempinho para poder me explicar. Neste artigo, eu faço um discurso retórico por
alguns parágrafos para tentar te persuadir a não usar herança, e então mostrar
como você pode usar Strategy Pattern para seu próprio software. As amostras de
código deste artigo estão em Actionscript, mas o conceito se aplica da mesma
maneira ao JavaScript, ou a qualquer linguagem orientada ao objeto.  

Por que a herança é horrível

Toda a dor causada pela herança pode ser atribuída ao fato de que ela
força relacionamentos ‘é-um’ ao invés de ‘tem-um’. Se a classe R2Unit estende
o Droid, então o R2Unit é-um Droid. Se a classe Jedi contém uma instância
variável do tipo Lightsabre, então um Jedi tem-um Lightsabre.

O outro tipo de herança – Falando nisso, minha bronca é com herança concreta
– uma classe derivando de outra e herdando o comportamento da classe pai. Eu
não tenho problemas com a interface da herança, na qual somente assinaturas de
métodos foram herdadas.

A diferença entre relacionamentos é-um e tem-um é
bem conhecida e uma parte fundamental do OOAD,
mas o que é menos conhecido é que quase todo relacionamento é-um seria melhor
se fosse rearticulado como um relacionamento tem-um.

Ahn?

Em vez de estender a classe e adicionar algumas funcionalidades na
subclasse, tente colocar a nova funcionalidade dentro de sua própria classe.
Digamos que você queira criar uma classe DarkJedi; uma classe dark Jedi é como
um Jedi normal, mas com poderes escuros também. A maneira óbvia de fazer isso é
entendendo a classe Jedi e adicionando alguns métodos apropriados:

// bad
class Jedi {
function drawSabre():Sabre { ... }
}
class DarkJedi extends Jedi {

function crushTownspeople():void { ... }
}
dj:DarkJedi = new DarkJedi();
dj.crushTownspeople();

Essa parece a abordagem mais simples, e ela é, em um primeiro momento. No entanto, seus poderes escuros estão presos dentro da classe DarkJedi. Se você precisar fazer um DarkDroid e uma DarkSpaceship que possam juntas esmagar (crush) as pessoas da cidade (townspeople), você está perdido. Essas classes obviamente não podem estender o Jedi, então você terá que duplicar a funcionalidade de esmagamento das pessoas da cidade através de todo o seu DarkArmy ou dividi-la dentro de funções de utilidade que você possa chamar de cada método crushTownspeople. De qualquer maneira, fica complicado.

Agora digamos que você tenha feito isto:

// good
class Jedi {
function drawSabre():Sabre { ... }
}
class DarkPowers {
function crushTownspeople():void { ... }
}
class DarkJedi extends Jedi {
// DarkJedi has-a DarkPowers
public var darkPowers:DarkPowers = new DarkPowers();
}
dj:DarkJedi = new DarkJedi();
dj.darkPowers.crushTownspeople();

Tudo que era possível na primeira versão ainda é possível na segunda,
mas, como o DarkPowers é uma classe separada, não existe limite no tipo de
objeto que pode ser mau.

class DarkHippo {
public var darkPowers:DarkPowers = new DarkPowers();
public function capsizeCanoe(canoe:Canoe):void { … }
}

É, mas eu não faço Jedis. Nem hipopótamos.

Boa, mas o problema acima ocorre em qualquer lugar em que a herança ocorra. Darei um exemplo do API do Flash Player porque considero o Actionscript 3.0 um dos trabalhos mais bonitos da engenharia de software que usei nos últimos anos, e se o time que o criou não consegue fazer a herança funcionar corretamente, como o resto de nós pode esperar conseguir isso?

Aqui temos dois exemplos do uso de herança, um apropriado, e o outro não. Por “apropriado”, quero dizer que os problemas que a herança inevitavelmente acaba causando são provavelmente compensados pela baixa complexidade.

Bom

No Flash, a hierarquia do DisplayObject é um conjunto de classes que representam os diferentes tipos de objetos na tela: 


A hierarquia do DisplayObject é um bom uso da herança. O MovieClip estende o Sprite e adiciona uma timeline. Um MovieClip é-um Sprite do início ao fim: o objetivo principal do MovieClip é ser ‘um novo tipo de Sprite’. Um MovieClip sempre pode ser usado no lugar de um Sprite, ele tem os mesmos métodos e as mesmas capacidades de um Sprite, é desenhado na tela como um Sprite; ele apenas adiciona uma timeline extra de funcionalidades no topo.

Mais importante é que você geralmente não precisa usar recursos da timeline na classe do Movieclip, independentemente dos recursos de desenho na tela da classe do Sprite.

Nem tudo é perfeito. Eu já quis adicionar uma funcionalidade comum para ambos MovieClips e Sprites, mas não consegui porque não é possível modificar a classe base DisplayObjectContainer em que ambas essas classes estendem (e eu me recuso a usar monkey patch). No final, eu tive que usar MovieClips nos quais os Sprites iriam funcionar, o que é ineficiente.

Seria possível rearticular esse relacionamento como um tem-um: a funcionalidade na classe MovieClip iria estender o Sprite com uma propriedade ‘timeline’ pública.

// old style:
mc.goToAndPlay("shizzle");
// would become:
mc.timeline.goToAndPlay("shizzle");

Mas a versão existente funciona bem o suficiente, e acho que os arquitetos do API do Flash tomaram a decisão correta. 

Mau
No Flash, o código para despachar eventos DOM está contido na classe EventDispatcher.

O EventDispatcher é um mau uso da herança, porque, para conseguir despachar os eventos, as classes estendem o EventDispatcher. Isso é uma análise incorreta: só porque uma classe pode despachar eventos, isso não significa que o principal objetivo da classe é ‘ser um novo tipo de EventDispatcher’. As chances de a classe ter um objetivo fundamental e a habilidade de despachar eventos são secundárias ao objetivo principal do objeto.

Isso é ok para DisplayObjects, porque todos os DisplayObjects despacham eventos, e o DisplayObject estende o EventDispatcher, mas o que você faz se você quiser despachar eventos de um objeto que não consegue estender o EventDispatcher por já estar estendendo outra coisa, talvez uma Array? Nesse caso, você deve se virar com a interface do EventDispatcher e um monte de código clichê extra.

O problema não aconteceria se, em vez de estender o EventDispatcher para despachar eventos, a classe tivesse ‘eventos’ de propriedade pública que contivessem um EventDispatcher. Em vez de chamar foo.addEventListener(), você iria chamar foo.events.addListener(). Isso poderia ser uma convenção, ou poderia ser reforçado com uma interface. Agora qualquer classe pode participar do sistema de eventos, basta adicionar uma propriedade pública, porque o relacionamento foi rearticulado de “x é um EventDispatche”’  para “x tem um event dispatcher”. (Sendo justo com o time do Flash player API, eles tomaram essa decisão porque estavam seguindo a DOM Events specification, que precisa que métodos como addEventListener existam nos nós do DOM, não como objetos de eventos separados).

Por que as pessoas usam
herança?

O
JavaScript faz com que a herança seja um saco para implementar, então por que
ela é tão popular entre os frameworks? Parte do problema é que o JavaScript
sempre pareceu ser uma linguagem de script leve e inconsistente, comparada ao
Java e sua rigidez aparente; dispostos a provar que eles estão usando uma
linguagem real para gente grande, os desenvolvedores JavaScript se apressaram
em adotar um recurso OO que nunca foi bom para começo de conversa. Linguagens
fortemente escritas não usam herança porque é uma boa idéia, eles usam por 3
razões ruins:

  1. Porque elas têm que usar. O Java é bagunçado por situações em que, por exemplo,
    um método precisa de um conjunto de inputs, então é passado um InputStream. O InputStream deveria ser uma interface, mas não é, é uma classe. Portanto, se
    você quiser passar seu próprio input para dentro de tal método, você terá que
    criar uma nova subclasse do InputStream, ou o programa não será compilado.
  2. Alguns dos poucos casos em que a herança é apropriada são encontrados na criação
    de grandes frameworks como HTML DOM e a hierarquia DisplayObject do Flash: o
    tipo de sistemas aos quais os novos desenvolvedores são expostos. As pessoas
    veem esses sistemas quando estão aprendendo a programar, e acreditam que é
    assim que tem que ser feito (como eu).
  3. Pura
    força do hábito: alguns <http://en.wikipedia.org/wiki/Simula>
    people<http://en.wikipedia.org/wiki/Smalltalk>  nos anos 60
    decidiram que era uma boa idéia, e dá muito trabalho parar.

Um exemplo

Suponha que você tenha uma classe Ball que implementa um red Ball. Você
estende essa classe para criar uma classe BouncingBall que, bem, pula. Você
mais uma vez estende a Ball para criar uma classe FadingBall que fades in e
fades out. Não me pergunte por que, vamos apenas pensar que seu cliente é
estranho. Vai ficar algo parecido com isto (a ball, não o cliente):

class Ball extends MovieClip {    
function Ball() {
graphics.beginFill(0xAA0000);
graphics.drawCircle(0, 0, 20);
graphics.endFill();
}
}
class BouncingBall extends Ball {
private var frame:int = 0;
function BouncingBall() {
addEventListener(Event.ENTER_FRAME, setPosition);
}
private function setPosition(e:Event):void {
frame++;
this.y = 280 - Math.abs(Math.cos(frame / 15) * 200);
}
}
class FadingBall extends Ball {
private var frame:int = 0;
function FadingBall() {
addEventListener(Event.ENTER_FRAME, setAlpha);
}
private function setAlpha(e:Event):void {
frame++;
this.alpha = Math.abs(Math.cos(frame / 15));
}
}

Agora digamos que você queira fazer uma bola que pula e esmaece. Droga. Seu problema aqui é que bolas que pulam e que esmaecem não são na verdade novos tipos de bolas, são novas maneiras de comportamento das mesmas bolas. A bola tem um comportamento de pulo. Ao usar a herança, o código que lida com os pulos e com o esmaecimento fica trancado dentro das classes BouncingBall e FadingBall e não pode ser usado em outro lugar.

Se você estiver prestando atenção, já sabe a solução:

// To create a bouncing and fading ball:
// var d = new StrategyBall();
// d.yMotionStrategy = new AbsSineStrategy(80, 200);
// d.alphaStrategy = new AbsSineStrategy(0, 1);
class StrategyBall extends MovieClip {

public var yMotionStrategy:NumberSequenceStrategy;
public var alphaStrategy:NumberSequenceStrategy;

function StrategyBall() {
graphics.beginFill(0xAA0000);
graphics.drawCircle(0, 0, 20);
graphics.endFill();
addEventListener(Event.ENTER_FRAME, handleEnterFrame);
}

private function handleEnterFrame(e:Event):void {
if (yMotionStrategy != null) {
y = yMotionStrategy.nextValue();
}
if (alphaStrategy != null) {
alpha = alphaStrategy.nextValue();
}
}
}
interface NumberSequenceStrategy {
function nextValue():Number;
}
// Note how one class is used for both the fading and bouncing behavior - componentisation allows
// for greater code reuse
class AbsSineStrategy implements NumberSequenceStrategy {

private var frame:int = 0;
private var from:Number;
private var to:Number;

public function AbsSineStrategy(from:Number, to:Number) {
this.from = from;
this.to = to;
}

public function nextValue():Number {
frame++;
return to + Math.abs(Math.sin(frame / 15)) * (from - to);
}
}

Além de ser capaz de fazer uma bola que pula e que esmaece, essa solução apresenta dois grandes benefícios:

  1. Você pode mudar o comportamento de cada bola na hora da execução: uma boucing ball pode se tornar uma fading ball a qualquer momento.
  2. Você pode reutilizar algoritmos de geração de números entre propriedades – note como existe somente um algoritmo de geração de número – AbsSineStrategy – é parametrizado com os valores ‘de’ e ‘para’ de modo que você possa utilizá-lo para controlar alpha ou altura.

Chega de teoria – no meu próximo artigo eu irei de fato fazer algo útil com tudo isso!

Faça download do código completo

?
Texto original disponível em http://berniesumption.com/software/inheritance-is-evil-and-must-be-destroyed/