Le interfacce consentono linguaggi tipizzati staticamente per supportare il polimorfismo. Un purista orientato agli oggetti insisterebbe sul fatto che un linguaggio dovrebbe fornire eredità, incapsulamento, modularità e polimorfismo al fine di essere un linguaggio orientato agli oggetti completo. Nelle lingue tipizzate in modo dinamico - o in quelle anatre - (come Smalltalk,) il polimorfismo è banale; tuttavia, nei linguaggi tipicamente statici (come Java o C #), il polimorfismo è tutt'altro che banale (in effetti, in superficie sembra essere in contrasto con la nozione di tipizzazione forte.)
Lasciami dimostrare:
In un linguaggio di tipo dinamico (o di tipo duck) (come Smalltalk), tutte le variabili sono riferimenti ad oggetti (niente di meno e niente di più.) Quindi, in Smalltalk, posso fare questo:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Quel codice:
- Dichiara una variabile locale chiamata anAnimal (nota che NON specifichiamo il TIPO della variabile - tutte le variabili sono riferimenti a un oggetto, né più né meno).
- Crea una nuova istanza della classe denominata "Pig"
- Assegna quella nuova istanza di Pig alla variabile anAnimal.
- Invia il messaggio
makeNoise
al maiale.
- Ripete il tutto usando una mucca, ma assegnandola alla stessa variabile esatta del Maiale.
Lo stesso codice Java sarebbe simile a questo (ipotizzando che Duck e Cow siano sottoclassi di Animal:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
Va tutto bene, fino a quando non introduciamo la classe Vegetable. Le verdure hanno lo stesso comportamento degli animali, ma non tutte. Ad esempio, sia animali che vegetali potrebbero essere in grado di crescere, ma chiaramente le verdure non fanno rumore e gli animali non possono essere raccolti.
In Smalltalk, possiamo scrivere questo:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
Funziona perfettamente in Smalltalk perché è tipizzato come un'anatra (se cammina come un'anatra e ciondola come un'anatra - è un'anatra.) In questo caso, quando un messaggio viene inviato a un oggetto, viene eseguita una ricerca su l'elenco dei metodi del destinatario e, se viene trovato un metodo corrispondente, viene chiamato. In caso contrario, viene generata una sorta di eccezione NoSuchMethodError, ma viene eseguita al momento dell'esecuzione.
Ma in Java, un linguaggio tipicamente statico, che tipo possiamo assegnare alla nostra variabile? Il mais ha bisogno di ereditare dalla verdura, per sostenere la crescita, ma non può ereditare dall'animale, perché non fa rumore. La mucca deve ereditare dall'animale per supportare makeNoise, ma non può ereditare da Vegetable perché non dovrebbe implementare il raccolto. Sembra che abbiamo bisogno dell'ereditarietà multipla : la capacità di ereditare da più di una classe. Ma questa risulta essere una caratteristica del linguaggio piuttosto difficile a causa di tutti i casi limite che compaiono (cosa succede quando più di una superclasse parallela implementa lo stesso metodo ?, ecc.)
Arrivano le interfacce ...
Se realizziamo classi di animali e verdure, con ciascuna di esse coltivabile, possiamo dichiarare che la nostra mucca è animale e il nostro mais è vegetale. Possiamo anche dichiarare che sia animali che vegetali sono coltivabili. Questo ci permette di scrivere questo per far crescere tutto:
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 ci permette di fare questo, per fare rumori di animali:
List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}
Il vantaggio del linguaggio tipografico è che si ottiene un polimorfismo davvero gradevole: tutto ciò che una classe deve fare per fornire un comportamento è fornire il metodo. Finché tutti giocano bene e inviano solo messaggi che corrispondono a metodi definiti, tutto va bene. Lo svantaggio è che il tipo di errore riportato di seguito non viene rilevato fino al runtime:
|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.
I linguaggi tipizzati staticamente forniscono una "programmazione per contratto" molto migliore perché colpiranno i due seguenti tipi di errore al momento della compilazione:
// 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();
Quindi .... per riassumere:
L'implementazione dell'interfaccia consente di specificare quali tipi di cose possono fare gli oggetti (interazione) e l'ereditarietà delle classi consente di specificare come le cose dovrebbero essere fatte (implementazione).
Le interfacce ci offrono molti dei vantaggi del polimorfismo "vero", senza sacrificare il controllo del tipo di compilatore.