Licence CC BY

Un point sur la programmation objet (POO)

La POO, ses problèmes, et qu’en faire

Comme le sujet revient comme un marronnier, voici un billet sur la programmation orientée objet et pourquoi elle est mal utilisée et enseignée.

Ce billet repose principalement sur ces discussions du forum : [1] [2]. D’autre part, il présente une version pragmatique de la POO et repose surtout sur la pratique ; il peut donc parfois s’éloigner des théories développées dans le cadre de l’étude formelle des langages de programmation.

Il ne représente que ma vision de la chose et n’engage que moi ; vous êtes les bienvenus pour en discuter – avec respect et cordialité – en commentaires.

À propos de la programmation orientée objet en elle-même

Qu’est-ce que la programmation orientée objet ?

La programmation orientée objet – ou POO – est un paradigme de programmation informatique qui organise le code en un assemblage de briques appelées objets (merci Captain Obvious).

Ces objets ont un état interne et un comportement, qui leur permet d’interagir entre eux.

Et… c’est tout.

Si le principe d’encapsulation est correctement respecté (ce qui devrait toujours être le cas en théorie mais ne l’est pas en pratique), l’état interne d’un objet est inaccessible de l’extérieur, et ne peut pas être manipulé directement.

La conséquence immédiate de tout ça, c’est que la phase de conception est primordiale, parce que c’est la définition correcte des objets et de leurs interactions qui va permettre de faire fonctionner le programme.

Qu’est-ce que n’est pas la programmation orientée objet ?

Beaucoup d’éléments de langages de programmation sont souvent considérés comme « indispensables à la POO » alors qu’en fait pas du tout. Comme les classes ou les prototypes, le typage statique et/ou fort, les interfaces, etc.

La programmation orientée objet n’est pas non plus – et là je vais avoir tous les théoriciens du langage sur le dos – une application stricte des principes SOLID. C’est un simple problème de pragmatisme : ces règles sont simplement des indications qu’il est préférable de suivre, pas des lois absolues – et donc parfois il est plus simple de ne pas les suivre.

Cela dit, suivez SOLID autant que possible, ça ne fera pas de mal.

À quoi sert la programmation orientée objet ?

D’abord à regrouper le code en unités cohérentes, ce qui passe par une encapsulation correcte et une définition claire des interfaces.

Ensuite à permettre le découplage entre le fonctionnement interne d’un objet et son comportement : si on respecte les contrats de comportements, on doit pouvoir changer tout le fonctionnement interne de l’objet sans conséquence fonctionnelle.

À quoi ne sert pas la programmation orientée objet ?

La programmation orientée objet permet la réutilisation « simple » du code (via la réutilisation des objets), mais ça n’est pas du tout une caractéristique propre à ce paradigme. On réutilisait du code bien avant la POO.

Ça n’est pas non plus un paradigme orienté performances : le découplage et la cohérence du code (et donc sa maintenabilité) sont prioritaires. Ce point fait que la POO n’est pas très utilisée dans les domaines qui ont un besoin massif de performances, comme le jeu vidéo ou le calcul scientifique.

Et donc, c’est quoi le problème ?

Au fait, « programmation orientée objet » c’est très long, donc à partir de maintenant je vais surtout utiliser « POO ». D’accord ? Oui ? Merci !

« Quand tu as un marteau, tout ressemble à un clou »

La POO devrait être utilisée (et devrait n’être utilisée que) lorsqu’on a un problème qui peut se traduire en objets qui interagissent entre eux.

Mais comme dit le proverbe, « Quand tu as un marteau, tout ressemble à un clou », et donc la tentation est grande de créer des objets très artificiels, qui n’ont à peu près aucun sens, juste pour « faire de la POO » (quelle qu’en soit la raison à l’origine).

Alors qu’en fait il faudrait limiter l’usage de la POO aux parties du code pour lesquelles c’est pertinent, et utiliser d’autres paradigmes pour gérer le reste. Le problème, c’est que ça n’est pas toujours facile parce que…

Un enseignement complètement à la rue

L’immense majorité des cours de POO n’expliquent pas ce qu’est la POO : ils expliquent une vision défectueuse de la POO. Généralement en lui prêtant des buts et des moyens qui ne sont pas les siens, ce qui conduit à une mécompréhension générale de l’outil.

Le pire étant les exemples donnés, qui sont très souvent :

  • Mauvais (entre autres parce qu’ils ne définissent généralement que l’héritage et pas les autres formes d’interaction comme la composition)
  • Impossibles à mettre en pratique dans un vrai programme, ce qui n’aide pas à comprendre. Sérieusement, qui a besoin de représenter « un chat » ou « une voiture » sous cette forme dans un programme ?
  • Le simple fait de prétendre que tout problème peut être résolu par la POO. Je ne compte même plus les exercices où on exige de l’élève qu’il utilise la POO pour résoudre un problème où ça n’a aucun sens.

Par pitié, arrêtez de donner des exemples à base d’animaux ou de véhicules quand vous expliquez la POO ! Ils sont totalement absurdes et se contentent d’embrouiller le monde ! Même les exemples à base de figures géométriques sont douteux, parce qu’ils conduisent généralement à une violation du principe de substitution de Liskov – un cas qui sera discuté plus loin.

Des langages qui font n’importe quoi

On a un looong historique de langages « orientés POO » qui ont fait n’importe quoi avec. Soit comme PHP < 5.3 qui prétentait permettre d’en faire alors que non, soit comme Java (surtout < 8) qui forçait en pratique à en faire là où ça n’était pas utile.

Si je prends l’exemple de Java que je connais le mieux (vu que je bosse avec depuis près de 15 ans), on peut noter en vrac :

  • Tous les types natifs qui ne sont pas des objets, mais qui ont des autoboxing / autounboxing vers des classes équivalentes pour pouvoir les considérer comme tels (avec tous les pièges et comportements bizarres que ça peut entrainer).
  • L’obligation de déclarer une classe pour tout et n’importe quoi, y compris ce qui n’est de doute évidence pas orienté objet (coucou la fonction main1). Des objets planqués sous le tapis (dans les Enum par exemple).
  • Des chaines d’héritage qui n’ont aucun sens et qui conduisent à des comportements aberrants2.

Et là je parle bien du langage lui-même, et pas de l’utilisation qui en est faite.

Et puisqu’on en parle…

Le cas particulier de l’héritage

Le concept d’héritage est sans doute le plus polémique, mal compris et mal utilisé de la POO. Et pourtant il est utilisé en masse.

Le principal problème de l’héritage, dans la vie de tous les jours, c’est qu’il conduit très vite à un code incompréhensible et difficile à maintenir, où pour comprendre le comportement d’un objet (et je rappelle que c’est l’un des principes de base de la POO !) on doit remonter une pile complète d’héritage, en vérifiant à chaque étape si la méthode du comportement qui nous intéresse n’est pas redéfinie.

Un système d’héritage mal conçu peut aussi engendrer des effets de bords indésirables, avec une modification interne d’un objet parent qui casse le comportement des objets fils.

Si l’héritage est mal utilisé, c’est à la fois par mimétisme (beaucoup de langages ou frameworks orientés objets ont des chaines d’héritage à rallonge dans leurs API), par mauvais enseignement et par mauvaise compréhension. De plus, l’héritage peut être performant par rapport à d’autres techniques comme la composition3.

En fait, on a tendance, quand on utilise la POO, à utiliser l’héritage dès qu’on a du code – ou pire : des données – à factoriser (hop, le code commun part dans un objet parent) sans que ça soit pertinent. Il y a d’autres techniques, comme la composition ou les traits qui répondent souvent mieux au besoin4.

Si l’héritage s’avère être une solution pertinent, la solution devrait respecter le principe de substitution de Liskov, dit « LSP » (c’est le L du principe SOLID mentionné plus haut). Et je dis bien « devrais » et pas « doit », parce que c’est une règle informelle5 et pas une règle absolue. En particulier, elle part d’un principe qui s’avère assez faux en pratique dans les programmes réels : celui de la substituabilité. En pratique, l’usage des objets fils n’est pas totalement substituable aux objets parents et ne cherche pas à l’être, les objets fils sont plutôt utilisés comme « des objets parents avec des propriétés en plus », des objets parents qui sont aussi des objets fils. Le cas le plus commun de violation du LSP, c’est les objets parents abstraits, sans implémentation.

En résumé : l’héritage ne devrait être utilisé surtout pour modifier des comportements. On peut s’en servir pour ajouter des comportements, à condition d’être à l’aise avec le fait que ça va provoquer des choses bizarres du point de vue du LSP.

Cela dit, comme le principe SOLID en général : vous avez tout à gagner à respecter au maximum le LSP.

Des pratiques habituelles complètement cassées

Tout ça conduit à des pratiques habituelles considérées comme « de la POO » (voire imposées en entreprise) qui n’ont en fait rien à voir avec la POO, et qui sont totalement contre-productives. En vrac :

  • Cette manie de mettre des getters et setters partout. Ça expose l’état interne de l’objet et casse complètement la notion d’encapsulation donc d’état interne (donc non exposé à l’extérieur).
  • Tout faire par héritage. Il y a des cas où ça n’a juste pas de sens, la composition c’est bien aussi.
  • Les piles de instanceof pour déterminer le comportement à appliquer en fonction du type de l’objet. Ça déporte la logique dans le code appelant, au lieu de laisser les objets réagir correctement à leur manipulation. Accessoirement, c’est un signe que le LSP est cassé sur l’objet en question.
  • Le classique « il doit impérativement y avoir des interfaces » (et ses petits frères, les Factory etc). Là c’est plus qu’on se rajoute du travail inutile sous de faux prétextes.

  1. En instance de disparition avec la JEP 445 qui devrait arriver avec Java 21.
  2. Par exemple, l’interface List expose des méthodes de modification, qui sont donc présentes y compris sur les listes non modifiables, qui ont donc des méthodes qui lancent des exceptions quand on les appelle.
  3. Je n’ai plus les chiffres exacts sous la main, mais le temps d’accès à une méthode, même avec du polymorphisme, est presque constant dans les langages à JVM (Java, Kotlin…) quelle que soit la profondeur d’héritage, même déraisonnable (plusieurs centaines de niveaux). La même structure représentée avec de la composition a un temps d’accès grossièrement linéaire avec la profondeur de composition. Ça peut être différent avec d’autres langages, à vérifier.
  4. L’héritage privé peut être aussi une bonne solution, sous certaines conditions – la première est que la possibilité existe dans le langage utilisé…
  5. D’après Barbara Liskov elle-même.

Mais alors, qu’est-ce qu’on fait ?

L’idée, c’est d’utiliser un outil adapté à chaque problème, et donc plusieurs paradigmes au sein d’un même programme. La plupart des langages modernes sont d’ailleurs multiparadigmes et assez souples là-dessus. Idem avec les évolutions modernes de langages anciens, comme Java qui a ajouté une bonne dose de fonctionnel depuis Java 8 – et dans les cas où c’est utile, comme les traitements de séries d’objets, c’est très appréciable.

Il y a trois inconvénients à ça :

  1. Il faut maitriser plusieurs paradigmes différents en même temps et être capable de changer facilement selon le contexte, ce qui n’est pas facile.
  2. On voit arriver de nouveaux problèmes. Comme cette mode qui veut que l’on ne crée plus que des objets immuables, ce qui est très pratique pour les traitements fonctionnels et peut sensiblement améliorer les performances… mais qui est contraire à la définition d’ « objet » dans la POO. Et donc appeler ça des « objets » devient abusif, casse les parties de code où ils sont utilisés dans le paradigme de POO, et engendre encore plus de confusion1.
  3. Il faut être capable d’identifier le paradigme le plus efficace en fonction du contexte. Et ce même quand ce contexte est mensonger, comme quand un langage se déclare « orienté objet » mais permet de faire aussi du procédural, du fonctionnel, de l’impératif2 et j’en passe… ou quand un exercice parle beaucoup de POO alors que c’est sans doute l’un des pires paradigmes pour le réaliser.

D’autre part, lorsque vous utilisez la programmation orientée objet (parce que c’est une solution adaptée au problème en cours, donc), revenez toujours aux fondamentaux. Pensez en termes d’état interne et de comportement, et fuyez tout autre vocabulaire (en particulier : champ, attribut, propriété qui sont de bons candidats pour conduire à une conception cassée).


  1. Notez que l’auteur de ces lignes est néanmoins partisan de l’usage d’« objets immuables » dès que possible. Parce que c’est quand même vachement pratique !
  2. Je pars ici du principe que vous utilisez ici un langage orienté objet mainstream, qui sont tous procéduraux et impératifs. La situation pourrait être différente avec des langages exotiques.

Voilà, c’était ma vision de la programmation orientée objet à à fin juin 2023. J’aurais pu rajouter plein de détails mais ce billet est déjà beaucoup trop long.

Merci à @lmghs, @pierre_24, @gbdivers, @entwanne et tous les gens que j’oublie pour leurs réflexions pertinentes qui ont nourri la mienne.

Je vous laisse pinailler dans les commentaires !

17 commentaires

Cool! Je te rejoins sur bien des points. Bon j’ai quelques petits pinaillages après ^^'

A quoi ça sert: D’abord à regrouper le code en unités cohérentes, ce qui passe par une encapsulation correcte et une définition claire des interfaces.

Je suis d’accord. Petite parenthèse, ce n’est pas une spécificité OO, on avait ça avec les types abstraits de données à ma connaissance.

Le gros intérêt limite spécifique lorgnera plus du coté de : dans une commonality, j’ai des points de variations dont la résolution pourra être dynamique. En tout cas le polymorphisme d’inclusion/héritage sera le seul qui soit dynamique dans les langages tels que C++, Java… En Python, le polymorphisme paramétrique (~ Duck Typing) est aussi dynamique.

Le cas particulier de l’héritage: […] on doit remonter une pile complète d’héritage, en vérifiant à chaque étape si la méthode du comportement qui nous intéresse n’est pas surchargée.

s/surchargée/redéfinie

Cela me rappelle une règle de MISRA C++ 2008 (norme de codage en C++ par un consortium trouvant ses racines dans le milieu de l’embarqué automobile) qui en gros interdit de redéfinir une méthode qui dispose déjà d’une définition. Comme le SOLID sans le L: le non respect n’apportera pas des bugs, mais qu’est-ce qu’il nous compliquera l’existence!

En fait, on a tendance, quand on utilise la POO, à utiliser l’héritage dès qu’on a du code à factoriser

Pire: dès que l’on a des données à factoriser. Cf les exercices de modélisation de base de données (stocks de livres & CD, de pharmacies…) dans des chapitres sur … l’OO. Et tous les autres Points et PointColores… :-( Et là… oui. La composition!!! (ou l’héritage privé sous certaines conditions).

[LSP] Et je dis bien « devrait » et pas « doit »

Disons que quand on prend la version (/corollaire?) orientée contrat du LSP, un non respect des contrats hérités ne peut que conduire à des bugs si on exploite entièrement toutes les possibilités syntaxique de substitution d’objets enfants là où des instances respectant l’interface parente sont attendues. Que la classe mère soit abstraite ou non ne change rien. Si on a une interface List sans définition des fonctions, on ne pourra jamais disposer d’une SortedList qui respecte le contrat de List::append(e) (postcondition: back() == e; precondition: aucune sur e).

Si l’objet a des comportements en plus, pas de soucis coté LSP pour moi: on peut toujours passer une instance fille là où j’attends une instance (théorique — le parent peut être abstrait) parente. Par contre, pour profiter des nouveaux comportements, ça veut dire qu’il faudra de temps à autres déplacer le point de vue. C’est plus l’OCP qui pourrait souffrir pour moi si on commence à avoir des downcastings (on rejoint ton autre remarque sur isinstanceof).

performance, héritage, et composition

Je soupçonne que tu es sur des particularités Java. Surtout si la composition va pointer sur un objet qui ne sera pas dans la même ligne de cache… On n’aura pas d’indirection en C++ si on peut l’éviter.

un langage se déclare « orienté objet » mais permet de faire aussi du procédural

Tous les langages OO mainstreams sont procéduraux et impératifs: on donne des ordres dans des unités d’organisation qui sont des procédures/fonctions rangées dans des classes, et qui deviennent donc des méthodes. J’ai un doute pour le CLOS qui venait du LISP.

Merci pour ces pinaillages !

J’ai mis à jour le billet en tenant compte de quelques-unes de tes remarques, et j’en ai profité pour modifier le titre vers quelque chose de moins prétentieux… ce qui a fait planter la page d’accueil du site >_< – mais c’est réparé, merci à l’équipe technique !

D’autre part, il présente une version pragmatique de la POO et repose surtout sur la pratique ; il peut donc parfois s’éloigner des théories développées dans le cadre de l’étude formelle des langages de programmation.

Pour ceux qui seraient intéressé par les aspects formels de la POO, il y a cette discussion entre lmghs et Ksass`Peuk, avec pleins de sources : https://zestedesavoir.com/forums/sujet/6264/les-principes-solid/?page=2#p142720

Ce point fait que la POO n’est pas très utilisée dans les domaines qui ont un besoin massif de performances, comme le jeu vidéo ou le calcul scientifique.

C’est discutable. On voit régulièrement des implémentations from scratch dans les jeux de vtable ou équivalents. Parce que cela répond a un besoin, sur une problématique spécifique. Et dans ce cas, un héritage en POO n’est pas une mauvaise approche, même pour les jeux.

A mon sens, le fait que la POO et surtout l’héritage n’est pas beaucoup utilisé dans les jeux, c’est :

  • la confusion entre héritage et le deep inheritance hierarchy approach (encore appelé "abuser sa mère de l’héritage")
  • les habitudes des devs de JV, qui aiment beaucoup trop le bas niveau et la recherche de performances, au point où c’est parfois pas justifié rationnellement

C’est vraiment plus l’héritage que la POO en elle même qui pose des problèmes de performances.

Dans la liste des idées préconçues de ce que n’est pas la POO, la POO n’est pas l’héritage.

Sérieusement, qui a besoin de représenter « un chat » ou « une voiture » sous cette forme dans un programme ?

Je ne serais pas aussi catégorique. Il est vrai que la majorité des classes que l’on crées ne sont pas des objets physique du monde réels. C’est souvent des concepts qui ont un sens au niveau informatique ou des concepts qui ont un sens dans le domaine d’application sur lequel on travaille.

Pour autant, utiliser de tels objets physiques pour expliquer a un débutant ce qu’est la POO ne me semble pas une erreur :

  • parce que c’est pas non plus impossible d’imaginer des programme qui peuvent utiliser réellement des classes "chat" (par exemple dans un jeu de la vie ou même dans un javaquarium) ou "véhicule" (par exemple un programme pour calculer une route entre 2 points, et qui devrait différentier une voiture, un camion, un vélo, un piéton, etc)
  • parce que, pédagogiquement, c’est plus simple d’expliquer une chose en la rattachant à des choses connues. Et ce qu’un débutant connait en informatique, c’est généralement… pas grand chose. Donc utiliser des exemples du monde physiques, que les débutants vont pouvoir plus facilement visualiser, peut aider à mieux comprendre ces concepts.

A mon sens, l’erreur serait surtout de laisse penser à un débutant que la POO = "représente le monde physique". L’analogie sur les chats et les véhicules peut être utilisée comme introduction, mais on doit passer ensuite à des classes qui représente des choses plus abstraites.

En pratique, l’usage des objets fils n’est pas totalement substituable aux objets parents et ne cherche pas à l’être, les objets fils sont plutôt utilisés comme « des objets parents avec des propriétés en plus »

Comme je l’ai dit dans la discussion, je pense pas que LSP/substituabilité et ajouts de comportements soient contradictoires. Ce qui veut dire que, pour moi, même si on ajoute des comportements a une classe enfants, on doit respecter le LSP. Tout dépend de quel point vue on se place (quelle classe) pour le respect du LSP.

class animal {
public:
    void respire();
};

// on dérive de animal et on ajoute un comportement !?
class mammifère : public animal {
public:
    void pousse_un_cri();
};

// on dérive de animal sans ajouter de comportement
class poisson : public animal {};

class chat : public mammifère {};
class chien : public mammifère {};

// on définie des fonctions qui acceptent certains types
void utilise_animal(animal* p) { p->respire(); }
void utilise_mammifère(mammifère* p) { p->pousse_un_cri(); }

int main() {
    // on crée un chien avec une factory
    chien* p = create("chien"); 

    // ok, un chien est un mammifère et un animal
    mammifère* q = p;
    animal* r = p;

    // ok, utilise_mammifère accepte le type "mammifère"
    utilise_mammifère(q);
    // le LSP dit que quelque soit le type concret de q (chien, chat ou autre), ce type
    // concret doit respecter certaines règles pour garantir que cet appel soit valide.
    // A mon sens, ici, le LSP DOIT être respecté. C'est a dire qu'on ne doit pas écrire
    // de classe qui dérive de "mammifère" et qui invaliderait cet appel de fonction.

    // ok, utilise_animal accepte le type "animal"
    utilise_animal(r);
    // le LSP est respecté aussi ici.
}

A mon sens, le fait que mammifère ajoute le comportement pousse_un_cri ne change rien au respect du LSP. Du point de vue de ceux qui utilisent animal, le nouveau comportement est simplement ignoré et le LSP est respecté de leur point de vue. Et ceux qui utilisent mammifère et connaissent le comportement ajouté, le LSP est aussi respecté.

La violation de LSP vient si on essaie d’utiliser pousse_un_cri quand on a le type animal. C’est vrai que cela arrive parfois, mais c’est quand même un chose qui a un impact négatif sur la maintenance du code (il faut modifier les utilisateurs de animal quand on modifie mammifère) et c’est donc une chose qui doit être évité (et en pratique, pour ce que j’ai vu, est assez rare).

Mais globalement, pour moi, pas de problème avec le LSP et l’ajout de comportements.

+3 -0

Pour revenir sur l’enseignement de la POO à l’aide d’animaux, de véhicules ou de formes géométriques :

Le problème clé, c’est que 1. on donne ces exemples « de la vie réelle » aux étudiants, et que 2. on finit généralement par enseigner que « l’héritage, c’est quand on peut dire "A est un B" ».

La combinaison des deux crée des générations d’étudiants qui ont mal compris la POO et qui font n’importe quoi avec – j’en ai encadré assez pour le savoir.

Ça devient très intéressant quand tu écris ceci :

parce que c’est pas non plus impossible d’imaginer des programme qui peuvent utiliser réellement des classes "chat" (par exemple dans un jeu de la vie ou même dans un javaquarium)

Parce que le Javaquarium, malgré son enrobage qui laisse penser le contraire, est précisément un exercice conçu pour montrer que cette approche « naïve » de la conception des objets ne fonctionne pas, ou très mal. En particulier le point 3.3 avec sa double « hiérarchie » qui pose d’énormes problèmes si tu as appliqué ce genre de règle sans trop réfléchir. Et ça fonctionne très bien : pratiquement toutes les implémentations que j’ai vues de l’exercice en POO s’arrêtent au point 3.2 (et esquivent le problème), ou bien implémentent des « solutions » horribles à base de méthodes copiées/collées dans plusieurs classes.

Pour autant, utiliser de tels objets physiques pour expliquer a un débutant ce qu’est la POO ne me semble pas une erreur :

  • parce que c’est pas non plus impossible d’imaginer des programme qui peuvent utiliser réellement des classes "chat" (par exemple dans un jeu de la vie ou même dans un javaquarium) ou "véhicule" (par exemple un programme pour calculer une route entre 2 points, et qui devrait différentier une voiture, un camion, un vélo, un piéton, etc)
  • parce que, pédagogiquement, c’est plus simple d’expliquer une chose en la rattachant à des choses connues. Et ce qu’un débutant connaît en informatique, c’est généralement… pas grand chose. Donc utiliser des exemples du monde physiques, que les débutants vont pouvoir plus facilement visualiser, peut aider à mieux comprendre ces concepts.

Personnellement, je ne suis pas d’accord. Le principal soucis de parler d’objet physique c’est que tu mets en avant l’état sur le comportement.
La POO est avant tout une question de comportement. Avant même de parler de composition, d’héritage, de factory et autres pattern il faut parler de comportement de lien entre les comportement. Et on peut tout à fait introduire des notions d’objets qui sont alors purement informatique tout en se basant sur une situation réelle plutôt que d’utiliser des objets réels qu’on essaie de mettre en "mode informatique" au forceps.

Petit exemple auquel je pense depuis longtemps : un système de paiement. Tout le monde sait s’imaginer un contexte informatisé sur ça. Et du coup on va pouvoir guider pas à pas le débutant.

Au début on crée un objet "Payeur", un objet "Receveur" et on propose à payeur de payer. Puis on va complexifier avec "moyen de paiement". Comme on est en informatique, on va bypass la monnaie, on va faire par carte, par virement, par paypal. Puis tu vas te rendre compte qu’il faut un intermédiaire, par exemple pour faire des conversions de monnaie. Et là tu te retrouve avec un objet qui est payeur et receveur à la fois, tu as des notions de pattern etc. C’est le même concept que le javaquarium : tu vas petit à petit et tu conçois. Ici pas besoin d’objet physique, mais pourtant tu as bien des objets et c’est le comportement qui est mis en avant.

Et si tu es dans le cadre d’un tuto, tu peux même mettre en avant le fait que refactorer c’est une activité fréquente en informatique.

Tu peux introduire chacun des principe solide un à un et en collaboration avec les autres au fur et à mesure. Tu peux aussi avoir des retours critiques facilement sans être bloqué par des considérations stupides telles que "du coup carré c’est losange et rectangle mais les méthodes sont pas les mêmes alors du coup je dois coder mes losanges et rectangles en connaissance du carré alors que le carré est leur enfant". C’est exactement pour ça que les figures géométriques, les animaux etc. sont mauvais pour la POO.

Au début on crée un objet "Payeur", un objet "Receveur" et on propose à payeur de payer. Puis on va complexifier avec "moyen de paiement".

Au final, tu modélise quand même des objets physiques, pas des concepts abstraits. Ce sont des personnes (peut importe que tu les appelles "payeur" et "receveur" plutôt que "Jean-Pierre" et "Noé, caissière au McDo"). Idem pour le javaquarium, ce sont des objets physiques (des poissons).

Parce que le Javaquarium, malgré son enrobage qui laisse penser le contraire, est précisément un exercice conçu pour montrer que cette approche « naïve » de la conception des objets ne fonctionne pas, ou très mal.

SpaceFox

Justement, cela montre que c’est une approche qui a un sens limité, pas que c’est une approche qui n’a aucun sens. Pour cela que je disais qu’on devais assez rapidement utiliser des concepts plus abstraits, pour que les étudiants n’assimilent pas "POO = objets physiques". Mais c’est différent de dire que la POO ne peut jamais représenter des objets physiques.

C’est vraiment un choix pédagogique pas simple. Mais qui ne parait pas absurde en soi. Et probablement que dans la réalité, on multipliera les exemples et on sera attentif aux retours des apprenants, pour être sur que les idées soient bien comprises.

+0 -0

objets physiques pour expliquer a un débutant ce qu’est la POO

Je comprends gbdivers dans le côté exemple qui parle. J’ai plein d’analogies (bancales si on creuse) pour expliquer abstraction, encapsulation, etc à base de chats, de machines à laver, et ainsi de suite.

Toute fois, ces objets physiques sont vite un piège car il traînent "est-un" avec eux (plus que l’état ou le comportement: juste le "est-un" de la langue française (et pas que) et des classifications usuelles de la biologie ou autre). Un chat est un mammifère, et même un félin. Cool un héritage. Sauf que si on commence à creuser, c’est la cata avec les oiseaux qui volent et ceux qui ne volent pas, mais qui marchent-glissent-et-nagent. Les mammifères qui volent, ceux qui nagent… Bref notre chat est aussi un carnivore qui aime les frittes et les bananes. C’est un animal qui fait sa toilette et qui du coup va déglutir des boules de poils (sauf les Sphinx…). Bref on multiplie les "est-un" et… les exceptions à la règle. (1)

Alors il est vrai que pédagogiquement cela pourrait être intéressant de pousser la chose jusqu’au bout pour montrer les limites des 150 parents pour le chat. Sauf que non. Au chapitre/à la session qui présente les bien faits de l’OO, on ne va quand même pas démontrer que l’on va galérer pour modéliser proprement les exemples que l’on utilise. Non?

Du coup, je suis plutôt dans le camp des : on va éviter de prendre des exemples qui vont perturber les apprenants vifs qui sont en capacité de sentir que l’analogie est foireuse. Et qui sont en plus fichus de poser la question qui ouvre la porte aux digressions en plein cours. Je préfère l’aborder plus tard avec un exo de modélisation (seulement) du javaquarium, que je fais faire en groupes de 3 si je peux.

Côté exemple, je préfère mes balais & autres aspirateurs employés au milieu de la commonality nettoyer_le_sol.

(1) EDIT150: Vous noterez que sur les derniers exemples j’ai pris des est-un qui font référence à des comportements, et non à une classification liée aux ancêtres (dans l’"arbre" de l’évolution) (-> état). Tout ça pour dire que mon problème avec les objets physiques, c’est le est-un et pas les états. Le problème des modélisations et exemples de cours trop centrées sur les états on les a sur toutes les modélisations de base de données (gestion de bibliothèque), les objets valeurs (ex.: point & point coloré). C’est aussi un problème, mais pas intrinsèquement lié aux objets physiques, je trouve.

les objets valeurs (ex.: point & point coloré). C’est aussi un problème, mais pas intrinsèquement lié aux objets physiques, je trouve.

C’est quoi plus exactement le problème de ce type d’objets ? Car je ne vois pas trop le soucis avec ça.

+0 -0

les objets valeurs (ex.: point & point coloré). C’est aussi un problème, mais pas intrinsèquement lié aux objets physiques, je trouve.

C’est quoi plus exactement le problème de ce type d’objets ? Car je ne vois pas trop le soucis avec ça.

Renault

Cela fut discuté chez les voisins: https://openclassrooms.com/forum/sujet/erreur-de-conception-pointcolore-est-un-point

Ah ok, tu parles d’un cas précis d’héritage entre deux objets valeurs, je pensais que tu généralisés à tout objet dont l’objectif est essentiellement de porter des valeurs.

+0 -0

Au final, tu modélise quand même des objets physiques, pas des concepts abstraits. Ce sont des personnes (peut importe que tu les appelles "payeur" et "receveur" plutôt que "Jean-Pierre" et "Noé, caissière au McDo").

Le contexte est "physique" mais l’objet est défini par son comportement (payer ou recevoir) et non par sa nature physique. Dans un système informatique tu auras sûrement un "PayeurParticulier" ou "PayeurProfessionnel" (lié aux taxes par exemple), et ce payeur aura par composition un "identification* qui aura soit sa raison sociale soit son nom/prénom par exemple.

Une nouvelle fois, l’idée est de créer un objet-comportement qu’on peut lier/instancier de manière physique plutôt qu’un objet physique qu’on force à se comporter de manière informatique.

Ça n’est pas non plus un paradigme orienté performances : le découplage et la cohérence du code (et donc sa maintenabilité) sont prioritaires. Ce point fait que la POO n’est pas très utilisée dans les domaines qui ont un besoin massif de performances, comme le jeu vidéo ou le calcul scientifique.

Ça a déjà été un peu mentionné, mais ce point me parait assez douteux. Sauf cas très pathologique, le paradigme de programmation utilisé n’a qu’une influence extrêmement marginale sur les performances. En pratique, ce qui compte surtout pour avoir des bonnes performances (si on parle d’une tâche limitée par le CPU/les accès mémoire plutôt que l’IO) est que (sans ordre particulier) :

  1. les algorithmes utilisés sont efficaces ;
  2. l’implémentation tient compte si besoin de problématiques matérielles (taille des caches, topologie) ;
  3. le langage utilisé (ou plutôt son implémentation) est capable d’optimiser pour virer les indirections et les copies inutiles (et de produire du code machine efficace pour l’architecture cible).

En pratique, à algorithme équivalent (point 1 OK) et en prenant des langages axés performances (C++, Rust, Fortran ; point 3 possible) ce qui peut rendre un code OOP inefficace est exactement la même chose que ce qui peut rendre un code procédural inefficace : les gens qui codent n’importe comment en isolant mal les abstractions qui vont bien. Ça tire un trait d’une part sur le point 2 (si la logique métier est déjà mal gérée, améliorer le point 2 ne va pas bien se passer), et d’autre part rend le travail du compilo pour le point 3 plus difficile (et donc moins susceptible de bien se passer et plus difficile à contrôler). Un exemple concret de ça est un code de calcul qu’on refactore avec des collègues qui fait des calculs flottants sur des gros tableaux (difficile de faire plus classique). Le résultat final est quasiment une réécriture complète passant d’un gros tas de spaghetti procéduraux à un code OOP plutôt propre. Les détails du point 2 (en l’occurrence ici simplement découper les calculs pour qu’ils passent dans L3) sont enterrés dans des objets que le reste du code peut manipuler comme des vecteurs d’états avec une logique qui correspond à la physique qu’on veut implémenter. Les performances sont deux fois meilleures que le code de départ et le code est beaucoup plus propre et facile à maintenir. Les coût potentiels associés au fait qu’on utilise l’OOP sont complètement invisibles ; les compilateurs sont très bons pour le point 3 tant qu’on leur fournit un code sur lequel il leur est possible de raisonner (ce pour quoi l’OOP peut même aider!).

La raison pour laquelle l’OOP est peu utilisée dans le domaine du calcul scientifique est essentiellement culturelle : il y beaucoup de programmes qui ont été écrits par des physiciens qui ne sont pas aussi de bons développeurs, potentiellement avec un langage qui supporte mal (ou pas du tout) l’OOP (e.g. Fortran 90). Ça commence à s’améliorer avec la culture en software design qui percole un peu et l’embauche de gens dont le métier est spécifiquement de développer les programmes scientifiques.


De plus, l’héritage peut être performant par rapport à d’autres techniques comme la composition.

Je n’ai plus les chiffres exacts sous la main, mais le temps d’accès à une méthode, même avec du polymorphisme, est presque constant dans les langages à JVM (Java, Kotlin…) quelle que soit la profondeur d’héritage, même déraisonnable (plusieurs centaines de niveaux). La même structure représentée avec de la composition a un temps d’accès grossièrement linéaire avec la profondeur de composition. Ça peut être différent avec d’autres langages, à vérifier.

Déjà, les temps associés aux résolutions d’arbre d’héritage vs composition ont de fortes chances d’être négligeables dans un code qui fait beaucoup de calculs (e.g. un code Python avec plein d’objets qui appelle ultimement numpy pour un calcul suffisamment gros pour qu’on s’inquiète des performances va probablement passer le plus clair de son temps dans les routines natives numpy). Ensuite, la note de bas de page est complètement langage dépendent et n’a pas grand chose à voir avec le paradigme OOP. Par exemple en Python, composition et héritage ont probablement un coût relativement similaire (il faut résoudre O(n)O(n) symboles dans les deux cas, avec nn la profondeur d’héritage ou le nombre d’indirections), et dans les langages compilés comme C++, les optimisations pour virer les indirections et inline les fonctions rendent la comparaison générale impossible.


On voit arriver de nouveaux problèmes. Comme cette mode qui veut que l’on ne crée plus que des objets immuables, ce qui est très pratique pour les traitements fonctionnels et peut sensiblement améliorer les performances… mais qui est contraire à la définition d’ « objet » dans la POO.

Je ne suis pas sûr de comprendre ce que tu veux dire par là. Si on prend la définition d’objet que tu choisis dans cet article d’avoir un état interne et un comportement, alors être immuable est une propriété orthogonale à la définition d’objets. Ça veut juste dire qu’on n’expose pas de comportement qui modifie l’état interne. Si on se place dans le cas où "immuable" veut en fait dire "idempotent" (qui est la propriété intéressant pour pouvoir raisonner avec le code, et qui autorise la mutabilité interne), c’est même une conséquence assez naturelle de l’encapsulation pour les objets qui ne sont pas des conteneurs (ou non-idempotent par design, comme un générateur de nombres aléatoires).

+1 -0

Ça a déjà été un peu mentionné, mais ce point me parait assez douteux […]

Je suis à moitié d’accord avec toi pour le calcul scientifique, et j’ai en tête la problématique array of structures versus structure of arrays. J’ai l’impression que la POO, appliqué naïvement, te pousse naturellement à définir des classes pour chaque objet de ton système, et donc naturellement à partir sur array of structures. On peut penser au Javaquarium, ou chaque poisson va naturellement être modélisé par une classe et ces attributs.

Et là, je serai curieux de voir à quel point un compilateur est malin pour aider à vectoriser toute opération d’agrégation sur un attribut de chacun des objets (pour reprendre le Javaquarium, on peut penser à une moyenne des points de vie des poissons qui donnerait une indication d’à quel point l’aquarium se porte bien). En particulier dans des langages comme C ou on a des pointeurs dans tout les sens, et ou le processeurs peu avoir des difficulté à mettre en cache les bonnes données. Je pense que c’est ça que @Spacefox a en tête quand il dit que "ce point fait que la POO n’est pas très utilisée dans les domaines qui ont un besoin massif de performances, comme […] le calcul scientifique."

Après, je suis d’accord que dans certains cas (algèbre linéaire, typiquement), on se retrouve à travailler sur des tableaux de nombre et cet aspect passe clairement au second plan. Par contre, j’ai plus de doute sur une approche type "équation du mouvement", ou une approche naïve (et qui fonctionne très bien d’un point de vue pédagogique) est de donner à chaque point sa coordonnée (et, disons, sa vitesse), et de faire un tableau (ou une liste) de points. C’est là que l’approche Entity Component System peut se révéler intéressante, puisqu’elle pousse plus naturellement à imaginer l’inverse.

Finalement, je suis d’accord avec que la POO reste une bonne manière d’architecturer un code scientifique dans l’absolu, et qu’avant d’en arriver à AOS versus SOA, il y a déjà d’autres questions à ce poser.

+0 -0

Mouais, AOS vs SOA est vraiment une problématique de base qui n’est pas attaché à un paradigme quelconque. C’est aussi un problème qui n’est pas si gros que ça en pratique, dans le pire des cas tu essaies les deux et tu vois vite ce qui marche le mieux…

J’ai l’impression que la POO, appliqué naïvement, te pousse naturellement à définir des classes pour chaque objet de ton système, et donc naturellement à partir sur array of structures.

Comme tu le dis toi-même, c’est une application naïve de l’OOP, donc c’est pas franchement un argument pertinent pour dire que l’OOP est adaptée ou non pour faire du HPC (qui demande en général de ne pas être naïf, si c’était facile ça se saurait). De manière plus générale que AOS vs SOA, et probablement plus pertinent dans une discussion sur l’OOP, l’ordre dans lequel les objets sont composés en général compte pour les performances. Là encore, c’est pas un effort incroyable d’essayer plusieurs ordres et voir ce qui marche le mieux si on n’a pas d’argument très fort en faveur de l’un ou l’autre.

Par contre, j’ai plus de doute sur une approche type "équation du mouvement", ou une approche naïve (et qui fonctionne très bien d’un point de vue pédagogique) est de donner à chaque point sa coordonnée (et, disons, sa vitesse), et de faire un tableau (ou une liste) de points. C’est là que l’approche Entity Component System peut se révéler intéressante, puisqu’elle pousse plus naturellement à imaginer l’inverse.

Mouais, j’ai envie de dire que si tu écris des programmes performants, tu vas croiser les notions de data locality, de taille de cache, et de vectorisation assez vite et donc que tout stacker ensemble est une idée qui va venir assez naturellement (et me semble assez éloigné de ce à quoi sert un ECS en pratique, surtout que les ECS masquent typiquement l’organisation en mémoire des composants). Ce qui peut compter ensuite (et rejoint AOS vs SOA) est l’ordre dans lequel tu stackes ([vx1, vx2, ..., vy1, vy2, ...] vs [vx1, vy1, vx2, vy2, ...]) et là c’est encore une fois juste une question d’essayer les deux dans le pire des cas, ou même de transposer entre différentes parties du code.

EDIT : l’OOP peut d’ailleurs aider dans ce dernier cas. On peut imaginer abstraire le tableau des vitesses stackées dans un objet qui s’occupe de transposer au besoin lorsqu’on lui demande d’effectuer une opération (qui elle-même expose son ordre préférée pour les données).

+0 -0

Hum… L’OO ce n’est pas exactement la façon d’organiser les données qui est la plus adaptée pour des SoA… Et du coup dans les couches calculatoires intenses ce n’est pas ce qu’il y a de mieux pour stocker des millions de données.

près comme d’hab, il faut mesurer, tout ça…

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte