Pourquoi l'héritage et le polymorphisme sont-ils si largement utilisés?

18

Plus j'en apprends sur les différents paradigmes de programmation, tels que la programmation fonctionnelle, plus je commence à remettre en question la sagesse des concepts de POO comme l'héritage et le polymorphisme. J'ai appris pour la première fois l'héritage et le polymorphisme à l'école, et à l'époque le polymorphisme semblait être une merveilleuse façon d'écrire du code générique qui permettait une extensibilité facile.

Mais face au typage du canard (à la fois dynamique et statique) et aux caractéristiques fonctionnelles telles que les fonctions d'ordre supérieur, j'ai commencé à considérer l'héritage et le polymorphisme comme imposant une restriction inutile basée sur un ensemble fragile de relations entre les objets. L'idée générale derrière le polymorphisme est que vous écrivez une fonction une fois, et que vous pouvez ensuite ajouter de nouvelles fonctionnalités à votre programme sans changer la fonction d'origine - tout ce que vous devez faire est de créer une autre classe dérivée qui implémente les méthodes nécessaires.

Mais c'est beaucoup plus simple à réaliser grâce à la frappe de canard, que ce soit dans un langage dynamique comme Python ou un langage statique comme C ++.

À titre d'exemple, considérons la fonction Python suivante, suivie de son équivalent C ++ statique:

def foo(obj):
   obj.doSomething()

template <class Obj>
void foo(Obj& obj)
{
   obj.doSomething();
}

L'équivalent OOP serait quelque chose comme le code Java suivant:

public void foo(DoSomethingable obj)
{
  obj.doSomething();
}

La différence majeure, bien sûr, est que la version Java nécessite la création d'une interface ou d'une hiérarchie d'héritage avant de fonctionner. La version Java implique donc plus de travail et est moins flexible. De plus, je trouve que la plupart des hiérarchies d'héritage du monde réel sont quelque peu instables. Nous avons tous vu les exemples artificiels de formes et d'animaux, mais dans le monde réel, à mesure que les besoins de l'entreprise changent et que de nouvelles fonctionnalités sont ajoutées, il est difficile de faire un travail avant d'avoir vraiment besoin d'étirer la relation "est-un" entre sous-classes, ou bien remodeler / refactoriser votre hiérarchie pour inclure d'autres classes de base ou interfaces afin de répondre aux nouvelles exigences. Avec la saisie de canard, vous n'avez pas à vous soucier de modéliser quoi que ce soit - vous vous inquiétez simplement des fonctionnalités dont vous avez besoin.

Pourtant, l'héritage et le polymorphisme sont si populaires que je doute qu'il serait exagéré de les appeler la stratégie dominante d'extensibilité et de réutilisation du code. Alors, pourquoi l'héritage et le polymorphisme sont-ils si couronnés de succès? Suis-je en train de négliger certains avantages sérieux de l'héritage / polymorphisme par rapport au typage canard?

Channel72
la source
Je ne suis pas un expert en Python, je dois donc demander: que se passerait-il s'il objn'y a pas de doSomethingméthode? Une exception est-elle levée? Il ne se passe rien?
FrustratedWithFormsDesigner
6
@Frustrated: Une exception très claire et spécifique est levée.
dsimcha
11
Le typage de canard n'est-il pas seulement le polymorphisme pris jusqu'à onze? Je suppose que vous n'êtes pas frustré par la POO mais par ses incarnations statiquement typées. Je peux vraiment comprendre cela, mais cela ne fait pas de la POO dans son ensemble une mauvaise idée.
3
Lire d'abord "largement" comme "sauvagement" ...

Réponses:

22

Je suis surtout d'accord avec vous, mais pour le plaisir, je jouerai Devil's Advocate. Les interfaces explicites permettent de rechercher un contrat explicitement et formellement spécifié, vous indiquant ce qu'un type est censé faire. Cela peut être important lorsque vous n'êtes pas le seul développeur d'un projet.

De plus, ces interfaces explicites peuvent être implémentées plus efficacement que le typage canard. Un appel de fonction virtuel a à peine plus de surcharge qu'un appel de fonction normal, sauf qu'il ne peut pas être inséré. Le typage de canard a des frais généraux substantiels. Le typage structurel de style C ++ (à l'aide de modèles) peut générer d'énormes quantités de ballonnement de fichier objet (car chaque instanciation est indépendante au niveau du fichier objet) et ne fonctionne pas lorsque vous avez besoin de polymorphisme au moment de l'exécution, pas de compilation.

Conclusion: je conviens que l'héritage et le polymorphisme de style Java peuvent être un PITA et que les alternatives doivent être utilisées plus souvent, mais elles ont toujours leurs avantages.

dsimcha
la source
29

L'héritage et le polymorphisme sont largement utilisés car ils fonctionnent pour certains types de problèmes de programmation.

Ce n'est pas qu'ils sont largement enseignés dans les écoles, c'est à l'envers: ils sont largement enseignés dans les écoles parce que les gens (alias le marché) ont constaté qu'ils fonctionnaient mieux que les anciens outils, et donc les écoles ont commencé à les enseigner. [Anecdote: lorsque j'ai commencé à apprendre la POO, il était extrêmement difficile de trouver un collège qui enseignait n'importe quelle langue de POO. Dix ans plus tard, il était difficile de trouver un collège qui n'enseignait pas une langue POO.]

Tu as dit:

L'idée générale derrière le polymorphisme est que vous écrivez une fonction une fois, et plus tard vous pouvez ajouter de nouvelles fonctionnalités à votre programme sans changer la fonction d'origine - tout ce que vous devez faire est de créer une autre classe dérivée qui implémente les méthodes nécessaires

Je dis:

Non, ce n'est pas

Ce que vous décrivez n'est pas le polymorphisme, mais l'héritage. Pas étonnant que vous ayez des problèmes de POO! ;-)

Reculez d'une étape: le polymorphisme est un avantage du passage de message; cela signifie simplement que chaque objet est libre de répondre à un message à sa manière.

Alors ... Duck Typing est (ou plutôt, permet) le polymorphisme

L'essentiel de votre question ne semble pas être que vous ne comprenez pas la POO ou que vous ne l'aimez pas, mais que vous n'aimez pas définir des interfaces . C'est très bien, et tant que vous faites attention, tout fonctionnera bien. L'inconvénient est que si vous avez fait une erreur - omis une méthode, par exemple - vous ne le découvrirez qu'au moment de l'exécution.

C'est une chose statique vs dynamique, qui est une discussion aussi ancienne que Lisp, et non limitée à la POO.

Steven A. Lowe
la source
2
+1 pour "le typage du canard est un polymorphisme". Le polymorphisme et l'héritage sont enchaînés inconsciemment depuis trop longtemps, et le typage statique strict est la seule vraie raison pour laquelle ils ont dû l'être.
cHao
+1. Je pense que la distinction statique vs dynamique devrait être plus qu'une note secondaire - elle est au cœur de la distinction entre le typage de canard et le polymorphisme de style java.
Sava B.
8

J'ai commencé à considérer l'héritage et le polymorphisme comme imposant une restriction inutile basée sur un ensemble fragile de relations entre les objets.

Pourquoi?

L'héritage (avec ou sans typage canard) assure la réutilisation d'une caractéristique commune. S'il est courant, vous pouvez vous assurer qu'il est réutilisé de manière cohérente dans les sous-classes.

C'est tout. Il n'y a pas de "restriction inutile". C'est une simplification.

Le polymorphisme, de la même manière, est ce que signifie "typage du canard". Mêmes méthodes. Beaucoup de classes avec une interface identique, mais des implémentations différentes.

Ce n'est pas une restriction. C'est une simplification.

S.Lott
la source
7

L'héritage est abusé, mais il en va de même pour la frappe de canard. Les deux peuvent entraîner des problèmes.

Avec un typage fort, vous obtenez de nombreux "tests unitaires" au moment de la compilation. Avec la frappe de canard, vous devez souvent les écrire.

ElGringoGrande
la source
3
Votre commentaire sur les tests ne s'applique qu'au typage dynamique de canards. La saisie statique de canard de style C ++ (avec des modèles) vous donne des erreurs au moment de la compilation. (Bien que les erreurs de modèle C ++ accordées soient assez difficiles à déchiffrer, mais c'est un problème complètement différent.)
Channel72
2

L'avantage d'apprendre des choses à l'école, c'est que vous les apprenez. Ce qui n'est pas si bon, c'est que vous pouvez les accepter un peu trop dogmatiquement, sans comprendre quand ils sont utiles ou non.

Ensuite, si vous l'avez appris dogmatiquement, vous pourrez plus tard "vous rebeller" tout aussi dogmatiquement dans l'autre sens. Ce n'est pas bon non plus.

Comme pour toutes ces idées, il est préférable d'adopter une approche pragmatique. Développer une compréhension de leur place et de leur échec. Et ignorez toutes les façons dont ils ont été survendus.

Mike Dunlavey
la source
2

Oui, le typage statique et les interfaces sont des restrictions. Mais tout depuis que la programmation structurée a été inventée (c'est-à-dire "goto considéré comme nuisible") a été de nous contraindre. Oncle Bob a une excellente interprétation de cela dans son blog vidéo .

Maintenant, on peut affirmer que la constriction est mauvaise, mais d'un autre côté, elle apporte de l'ordre, du contrôle et de la familiarité à un sujet par ailleurs très complexe.

Assouplir les contraintes en (re) introduisant la frappe dynamique et même l'accès direct à la mémoire est un concept très puissant, mais il peut aussi rendre la tâche de nombreux programmeurs plus difficile à gérer. En particulier, les programmeurs comptaient sur le compilateur et sur la sécurité de type pour une grande partie de leur travail.

Martin Wickman
la source
1

L'héritage est une relation très forte entre deux classes. Je ne pense pas que Java soit plus fort. Par conséquent, vous ne devez l'utiliser que lorsque vous le pensez. L'héritage public est une relation «est-un», et non pas «habituellement est-un». Il est vraiment, vraiment facile de surexploiter l'héritage et de se retrouver avec un gâchis. Dans de nombreux cas, l'héritage est utilisé pour représenter "a-un" ou "prend-fonctionnalité-de-a", et cela est généralement mieux fait par composition.

Le polymorphisme est une conséquence directe de la relation «est-un». Si Derived hérite de Base, alors chaque Derived "est-une" Base, et donc vous pouvez utiliser un Derived partout où vous utiliseriez une Base. Si cela n'a pas de sens dans une hiérarchie d'héritage, alors la hiérarchie est fausse et il y a probablement trop d'héritage en cours.

La saisie de canard est une fonctionnalité intéressante, mais le compilateur ne vous avertira pas si vous en abusez. Si vous ne voulez pas avoir à gérer les exceptions au moment de l'exécution, vous devez vous assurer que vous obtenez les bons résultats à chaque fois. Il peut être plus simple de définir une hiérarchie d'héritage statique.

Je ne suis pas un vrai fan du typage statique (je considère que c'est souvent une forme d'optimisation prématurée), mais cela élimine certaines classes d'erreurs, et beaucoup de gens pensent que ces classes valent bien l'élimination.

Si vous préférez le typage dynamique et le typage canard au typage statique et aux hiérarchies d'héritage définies, c'est bien. Cependant, la méthode Java a ses avantages.

David Thornley
la source
0

J'ai remarqué que plus j'utilise de fermetures C #, moins je fais de POO traditionnel. L'héritage était le seul moyen de partager facilement l'implémentation, donc je pense qu'il était souvent surutilisé et que les limites de la conception étaient trop poussées.

Bien que vous puissiez généralement utiliser les fermetures pour faire la plupart de ce que vous feriez avec l'héritage, cela peut aussi devenir laid.

Fondamentalement, c'est une situation idéale pour l'emploi: la POO traditionnelle peut très bien fonctionner lorsque vous avez un modèle qui lui convient et les fermetures peuvent très bien fonctionner lorsque vous ne le faites pas.

Ben Hughes
la source
-1

La vérité se situe quelque part au milieu. J'aime la façon dont C # 4.0 étant un langage typé statiquement prend en charge la "saisie de canard" par mot-clé "dynamique".

SiberianGuy
la source
-1

L'hérédité, même lorsque la raison du point de vue de la PF, est un excellent concept; non seulement il fait gagner beaucoup de temps, mais il donne un sens à la relation entre certains objets dans votre programme.

public class Animal {
    public virtual string Sound () {
        return "Some Sound";
    }
}

public class Dog : Animal {
    public override string Sound () {
        return "Woof";
    }
}

public class Cat : Animal {
    public override string Sound () {
        return "Mew";
    }
}

public class GoldenRetriever : Dog {

}

Ici, la classe GoldenRetrievera la même chose Soundque Dogpour les remerciements gratuits à l'héritage.

Je vais écrire le même exemple avec mon niveau de Haskell pour que vous puissiez voir la différence

data Animal = Animal | Dog | Cat | GoldenRetriever

sound :: Animal -> String
sound Animal = "Some Sound"
sound Dog = "Woof"
sound Cat = "Mew"
sound GoldenRetriever = "Woof"

Ici, vous n'échappez pas à la spécification soundde GoldenRetriever. Le plus simple serait en général de

sound GoldenRetriever = sound Dog

mais imaginez si vous avez 20 fonctions! S'il y a un expert Haskell, veuillez nous montrer un moyen plus simple.

Cela dit, ce serait génial d'avoir à la fois la correspondance de modèles et l'héritage, où une fonction serait par défaut la classe de base si l'instance actuelle n'a pas l'implémentation.

Cristian Garcia
la source
5
Je le jure, Animalc'est l'anti-tutoriel de la POO.
Telastyn
@Telastyn J'ai appris l'exemple de Derek Banas sur Youtube. Bon enseignant. À mon avis, il est très facile de visualiser et de raisonner. Vous êtes libre de modifier le message avec un meilleur exemple.
Cristian Garcia
cela ne semble rien ajouter de substantiel par rapport aux 10 réponses précédentes
gnat
@gnat Alors que la "substance" est déjà là, une personne nouvelle sur le sujet pourrait trouver un exemple plus facile à saisir.
Cristian Garcia
2
Les animaux et les formes sont de très mauvaises applications du polymorphisme pour plusieurs raisons qui ont été discutées ad nauseum sur ce site. Fondamentalement, dire qu'un chat "est un" animal n'a pas de sens, car il n'y a pas d '"animal" concret. Ce que vous voulez vraiment modéliser, c'est le comportement, pas l'identité. Définir une interface "CanSpeak" qui pourrait être implémentée comme un miaulement ou un aboiement est logique. Définir une interface "HasArea" qu'un carré et un cercle peuvent implémenter est logique, mais pas une forme générique.