As interfaces permitem que as linguagens estaticamente digitadas suportem o polimorfismo. Um purista orientado a objetos insistiria que uma linguagem deveria fornecer herança, encapsulamento, modularidade e polimorfismo, a fim de ser uma linguagem orientada a objetos com todos os recursos. Em linguagens de tipo dinâmico - ou tipo pato - (como Smalltalk), o polimorfismo é trivial; no entanto, em linguagens estaticamente tipadas (como Java ou C #), o polimorfismo está longe de ser trivial (na verdade, parece superficialmente estar em desacordo com a noção de digitação forte).
Deixe-me demonstrar:
Em uma linguagem de tipo dinâmico (ou tipo pato) (como Smalltalk), todas as variáveis são referências a objetos (nada menos e nada mais.) Portanto, no Smalltalk, eu posso fazer isso:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Esse código:
- Declara uma variável local chamada anAnimal (observe que NÃO especificamos o TYPE da variável - todas as variáveis são referências a um objeto, nem mais nem menos.)
- Cria uma nova instância da classe chamada "Pig"
- Atribui essa nova instância do Pig à variável anAnimal.
- Envia a mensagem
makeNoise
para o porco.
- Repete a coisa toda usando uma vaca, mas atribuindo-a à mesma variável exata que o porco.
O mesmo código Java se pareceria com isso (assumindo que Duck e Cow são subclasses de Animal:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
Tudo bem, até introduzirmos a classe Vegetable. Os vegetais têm o mesmo comportamento do Animal, mas não todos. Por exemplo, animais e vegetais podem crescer, mas claramente os vegetais não fazem barulho e os animais não podem ser colhidos.
No Smalltalk, podemos escrever o seguinte:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
Isso funciona perfeitamente no Smalltalk porque é do tipo pato (se andar como um pato e grasnar como um pato - é um pato.) Nesse caso, quando uma mensagem é enviada para um objeto, é realizada uma pesquisa no a lista de métodos do receptor e, se um método correspondente for encontrado, ele será chamado. Caso contrário, algum tipo de exceção NoSuchMethodError é lançada - mas tudo é feito em tempo de execução.
Mas em Java, uma linguagem de tipo estaticamente, que tipo podemos atribuir à nossa variável? O milho precisa herdar do vegetal, para sustentar o crescimento, mas não pode herdar do animal, porque não faz barulho. A vaca precisa herdar do Animal para apoiar o makeNoise, mas não pode herdar do Vegetable porque não deve implementar a colheita. Parece que precisamos de herança múltipla - a capacidade de herdar de mais de uma classe. Mas isso acaba sendo um recurso de linguagem bastante difícil, devido a todos os casos extremos que aparecem (o que acontece quando mais de uma superclasse paralela implementa o mesmo método ?, etc.)
Junto vem interfaces ...
Se fizermos as classes Animal e Vegetal, com cada Growable implementando, podemos declarar que nossa Vaca é Animal e nosso Milho é Vegetal. Também podemos declarar que tanto os animais quanto os vegetais são cultiváveis. Isso nos permite escrever isso para crescer tudo:
List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());
for(Growable g : list) {
g.grow();
}
E isso nos permite fazer barulhos de animais:
List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}
A vantagem da linguagem do tipo pato é que você obtém um polimorfismo muito bom: tudo o que uma classe precisa fazer para fornecer comportamento é fornecer o método. Desde que todos se saibam bem e enviem apenas mensagens que correspondam a métodos definidos, tudo ficará bem. A desvantagem é que o tipo de erro abaixo não é detectado até o tempo de execução:
|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.
Linguagens de tipo estático fornecem "programação por contrato" muito melhor, porque elas capturam os dois tipos de erro abaixo em tempo de compilação:
// Compiler error: Corn cannot be cast to Animal.
Animal farmObject = new Corn();
farmObject makeNoise();
-
// Compiler error: Animal doesn't have the harvest message.
Animal farmObject = new Cow();
farmObject.harvest();
Então .... para resumir:
A implementação de interface permite especificar quais tipos de coisas os objetos podem fazer (interação) e a herança de classe permite especificar como as coisas devem ser feitas (implementação).
As interfaces nos oferecem muitos dos benefícios do polimorfismo "verdadeiro", sem sacrificar a verificação do tipo de compilador.