Back-End

26 nov, 2012

Herança múltipla em Java

Publicidade

O Java foi concebido sem heranças múltiplas. Enquanto alguns desenvolvedores consideram isso uma falha, a verdade é que o design geral do Java suporta a resolução de problemas comumente solucionados com heranças múltiplas de outras maneiras. Em particular, a única hierarquia enraizada (com o objeto como o último ancestral das classes) e as interfaces do Java resolvem a maioria dos problemas comumente solucionados usando múltiplas heranças em C++.

No entanto, existem algumas situações nas quais a herança múltipla é bastante útil. Neste artigo, iremos considerar um caso especial, e também o caso geral de heranças múltiplas.

Heranças Mixin

Nas heranças mixin, uma classe é especificamente designada para ser usada como uma das classes em um esquema de heranças múltiplas. Dizemos que ele fornece certa funcionalidade que é “misturada dentro” (“mixed in”) de outras classes que querem essa funcionalidade. Um outro jeito de pensar em heranças mixin é que à classe mixin
é dada um novo parente de classe, de modo que o mixin pareça se estender para outra classe. Em alguns projetos, é necessário contar com serviços comuns que devem ser fornecidos por muitas classes. A herança mixin é uma maneira de centralizar o desenvolvimento desses serviços.

Para fornecer a herança mixin, vamos precisar definir duas interfaces, bem como pelo menos uma classe que forneça o serviço: a classe Mixin. Em algumas situações, uma dessas interfaces fica vazia e pode então ser otimizada.

As Interfaces

A primeira interface (sempre necessária) define o que a classe mixin irá fornecer para os serviços. Ela define um ou mais métodos que serão implementados pela classe mixin. Iremos ver um exemplo simples e abstrato aqui. A classe será chamada (abstratamente) de MProvides, para enfatizar que ela define o que qualquer mixin compatível deve
fornecer. Iremos também partir do pressuposto de que o único serviço fornecido é uma função void, e daremos a ela o nome abstrato de func. Na prática, no entanto, pode haver vários números de métodos definidos, e eles podem ter quaisquer assinaturas.

interface MProvides
{	void func();
}

Um recurso especial da herança mixin que geralmente não está presente nos casos gerais de heranças múltiplas é o fato de as classes mixin pedirem por serviços da classe na qual ela está misturada. Isto é, para poder fornecer o serviço “func”, a mixin pode precisar pegar algumas informações das outras classes. Definimos isso com outra interface. Daremos a ela o nome abstrato MRequires para indicar que ela necessita de um ou de mais serviços da classe na qual ela está misturada.

Aqui iremos supor que as mixins compatíveis pedem que a outra classe forneça um método getValue, que retorna um int.

interface MRequires
{	int getValue();
}

Geralmente, o MRequires terá um nome mais apropriado e terá um ou mais métodos com assinatura arbitrária. No entanto, qualquer classe na qual misturamos nosso mixin deve fornecer serviços com os nomes e assinaturas dados, apesar de que ela não precisa implementar explicitamente a interface MRequires. Se a MRequires estiver vazia, ela pode ser otimizada.

Mixin

A classe mixin irá implementar a interface MProvides. Ela também será criada ao passar seu construtor como um argumento que implementa o MRequires. Aqui está um simples exemplo chamado (mais uma vez abstratamente) Mixin.

class Mixin implements MProvides
{	public Mixin(MRequires parent) { this.parent = parent; }

	public void func() { System.out.println("The value is: " + parent.getValue()); }

	private final MRequires parent;
}

Quando um novo Mixin é criado, ele sabe sobre o objeto MRequires. Ele pode então consultar esse objeto usando os serviços definidos no MRequires com o objetivo de fornecer seus próprios serviços. Nós chamamos esse objeto de pai para enfatizar que seu objetivo é simular dar à classe Mixin uma nova classe pai. Se o MRequires estiver vazio, nenhum objeto precisa ser passado para dentro do construtor. Note que, no geral, o construtor Mixin pode pedir por outros parâmetros também.

A classe usada como base

Agora digamos que queremos misturar essa classe com outra. Essa classe deve ter todos os métodos necessários para a interface MRequires, mas ela não precisa  implementar essa interface. Ela provavelmente terá outros métodos também. Aqui está um exemplo:

Class Parent
{	public P(int value ) { this.val = value; }
	public int getValue() { return this.val; }

	public toString() { return "" + this.val; }
	private int val;
}

O resultado da mistura

Agora, para realmente misturar essas duas classes, precisamos primeiro construir uma nova classe que estenda o Parent (pai) e implemente ambas interfaces:

class Child extends Parent implements MRequires, MProvides
{

Essa classe define ambos os serviços do Parent e aqueles do mixin (MProvides). Para implementar a classe Child filha, criamos um novo objeto Mixin e o salvamos. Iremos também delegar todas as mensagens definidas na interface MProvides para esse objeto. Quando criamos o objeto MIxin, precisamos passá-lo um objeto que implemente o MRequires, mas esse objeto faz isso, ao mesmo tempo em que a nova classe implementa a interface MRequires.

public Child(int value)
	{	super(value); 
		this.mixin = new Mixin(this);
	}

	public void func(){ mixin.func(); }

	private final MProvides mixin;

}

Note que o serviço chamado
func é fornecido pelo objeto Mixin, como definido anteriormente. Ele não precisa ser redefinido na classe Child. Note também que a classe Child automaticamente implementa o MRequires, uma vez que o método herdado cumpre com o contrato necessário definido pela interface MRequires. No entanto, nós temos que fornecer uma simples função de delegação para os métodos do MProvides.

A chave aqui é perceber que fomos capazes de construir a classe Mixin para ser um mixin, e então definimos duas interfaces necessárias. Note que na verdade são os serviços definidos pelo MProvides que estão misturados, e não, propriamente falando, a classe Mixin. Isso na verdade adiciona alguma flexibilidade, uma vez que várias classes podem implementar essa interface e então serem misturadas em outras classes para fornecer esse tipo de herança múltipla.

Heranças Múltiplas Gerais

Podemos usar o que foi dito acima para ver como fornecer heranças múltiplas gerais. A diferença aqui é que talvez não sejamos capazes de designar uma das classes para ser um mixin. No caso geral, as duas classes são pré-definidas e não precisam de serviços uma da outra. Isso significa que a interface MRequires não é necessária. No entanto,
ainda precisamos definir uma interface, uma vez que o Java não nos permite misturar duas classes juntas.

Se tivéssemos o luxo de designar pelo menos uma dessas classes, então poderíamos agir como antes, tratando cada classe como o mixin e definindo a interface que ele irá
implementar. Caso contrário, precisaríamos fazer um pouco mais. Suponha que gostaríamos de misturar a classe Parent da seção anterior com a classe Other definida abaixo:

Class Other
{	public Other(int value) { ... }

	public void whatever()
	{...
	}
}

Como o Java irá permitir a extensão de apenas uma classe, precisamos definir uma interface que declare os recursos públicos da outra. Faremos isso para facilitar nossa vida, apesar de que você pode escolher fazer o próximo passo para a classe de menor importância, e cujos métodos serão chamados menos frequentemente. Aqui iremos definir uma interface para fornecer os métodos públicos da classe Other.

Interface OtherInterface
{	void whatever();
}

Podemos agora construir uma sub-classe da Other ao implementar essa nova interface e fornecer quaisquer construtores necessários (que não são herdados).

class OtherChild extends Other implements OtherInterface
{	public OtherChild (int value){ super(value); }
}

Se tivéssemos a liberdade de modificar a classe Other, poderíamos evitar a classe OtherChild e ter apenas a Other implementando essa nova interface.

Essa nova classe é como se fosse uma Other, mas ela anuncia que implementa a OtherInterface. A partir daqui, podemos proceder como no caso mixin, estendendo a classe Parent, implementando a OtherInterface e criando um novo objeto OtherChild para o qual delegaremos as mensagens definidas na OtherInterface.

Class ParentChild extends Parent implements OtherInterface
{	public ParentChild(...) { child = new OtherChild(...); ... }

	public void whatever() { child.whatever(); }

	private final OtherInterface child;
}

Portanto, nessa classe nós juntamos as implementações de duas classes Other, Parent e Other, sem modificar nenhuma das classes. Isso é herança múltipla geral. No Java, precisamos definir e implementar as interfaces, e usar a delegação para um objeto de uma das classes para conseguir isso.

Conclusão

Heranças múltiplas são raramente necessárias. O fato de elas serem um pouco estranhas de atingir no Java não é uma desvantagem, pois isso pode te desencorajar a usá-las nos casos em que soluções melhores estão disponíveis. No entanto, você deve estar ciente de que extensões do Java não são necessárias para atingir o uso da maioria dos
recursos que parecem faltar ao Java. Pelo contrário, o Java é uma linguagem boa e completa, que suporta os tipos de coisas que os desenvolvedores precisam. Só que é preciso habilidade para entender como alcançar alguns dos idiomas menos utilizados, como as heranças múltilpas.

***

Texto original disponível em http://www.csis.pace.edu/~bergin/patterns/multipleinheritance.html