Après vous avoir parlé des langages que je connais (petit moment narcissique inutile), je vais maintenant partager quelques réflexions concernant le modèle objet, et comment il est implémenté dans les langages de programmation.
Les objets, l’héritage et le polymorphisme
La notion la plus importante de la programmation orientée objet, c’est… l’objet (tadaa !), une structure qui encapsule en même temps des données et les traitements qui y sont associés. Très vite, on trouve derrière l’héritage et le polymorphisme.
L’héritage : Un objet peut dériver d’un autre. Il en reprend toutes les caractéristiques, en y ajoutant des attributs (données) et/ou des méthodes (traitements) supplémentaires. La plupart du temps, il est même possible de suppléer une méthode de l’objet parent, en redéfinissant une méthode du même nom.
Prenons un exemple. L’objet Véhicule possède les attributs poids et vitesse, et la méthode avance. Les objets Voiture et Camion héritent de Véhicule, en ajoutant des attributs supplémentaires. Voici un petit diagramme UML :
Si on crée une instance de l’objet Voiture, on aura accès aux attributs poids, vitesse et nombre_passager, et aux méthodes avance et allume_autoradio.
Le polymorphisme : Tous les objets qui dérivent d’un même objet (avec différent sous-types) peuvent être manipulés comme s’ils étaient du type de l’objet parent − tant qu’on n’utilise que leur partie commune.
Si on reprend l’exemple précédent, toutes les instances des objets Voiture et Camion peuvent être manipulées comme si elles étaient du type Véhicule. Si on veut calculer leur poids total, il suffit d’additionner la somme des poids de tous les véhicules, sans avoir besoin de savoir s’il s’agit de voitures ou de camion.
Par contre, seuls les camions peuvent charger une cargaison.
Après l’héritage et le polymorphisme, on peut ajouter l’encapsulation et la notion de visibilité, qui permettent de déterminer les règles d’accès aux attributs et aux méthodes dans les objets et depuis l’extérieur des objets. Bien que ces notions soient très répandues, il existe des langages orientés objet qui ne les supportent pas (Javascript, Python).
Implémentation des types d’héritages
Il existe deux types d’héritage : l’héritage simple et l’héritage multiple.
L’avantage de l’héritage multiple, c’est qu’il permet − comme son nom l’indique − à un objet d’hériter de plusieurs objets à la fois. Parmi les langages qui le supportent, je ne connais vraiment que le C++ et le Perl, sans oublier le Python (que je connais mal) ; il en existe d’autres, qui sont plutôt exotiques (Eiffel, OCaml, …).
La plupart des langages ne supportent que l’héritage simple. Enfin, c’est l’impression que j’ai, je ne peux pas vraiment l’étayer de manière formelle. Mais si on prend l’exemple du Java, du PHP, du Ruby… ça semble être la mode.
Toujours sans la moindre preuve, j’ai tendance à penser que l’héritage multiple est mis de côté pour simplifier le développement des langages. Je m’explique. Dans les années 80, il n’existait pas de compilateur C++. Bjarne Soustrup, son créateur, avait mis à disposition un programme qui traduisait du code C++ en code C, qui pouvait alors être compilé. J’ai été amené, durant mes études, à m’intéresser à ce type de translateur, et c’est très intéressant.
Ce qu’il faut comprendre, c’est qu’il est possible de mettre en œuvre des techniques de programmation orientée objet en C. Cela est notamment utilisé dans les bibliothèques graphiques comme (Motif, LessTif ou GTK+), pour gérer leurs éléments : une fenêtre ou un bouton sera géré simplement comme un widget générique par moments.
Je vais expliquer le principe de ce genre de techniques, sachant que cela peut aller bien plus loin que ce que je vais vous montrer.
Imaginons un code C qui contiennent une structure Véhicule, et la fonction avance :
typedef struct { int poids; short vitesse; } vehicule_t; void avance(vehicule_t *vehicule, short vitesse) { vehicule->vitesse = vitesse; printf("Le véhicule avance à %d km/h", vitesse); }
La fonction avance prend en paramètre un pointeur sur le type correspondant au véhicule, et un seconde paramètre qui indique la vitesse à laquelle le véhicule se déplace. Elle se contente d’affecter la vitesse dans le champ prévu à cet effet dans la structure.
Si maintenant nous voulons créer l’objet Voiture, qui hérite de l’objet Véhicule, c’est très simple. La structure Voiture doit contenir la structure Véhicule :
typedef struct { vehicule_t parent; short nombre_passagers; } voiture_t; void allume_autoradio(voiture_t *voiture) { printf("Il y a %d mélomanes", voiture->nombre_passagers); }
Pourquoi avoir placé la structure « parente » au début de la structure « fille » ? Tout simplement parce qu’ainsi, nous pouvons retrouver l’un ou l’autre à la même adresse en mémoire. En faisant un transtypage, on peut facilement se retrouver à manipuler l’un aussi bien que l’autre.
Voici un exemple de code, dans lequel on alloue la mémoire pour créer une voiture, mais on va ensuite la faire avancer en l’utilisant comme n’importe quel véhicule :
// on alloue la structure en mémoire, en l'initialisant à zéro voiture_t *titine = memset(malloc(sizeof(voiture_t)), 0, sizeof(voiture_t)); // on allume l'auto-radio de la voiture allume_autoradio(titine); // on fait avancer la voiture comme n'importe quel véhicule avance((vehicule_t*)titine, 60);
Voilà. C’est super simple, hein ? La seule chose, c’est qu’il faut faire attention à bien faire la conversion de type à chaque fois qu’on appelle une fonction qui attend un véhicule, ou lorsqu’on veut utiliser les champs qui sont dans la structure du véhicule. Mais quand on programme en C, cela semble naturel.
Par exemple, pour récupérer la vitesse de notre voiture, il faut faire :
((vehicule_t*)titine)->vitesse
Et notre objet Camion, qu’est-ce qu’il devient ?
typedef struct { vehicule_t parent; void* cargaison; } camion_t; void charge_cargaison(camion_t *camion, void *cargaison) { camion->cargaison = cargaison; } // on alloue la structure en mémoire, en l'initialisant à zéro camion_t *mack = memset(malloc(sizeof(camion_t)), 0, sizeof(camion_t)); // on charge une cargaison dans le camion charge_cargaison(mack, "Flash McQueen"); // on fait avancer le camion comme n'importe quel véhicule avance((vehicule_t*)mack, 60);
Pas de problème, on voit bien que les camions et les voitures peuvent être manipulés comme des véhicules.
Surcharge de méthodes
Imaginons maintenant que l’on souhaite pouvoir faire de la surcharge de méthodes. Par exemple, on veut laisser la possibilité aux voitures et aux camions de redéfinir le code qui fait avancer le véhicule. Comment faire ?
En C, la réponse passe par les pointeurs de fonction. Le code devient plus complexe, mais rien qui ne devrait vous effrayer.
Pour le véhicule, on aurait quelque chose comme ça :
// définition de la structure typedef struct vehicule_s { int poids; short vitesse; void (*avance)(struct vehicule_s*, short); } vehicule_t; // fonction avance() void vehicule_avance(vehicule_t *vehicule, short vitesse) { vehicule->vitesse = vitesse; printf("Le véhicule avance à %d km/h", vitesse); } // fonction servant de "constructeur" vehicule_t* vehicule_create() { // Allocation mémoire vehicule_t *vroum = memset(malloc(sizeof(vehicule_t)), 0, sizeof(vehicule_t)); // initialisation du pointeur de fonction vehicule->avance = vehicule_avance; // retour return (vroum); }
Si on veut créer un véhicule et le faire avancer, c’est simple :
vehicule_t *vehicule = vehicule_create(); vehicule->avance(vehicule, 40);
Maintenant, faisons un camion ; lorsqu’on lui dit d’avancer, il va vérifier qu’on ne lui demande pas d’aller trop vite :
// définition de la structure typedef struct { vehicule_t parent; void *cargaison; } camion_t; // fonction de chargement de la cargaison void charge_cargaison(camion_t *camion, void *cargaison) { camion->cargaison = cargaison; } // fonction avance() spécifique aux camions void camion_avance(vehicule_t *vehicule, short vitesse) { if (vitesse > 90) printf("Ce camion ne dépasse pas 90 km/h"); else if (vitesse > 80 && ((camion_t)*vehicule)->cargaison != NULL) printf("Ce camion chargé ne dépasse pas 80 km/h"); else { vehicule->vitesse = vitesse; printf("Le camion avance à %d km/h", vitesse); } } // fonction servant de "constructeur" camion_t *camion_create() { camion_t *camion = memset(malloc(sizeof(camion_t)), 0, sizeof(camion_t)); ((vehicule_t)camion)->avance = camion_avance; return (camion); }
Maintenant, créons un camion, chargeons-lui une cargaison et faisons-le avancer :
camion_t *mack = camion_create(); charge_cargaison(mack, "Flash McQueen"); ((vehicule_t*)mack)->avance((vehicule_t*)mack, 85);
Si vous avez bien suivi, ce code affichera le résultat suivant :
Ce camion chargé ne dépasse pas 80 km/h
Bref, tout cela fonctionne très bien. Je ne dis pas que c’est facile à lire, ni que la syntaxe est super évidente. Mais c’est efficace.
Pour aller plus loin
Si la programmation orientée objet en C vous intéresse, n’hésitez pas à me contacter. À une époque j’étais allé assez loin dans le concept, avec notamment un support des exceptions, et des macros qui simplifiaient énormément de choses.
Je peux aussi vous recommander les liens suivants :
- http://www.bolthole.com/OO-C-programming.html
- http://www.planetpdf.com/codecuts/pdfs/ooc.pdf (PDF de 221 pages)
C’est bien beau, et alors ?
Qu’est-ce qu’on peut retirer de tout ça ? Eh bien, l’héritage simple et le polymorphisme sont des notions franchement pas compliquées à implémenter dans un langage de bas niveau. Je pense que cela fait partie des raisons qui ont conduit à privilégier l’héritage simple au détriment de l’héritage multiple. On m’objectera qu’il s’agit d’un choix philosophique, que les concepteurs de langages ne veulent pas proposer l’héritage multiple ; mais j’en douterais toujours.
Ce qui m’énerve un peu, c’est que s’est rapidement rendu compte que l’héritage simple réduit tellement les possibilités de programmation, que des solutions de contournement ont été mises en place.
Ainsi sont arrivées les interfaces, qui servent à enrichir la définition d’un objet (et de partager ces définitions entre plusieurs objets) sans fournir d’implémentation.
Puis sont arrivés les mixins et les traits, qui fournissent pour leur part des implémentations qui peuvent être intégrées dans des objets.
N’importe quel développeur C++ vous dirait que ces notions sont inutiles à partir du moment où vous savez créer des classes abstraites.
Récemment, je donnais à ce propos deux exemples à un ami.
- En Pascal, il y a une différence explicite entre les fonctions et les procédures. La différence, c’est simplement que les procédures ne retournent rien. En C et dans les langages qui en ont découlé, on n’a toujours que des fonctions, sauf que parfois elles ne retournent rien. C’est simple à comprendre, pas besoin de se retourner le cerveau.
- Autre parallèle : Que je veuille ouvrir mon ordinateur ou démonter un meuble, j’utilise le même tournevis cruciforme. J’ai beau aimer les meubles Ikea (ils sont faciles et rapides à monter), ça m’énerve d’avoir à aller chercher un clé Allen pour les démonter. Le tournevis cruciforme, c’est l’héritage multiple, qui s’adapte à tous les usages. Les meubles Ikea, c’est le Java ou le Ruby ; ça peut accélérer le développement, mais il n’y a − à mon sens − aucune bonne raison d’imposer les clés Allen (les interfaces).
Pour ma part, c’est le cheminement que je trouve bancal : On essaye de justifier la simplification (l’héritage multiple, c’est mal), mais on se retrouve à refaire du pseudo-héritage multiple incomplet.
Mais parce que le nom est différent, ça devient acceptable ? Mixin, c’est plus hype ?
Il existe des techniques pour gérer le problème de l’héritage en diamant (la recherche profonde et la linéarisation C3), plusieurs langages les utilisent déjà, il n’y a donc pas de raison. En plus de ça, si vous avez un objet qui essaye d’implémenter deux interfaces qui possèdent une méthode dont le nom est identique, ça ne marchera pas mieux pour autant. Dans tous les cas, c’est au développeur de faire correctement sa modélisation.
Dans le prochain article, je vais tenter de rassembler mes idées sur ce que j’aimerais avoir comme langage. C’est pas gagné.
Je maintiens que l’héritage multiple est une arme bien trop dangereuse pour être utilisée par n’importe qui dans un langage qui se veut « grand public ». Dans un langage comme C++, et qui de toute manière requiert tout un tas de compétences, c’est différent.
Un ami. 🙂
Bonjour,
Je souhaite apporter une remarque: dans l’OOP, l’élément important, à mon avis, n’est pas l’objet mais la CLASSE (tadaa!).
A partir de la classe, on instancie un objet, mais ce qui est important c’est de bâtir une CLASSE bien faite. L’essentiel du temps du développeur est consacré à concevoir et écrire une CLASSE de qualité.
L’utilisation de l’objet instancié des CLASSES ainsi écrites est un peu comme l’achèvement.
Gros troll détecté :).
Plus sérieusement, je suis assez d’accord avec toi : il y a de la mauvaise fois dans les explications données pour justifier le choix de l’héritage simple.
Maintenant, dans le cas de PHP et pour en avoir discuté avec certain développeur du langage, l’implémentation de l’héritage multiple poserait des problèmes de performance significatif sur l’ensemble du langage.
Et étant donné que son utilisation n’est justifiable que dans des cas très spécifique, ils ont décidé que le jeu n’en valais pas la chandelle, d’autant qu’il existe des alternatives comme les interfaces et maintenant les traits qui n’ont pas cet impact sur les performances.
J’ai usé et abusé de l’héritage multiple en C++, notamment dans le cadre de mes études d’imagerie pour mon plus grand bonheur et j’adorerais parfois en disposer en PHP.
Cependant, le contexte d’utilisation et le principe de fonctionnement de PHP font que même si sa syntaxe est proche de celle du C/C++, il n’est pas possible de faire tout ce que fait C/C++ en PHP du fait que ce soit un langage interprété.
J’en connais qui te dirais que si tu as besoin de l’héritage multiple, c’est que tu n’utilises pas le bon langage pour réaliser ton projet ;).
@ami : Je reste persuadé, pour ma part, que les traits sont une hérésie, et que les interfaces sont inutiles quand on sait écrire une classe abstraite et qu’on peut faire de l’héritage multiple.
@Mihamina : Oui, c’est vrai. Je me demandais si quelqu’un allait relever ! 🙂
Bon, disons que la phrase était plus marrante en ne parlant que des objets. Mais je sais bien que ce qu’on appelle « objet » correspond habituellement à des instances de classes.
@mageekguy : Oui, c’est un peu un troll sur les bords. Mais si tu te souviens bien, on avais déjà échangé quelques emails sur le sujet l’année dernière, lorsque l’implémentation des traits de PHP a été présentée.
Je comprends que l’héritage simple est plus facile à gérer que l’héritage multiple. Mais, en ce qui concerne PHP, je me permets de croire que :
– L’ajout des interfaces et des traits a quand même dû complexifier passablement les choses.
– Si Python est capable de gérer l’héritage multiple, ça veut dire que c’est possible, non ?
Je sais, c’est facile de faire ce genre d’affirmation sans connaître les entrailles de la chose 😉
Oui, moi je vais jusqu’à dire que si tu utilises les traits en PHP, c’est que tu ne devrais pas programmer. Mais c’est de l’humour noir. 😀
mwai, ca marche mais je ne suis pas fan.
Ca fait beaucoup d’écriture, dans le constructeur faut se taper de définir tous les pointeur de fonctions ( si ta classe à 20 méthodes et que tu n’a besoin que d’en modifier 1, voila le bordel quoi).
Bref, ok, mais pas fan. Y a des langages fait pour f
@Loïc : Oui, il y a des langages fait pour ça. Et au final, quand on code en C, on utilise rarement cette technique, à moins d’avoir vraiment besoin des notions de polymorphisme et d’héritage.
Le but de cet article était de pointer du doigt le discours des créateurs de langages « modernes ». Ils disent que l’héritage multiple est la source de tous les problèmes de modélisation objet (souvent résumé par « c’est mal ») ; et la solution serait l’héritage multiple + interfaces/mixins (« c’est bien »).
Ce que j’en dis, c’est qu’au fond ils privilégient l’héritage simple parce que c’est infiniment plus simple à coder (la preuve dans l’article). Si les choses étaient dites clairement, je serais plus compréhensif, mais là ça me gave.
mwai, moi je suis assez d’accord avec « un ami ».
Un design sans héritage multiple est très souvent moins problématique à maintenir à long terme (et dans ma boite, long terme, ça veut dire 15 ans) dans mon expérience.
Mais tu connais mon opinion: je n’aime pas qu’un langage me force a choisir un design plutôt qu’un autre.
En règle générale, j’évite au maximum l’héritage multiple, même s’il est disponible.
Dans certains cas particuliers en revanche, cela simplifie un design, et je l’utilise alors sans remords.
Bref, je préfère un langage supportant l’héritage multiple, même si personnellement, je l’utilise le moins possible.
Le problème soulevé par l' »ami » est largement compensé par la pratique des « code reviews ». Chez nous, chaque décisions de design de ce type est discutée pour être sur qu’elle est justifiée. Overall, cela permet d’harmoniser la qualité du code entre juniors et seniors, même quand on utilise un langage aussi permissif que perl.
(l’ami en question a bossé avec toi et moi il y a quelques années, il fait maintenant un post-doc sur les systèmes concurrent ; souviens-toi de l’homme-sucre 🙂 )
Je pense qu’on est tous un peu d’accord. Ma vision des choses, c’est qu’un langage met à disposition des outils, mais que c’est aux développeurs d’utiliser le bon outil en fonction de chaque situation. L’héritage multiple en fait partie : ce n’est pas parce que c’est là qu’il faut l’utiliser tout le temps ; ce n’est pas parce que ça peut être délicat à gérer qu’il faut l’éliminer.
C’est un peu comme le modèle objet lui même. Imaginons une classe Voiture qui hérite de la classe Véhicule ; pour faire une voiture bleue, on pensera naturellement à ajouter un attribut couleur à la classe Voiture ; on ne va pas créer une classe VoitureBleue qui hériterait de Voiture. C’est le niveau zéro du design logiciel ; eh bien savoir utiliser l’héritage multiple à bon escient, c’est juste le niveau un peu au-dessus.
J’avais deviné qui est ami 🙂
Tafdak sur le reste
Soit dit en passant, après en avoir discuté avec un pote (qui lui, contrairement à moi, est très, très fort en C++), et lui avoir montré le billet, sa réponse a été en gros : « C’est pinailler pour rien. Si Java propose pas l’héritage multiple, ben utilise les interfaces, utiliser la délégation, et puis c’est tout. Si C++ propose l’HM, ben utilise-le quand ça fait sens », etc. Bref, les particularités du langage importent peu : utilise ce que tu peux dedans, et surtout, programme dans le style du langage.
Bien entendu ça veut aussi dire qu’il ne faut pas abuser des bonnes choses. 🙂 Par exemple, quand tu programmes pour faire du code multithreadé, même si le principe est justement de tirer avantage de la mémoire partagée, la première chose que tu apprends à faire, c’est de rendre tes threads presque « purs » et d’éviter le plus possible les sections critiques, pour des raisons de performance bien sûr, mais aussi pour éviter les data races (et donc des plantages ou pire des résultats incorrects). Et du coup, un langage comme Erlang, hautement concurrent, suit le modèle dit de l’acteur, et du coup, même si le runtime lui-même peut parfaitement faire usage de la mémoire partagée comme il l’entend, le programmeur/utilisateur ne voit qu’une interface par passage de messages parce que ça rend la programmation concurrente plus facile à appréhender. Et après avoir passé des heures à essayer de piger pourquoi mon code multithreadé (en C) plantait, tout en sachant que j’avais forcément écrasé la pile du thread voisin, ben j’apprécie les langages qui rendent la prog concurrente et parallèle plus sûre.
Bref, je m’arrête là. 🙂
Merci pour cette contribution, elle m’aide aussi à relativiser 🙂
Oui, c’est vrai, il a raison. Mais (juste pour pinailler encore un peu), c’est un point de vue d’utilisateur des langages, ça. On est des geeks, on a toujours envie de creuser un peu plus loin, non ?
Au sujet de la programmation concurrentielle, ZeroMQ est maintenant là pour aider à implémenter un modèle proche du modèle acteur. Les créateurs de ZeroMQ disent qu’Erlang fait les choses correctement, et que ZeroMQ en démocratise les principes (même si tout le monde n’est pas d’accord).
Mais… je m’éloigne un peu du sujet, là. 🙂