Les interfaces permettent aux langages à typage statique de prendre en charge le polymorphisme. Un puriste orienté objet insisterait sur le fait qu'un langage devrait fournir l'héritage, l'encapsulation, la modularité et le polymorphisme afin d'être un langage orienté objet complet. Dans les langages à typage dynamique - ou canard - (comme Smalltalk,) le polymorphisme est trivial; cependant, dans les langages statiquement typés (comme Java ou C #), le polymorphisme est loin d'être trivial (en fait, à première vue, il semble être en contradiction avec la notion de typage fort.)
Laissez-moi vous démontrer:
Dans un langage typé dynamiquement (ou typé canard) (comme Smalltalk), toutes les variables sont des références à des objets (rien de moins et rien de plus.) Donc, dans Smalltalk, je peux faire ceci:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Ce code:
- Déclare une variable locale appelée anAnimal (notez que nous NE spécifions PAS le TYPE de la variable - toutes les variables sont des références à un objet, ni plus ni moins.)
- Crée une nouvelle instance de la classe nommée "Pig"
- Affecte cette nouvelle instance de Pig à la variable anAnimal.
- Envoie le message
makeNoise
au cochon.
- Répète le tout en utilisant une vache, mais en l'attribuant à la même variable exacte que le cochon.
Le même code Java ressemblerait à ceci (en supposant que Duck et Cow sont des sous-classes d'Animal:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
C'est bien beau, jusqu'à ce que nous introduisions la classe Vegetable. Les légumes ont le même comportement que les animaux, mais pas tous. Par exemple, les animaux et les légumes peuvent pousser, mais il est clair que les légumes ne font pas de bruit et les animaux ne peuvent pas être récoltés.
Dans Smalltalk, nous pouvons écrire ceci:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
Cela fonctionne parfaitement dans Smalltalk car il est typé canard (s'il marche comme un canard et charlatan comme un canard - c'est un canard.) Dans ce cas, lorsqu'un message est envoyé à un objet, une recherche est effectuée sur la liste des méthodes du récepteur, et si une méthode correspondante est trouvée, elle est appelée. Sinon, une sorte d'exception NoSuchMethodError est lancée - mais tout est fait au moment de l'exécution.
Mais en Java, un langage typé statiquement, quel type pouvons-nous attribuer à notre variable? Le maïs a besoin d'hériter de Vegetable, pour soutenir la croissance, mais ne peut pas hériter d'Animal, car il ne fait pas de bruit. La vache doit hériter d'Animal pour soutenir makeNoise, mais ne peut pas hériter de Vegetable car elle ne doit pas mettre en œuvre la récolte. Il semble que nous ayons besoin d' un héritage multiple - la possibilité d'hériter de plus d'une classe. Mais cela s'avère être une fonctionnalité de langage assez difficile à cause de tous les cas extrêmes qui apparaissent (que se passe-t-il lorsque plusieurs superclasses parallèles implémentent la même méthode?, Etc.)
Viennent les interfaces ...
Si nous créons des classes d'animaux et de légumes, à chaque implémentation de Growable, nous pouvons déclarer que notre vache est animale et notre maïs est légume. Nous pouvons également déclarer que les animaux et les légumes sont cultivables. Cela nous permet d'écrire ceci pour tout faire grandir:
List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());
for(Growable g : list) {
g.grow();
}
Et cela nous permet de faire cela, de faire des bruits d'animaux:
List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}
L'avantage du langage de type canard est que vous obtenez un polymorphisme vraiment sympa: tout ce qu'une classe a à faire pour fournir un comportement est de fournir la méthode. Tant que tout le monde joue gentiment et n'envoie que des messages correspondant aux méthodes définies, tout va bien. L'inconvénient est que le type d'erreur ci-dessous n'est pas détecté avant l'exécution:
|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.
Les langages à typage statique fournissent une bien meilleure «programmation par contrat», car ils détecteront les deux types d'erreurs ci-dessous au moment de la compilation:
// 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();
Alors ... pour résumer:
L'implémentation d'interface vous permet de spécifier les types de choses que les objets peuvent faire (interaction) et l'héritage de classe vous permet de spécifier comment les choses doivent être faites (implémentation).
Les interfaces nous offrent de nombreux avantages du "vrai" polymorphisme, sans sacrifier la vérification du type du compilateur.