Omission de l'héritage dans les langages de programmation

10

Je développe mon propre langage de programmation. C'est un langage à usage général (pensez Python tapé statiquement pour le bureau, c'est-à-dire int x = 1;) non destiné au cloud.

Pensez-vous qu'il est acceptable de ne pas autoriser l'héritage ou les mixins? (étant donné que l'utilisateur aurait au moins des interfaces)

Par exemple: Google Go, un langage système qui a choqué la communauté de programmation en ne permettant pas l'héritage.

Christophe
la source

Réponses:

4

En fait, nous constatons de plus en plus que l'utilisation de l'héritage est sous-optimale car (entre autres) elle conduit à un couplage étroit.

L'héritage d'implémentation peut toujours être remplacé par l'héritage d'interface plus la composition, et les conceptions logicielles plus modernes ont tendance à utiliser de moins en moins l'héritage, en faveur de la composition.

Alors oui , ne pas fournir d'héritage en particulier est une décision de conception tout à fait valable et très en vogue .

Mixins, d'autre part, ne sont même pas (encore) une fonctionnalité de langage grand public, et les langues qui ne fournissent une fonctionnalité appelée « mixin » comprennent souvent des choses très différentes par elle. N'hésitez pas à le fournir ou non. Personnellement, je le trouve très utile, mais sa mise en œuvre correcte peut être très difficile.

Konrad Rudolph
la source
13

Raisonner si l'héritage (ou n'importe quelle caractéristique unique, vraiment) est nécessaire ou non sans considérer également le reste de la sémantique du langage est inutile; vous vous disputez dans le vide.

Ce dont vous avez besoin, c'est d'une philosophie de conception de langage cohérente; la langue doit pouvoir résoudre avec élégance les problèmes pour lesquels elle est conçue. Le modèle pour y parvenir peut ou non nécessiter un héritage, mais il est difficile de juger cela sans une vue d'ensemble.

Si, par exemple, votre langage a des fonctions de première classe, une application de fonction partielle, des types de données polymorphes, des variables de type et des types génériques, vous avez à peu près couvert les mêmes bases que vous auriez avec l'héritage OOP classique, mais en utilisant un paradigme différent.

Si vous avez une liaison tardive, un typage dynamique, des méthodes en tant que propriétés, des arguments de fonction flexibles et des fonctions de première classe, vous couvrirez également les mêmes motifs, mais encore une fois, en utilisant un paradigme différent.

(Trouver des exemples pour les deux paradigmes décrits est laissé comme un exercice pour le lecteur.)

Alors, pensez au type de sémantique que vous voulez, jouez avec eux et voyez si elles sont suffisantes sans héritage. Si ce n'est pas le cas, vous pouvez soit décider de jeter l'héritage dans le mix, soit décider qu'il manque quelque chose d'autre.

tdammers
la source
9

Oui.

Je pense que ne pas autoriser l'héritage est très bien, surtout si votre langue est typée dynamiquement. Vous pouvez obtenir une réutilisation de code similaire par exemple par délégation ou composition. Par exemple, j'ai écrit des programmes modérément compliqués - d'accord, pas si compliqués:) - en JavaScript sans aucun héritage. J'ai essentiellement utilisé des objets comme structures de données algébriques avec certaines fonctions attachées comme méthodes. J'avais aussi un tas de fonctions qui n'étaient pas des méthodes qui opéraient sur ces objets.

Si vous avez un typage dynamique - et je suppose que c'est le cas - vous pouvez également avoir un polymorphisme sans héritage. De plus, si vous autorisez l'ajout de méthodes arbitraires aux objets lors de l'exécution, vous n'aurez pas vraiment besoin de choses comme les mixins.

Une autre option - qui je pense est une bonne option - consiste à émuler JavaScript et à utiliser des prototypes. Celles-ci sont plus simples que les classes mais également très flexibles. Juste quelque chose à considérer.

Donc, tout compte fait, je me lancerais.

Tikhon Jelvis
la source
2
Tant qu'il existe un moyen raisonnable d'accomplir les types de tâches généralement gérées par héritage, allez-y. Vous pouvez essayer quelques cas d'utilisation, pour vous en assurer (peut-être même aller jusqu'à solliciter des exemples de problèmes à d'autres, pour éviter l'auto-biais ...)
comingstorm
Non C'est typiquement et fortement typé, je le mentionnerai en haut.
Christopher
Comme il est typé statiquement, vous ne pouvez pas simplement ajouter des méthodes aléatoires aux objets lors de l'exécution. Cependant, vous pouvez toujours faire du typage de canard: consultez les protocoles Gosu pour un exemple. J'ai utilisé un peu les protocoles au cours de l'été et ils étaient vraiment utiles. Gosu a également des "améliorations" qui sont un moyen d'ajouter des méthodes aux classes après coup. Vous pourriez également envisager d'ajouter quelque chose comme ça.
Tikhon Jelvis
2
S'il vous plaît, s'il vous plaît, très bien, arrêtez de relier l'héritage et la réutilisation du code. On sait depuis longtemps que l'héritage est vraiment un mauvais outil pour la réutilisation de code.
Jan Hudec
3
L'héritage n'est qu'un outil pour la réutilisation de code. C'est souvent un très bon outil, et d'autres fois, c'est horrible. Préférer la composition à l'héritage ne signifie pas ne jamais utiliser l'héritage du tout.
Michael K
8

Oui, c'est une décision de conception parfaitement raisonnable d'omettre l'héritage.

Il existe en fait de très bonnes raisons de supprimer l'héritage de l'implémentation, car cela peut produire du code extrêmement complexe et difficile à maintenir. J'irais même jusqu'à considérer l'héritage (car il est généralement implémenté dans la plupart des langages POO) comme une erreur.

Clojure, par exemple, ne fournit pas d'héritage d'implémentation, préférant fournir un ensemble de fonctionnalités orthogonales (protocoles, données, fonctions, macros) qui peuvent être utilisées pour obtenir les mêmes résultats mais de manière beaucoup plus propre.

Voici une vidéo que j'ai trouvée très éclairante sur ce sujet général, où Rich Hickey identifie les sources fondamentales de complexité dans les langages de programmation (y compris l'héritage) et présente des alternatives pour chacun: Simple made easy

mikera
la source
Dans de nombreux cas, l'héritage d'interface est plus approprié. Mais s'il est utilisé avec modération, l'héritage d'implémentation peut permettre la suppression de beaucoup de code passe-partout, et est la solution la plus élégante. Cela nécessite simplement des programmeurs plus intelligents et plus prudents que le singe Python / JavaScript moyen.
Erik Alapää
4

Lorsque j'ai rencontré pour la première fois le fait que les classes VB6 ne prennent pas en charge l'héritage (uniquement les interfaces), cela m'a vraiment énervé (et le fait toujours).

Cependant, la raison pour laquelle c'est si mauvais, c'est qu'il n'avait pas non plus de paramètres constructeur, donc vous ne pouviez pas faire d'injection de dépendance normale (DI). Si vous avez une DI, c'est plus important que l'héritage, car vous pouvez suivre le principe de privilégier la composition à l'héritage. C'est de toute façon une meilleure façon de réutiliser le code.

Vous n'avez pas de Mixins? Si vous souhaitez implémenter une interface et déléguer tout le travail de cette interface à un ensemble d'objets via l'injection de dépendances, alors les mixins sont idéaux. Sinon, vous devez écrire tout le code passe-partout pour déléguer chaque méthode et / ou propriété à l'objet enfant. Je le fais beaucoup (grâce à C #) et c'est une chose que j'aurais aimé ne pas avoir à faire.

Scott Whitlock
la source
Oui, ce qui a suscité cette question, c'est l'implémentation du DOM SVG où la réutilisation du code est très bénéfique.
Christopher
2

Pour être en désaccord avec une autre réponse: non, vous jetez des fonctionnalités lorsqu'elles sont incompatibles avec quelque chose que vous voulez plus. Java (et d'autres langages GC'd) ont supprimé la gestion explicite de la mémoire car il souhaitait davantage la sécurité des types. Haskell a rejeté la mutation parce qu'il voulait davantage de raisonnement équationnel et de types fantaisistes. Même C a supprimé (ou déclaré illégal) certains types d'alias et d'autres comportements, car il souhaitait davantage d'optimisations du compilateur.

La question est donc la suivante: que voulez-vous de plus que l'héritage?

Ryan Culpepper
la source
1
Seule la gestion explicite de la mémoire et la sécurité des types sont totalement indépendantes et certainement pas incompatibles. Il en va de même pour la mutation et les «types fantaisie». Je suis d'accord avec le raisonnement général mais vos exemples sont plutôt mal choisis.
Konrad Rudolph
@KonradRudolph, la gestion de la mémoire et la sécurité des types sont étroitement liés. Une opération free/ deletevous donne la possibilité d'invalider les références; à moins que votre système de saisie ne soit capable de suivre toutes les références affectées, cela rend la langue dangereuse. En particulier, ni C ni C ++ ne sont de type sécurisé. Il est vrai que vous pouvez faire des compromis dans l'un ou l'autre (par exemple, les types linéaires ou les restrictions d'allocation) pour les faire accepter. Pour être précis, j'aurais dû dire que Java voulait une sécurité de type avec un système de type simple et particulier et une allocation sans restriction davantage.
Ryan Culpepper
Eh bien, cela dépend plutôt de la façon dont vous définissez la sécurité de type. Par exemple, si vous prenez la définition (tout à fait raisonnable) de la sécurité des types au moment de la compilation et que vous souhaitez que l'intégrité des références fasse partie de votre système de types, Java n'est pas non plus sûr pour les types (car il autorise les nullréférences). L'interdiction freeest une restriction supplémentaire assez arbitraire. En résumé, la sécurité d'une langue dépend plus de votre définition de la sécurité des types que de la langue.
Konrad Rudolph
1
@Ryan: Les pointeurs sont parfaitement sécurisés. Ils se comportent toujours selon leur définition. Ce n'est peut-être pas comme vous les aimez, mais c'est toujours selon la façon dont ils sont définis. Vous essayez de les étirer pour qu'ils ne soient pas quelque chose. Les pointeurs intelligents peuvent garantir la sécurité de la mémoire de manière assez triviale en C ++.
DeadMG
@KonradRudolph: En Java, chaque Tréférence se réfère à l'un nullou à un objet d'une classe s'étendant T; nullest moche, mais les opérations sur nulllever une exception bien définie plutôt que de corrompre le type invariant ci-dessus. Contrairement à C ++: après un appel à delete, un T*pointeur peut pointer vers la mémoire qui ne contient plus d' Tobjet. Pire encore, si vous effectuez une affectation de champ à l'aide de ce pointeur dans une affectation, vous pouvez mettre à jour entièrement un champ d'un objet d'une classe différente s'il venait à être placé à une adresse proche. Ce n'est pas la sécurité de type par une définition utile du terme.
Ryan Culpepper
1

Non.

Si vous souhaitez supprimer une fonctionnalité du langage de base, vous déclarez universellement qu'elle n'est jamais, jamais nécessaire (ou inutilement diffficile à mettre en œuvre, ce qui ne s'applique pas ici). Et «jamais» est un mot fort en génie logiciel. Vous auriez besoin d'une justification très puissante pour faire une telle déclaration.

DeadMG
la source
8
Ce n'est pas vrai: la conception de Java consistait à supprimer des fonctionnalités telles que la surcharge de l'opérateur, l'héritage multiple et la gestion manuelle de la mémoire. De plus, je pense que traiter les fonctionnalités du langage comme "sacrées" est mauvais; au lieu de justifier la suppression d' une fonctionnalité, vous devez justifier son ajout - je ne pense pas aux fonctionnalités que chaque langue doit avoir.
Tikhon Jelvis
6
@TikhonJelvis: C'est pourquoi Java est un langage terrible , et son manque de tout sauf "UTILISER LE PATRIMOINE COLLECTE DES DÉCHETS" est la raison numéro un . Les fonctionnalités linguistiques doivent être justifiées, je suis d'accord, mais celle-ci est une fonctionnalité de langue de base - elle a de nombreuses applications utiles et ne peut pas être reproduite par le programmeur sans violer DRY.
DeadMG
1
@DeadMG wow! Langage assez fort.
Christopher
@DeadMG: J'ai commencé à écrire une réfutation en tant que commentaire mais je l'ai ensuite transformée en réponse (qv).
Ryan Culpepper
1
"La perfection n'est pas atteinte quand il n'y a plus rien à ajouter mais quand il n'y a plus rien à supprimer" (q)
9000
1

Parlant d'une perspective strictement C ++, l'héritage est utile à deux fins principales:

  1. Il permet la réutilisation du code
  2. En combinaison avec le remplacement dans une classe enfant, il vous permet d'utiliser un pointeur de classe de base pour gérer un objet de classe enfant sans connaître son type.

Pour le point 1, tant que vous avez un moyen de partager du code sans avoir à vous livrer à trop d'acrobaties, vous pouvez supprimer l'héritage. Pour le point 2. Vous pouvez suivre la voie Java et insister sur une interface pour implémenter cette fonctionnalité.

Les avantages de la suppression de l'héritage sont

  1. empêcher les longues hiérarchies et les problèmes associés. Votre mécanisme de partage de code devrait être suffisant pour cela.
  2. Évitez que les changements dans les classes parentes interrompent les classes enfants.

Le compromis est en grande partie entre «Ne vous répétez pas» et la flexibilité d'un côté et la prévention des problèmes de l'autre côté. Personnellement, je détesterais voir l'héritage passer de C ++ simplement parce qu'un autre développeur peut ne pas être assez intelligent pour prévoir les problèmes.

DPD
la source
1

Pensez-vous qu'il est acceptable de ne pas autoriser l'héritage ou les mixins? (étant donné que l'utilisateur aurait au moins des interfaces)

Vous pouvez faire beaucoup de travaux très utiles sans héritage d' implémentation ou mixins, mais je me demande si vous devriez avoir une sorte d'héritage d'interface, c'est-à-dire une déclaration qui dit que si un objet implémente l'interface A, il doit également interfacer B (c'est-à-dire, A est une spécialisation de B et il existe donc une relation de type). D'un autre côté, votre objet résultant n'a besoin que d'enregistrer qu'il implémente les deux interfaces, il n'y a donc pas trop de complexité. Tout est parfaitement réalisable.

Le manque d'héritage d'implémentation a cependant un inconvénient clair: vous ne pourrez pas construire de tables vtables indexées numériquement pour vos classes et devrez donc faire des recherches de hachage pour chaque appel de méthode (ou trouver un moyen intelligent de les éviter). Cela pourrait être pénible si vous routez même des valeurs fondamentales (par exemple, des nombres) via ce mécanisme. Même une très bonne implémentation de hachage peut être coûteuse lorsque vous la frappez plusieurs fois dans chaque boucle interne!

Associés Donal
la source