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.
O 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