C'est quoi le problème avec la POO, au fait ?

a marqué ce sujet comme résolu.
Auteur du sujet

Disclaimer: ce sujet n’est pas un troll, je me pose très sérieusement la question

Salut !

Au cours de mes études d’informatiques, on m’a évidement vendu la POO (et Java) comme étant un paradigme puissant, à peu près solid(e)1 et donc tout à fait correct. Ayant été biberonné au SdZ et à son (pas génial, rétrospectivement2) tutoriel de C++, et ayant ensuite adopté le Python professionnellement et personnellement, je pense donc principalement mon code en terme d’objets. Je constate ceci dit qu’une partie de la communauté internet est fâchée avec la POO. Ça englobe les gens qui (sur)vendent la programmation fonctionnelle (que je n’ai jamais eu le plaisir de pratiquer) et puis des gens plutôt sérieux, puisque Rust s’en passe très bien (et Rust, c’est le bien, non ?). Et comme avant de juger, il faut comprendre, me voici.

Alors oui,

  • L’héritage multiple, c’est l’horreur.
  • On a des problèmes de références versus pointeurs dans certains langages qui font encore la différence entre ça, et de manière générale, de shallow versus deep copy.
  • Certains objets ne sont effectivement qu’une excuse pour ranger des variables et les fonctions qui les utilisent au même endroit (typiquement, une classe qui n’est instanciée qu’une fois dans le programme).3

… Mais est ce que c’est si grave que ça ?


  1. Liskov versus pas Liskov, c’est pour un autre jour.
  2. En tout cas dans sa première version, mais c’est pas la question
  3. Quand on a passé du temps à t’expliquer que les variables globales, c’est le mal, c’est ce qui arrive.

Édité par pierre_24

#JeSuisToujoursArius • Doctorant et assistant en chimiedev' à temps partiel (co-réalisateur ZEP-12, recherche et template LaTeX)

+2 -0

Lu’!

Les paradigmes de programmation sont globalement mal définis et ne donnent pas d’informations claires sur ce que permet ou ne permet pas un langage.

Personnellement ces classifications, je les trouve pénibles et pas claires. Je pense qu’on ferait bien mieux de définir en terme de fonctionnalités fournies (et de définir clairement ce que sont ces fonctionnalités et leurs propriétés), ça permettrait de juger plus clairement des avantages, restrictions, et nécessités (notamment en terme de tooling) de chaque langage en fonction d’une tâche cible.

Edit: amha il n’y a pas une feature qui est plus l’horreur qu’une autre (par exemple l’héritage multiple), elles ont juste des cas d’application et des manière correctes ou non de les utiliser.

Tout ça pour dire : on part sur quelle définition de l’orienté objet ?

Édité par Ksass`Peuk

Salut,

Déjà quelques remarques sur ton message :

puisque Rust s’en passe très bien

Les struct de Rust enrichis de fonctions membres à coup de impl, ce sont essentiellement des classes, le bouquin pour apprendre Rust à même un chapitre dessus. Alors, on peut ensuite pinailler sur les définitions, mais comme le dit @Ksass`Peuk, ce n’est pas très intéressant. En terme de features par contre, il est difficile de ne pas voir le parallèle énorme entre une struct couplée avec impl, et une classe basique comme on peut en écrire dans quasi n’importe quel langage POO canonique. Là où Rust diffère de beaucoup de langages de POO classiques, c’est sur la stratégie adoptée pour écrire du code générique, avec des Traits (qui essentiellement remplacent les ABC de Python que tu connais) plutôt que de l’héritage. J’y reviens.

On a des problèmes de références versus pointeurs dans certains langages qui font encore la différence entre ça, et de manière générale, de shallow versus deep copy.

C’est pas propre à la POO, d’ailleurs Rust (et C) expose encore cette différence à l’utilisateur. Là, ce n’est pas le modèle POO vs fonctionnel vs autre qui compte, c’est le modèle de gestion de la mémoire. Tout langage système va devoir exposer cette différence d’une façon ou d’une autre pour que l’utilisateur puisse choisir entre passage par copie et passage par référence, en gros. Là où Rust se démarque par rapport à C et C++ (outre les garanties en safety), c’est avec son système de déréférencement automatique (via le trait Deref) et le fait que le compilo écrit essentiellement le code à ta place quand tu as une référence/déréférence manquante en la détectant et en te disant comment la corriger.


Pour revenir sur le problème de fond, les emmerdes avec la POO commencent quand tu veux écrire du code générique. La stratégie de Python par exemple (via le duck typing) est que le code que tu écris ne doit pas s’appuyer sur le type de tes données (leur classes en fait), mais sur l’interface qu’elles t’offrent. C’est puissant et flexible, mais de fait pour éviter d’écrire sans cesse les même interfaces (sinon on déplace simplement les problèmes de duplication de code qu’un code générique est là pour limiter…), on est obligé de s’appuyer sur de l’héritage multiple, sans garantie (du moins en Python) qu’il n’y a pas une classe au milieu du MRO qui fait n’importe quoi en écrasant une interface dont tu as besoin. Les ABC sont d’ailleurs exactement là juste pour que nos classes concrètes en héritent, et qu’on puisse dire au code appellant (ou au type checker qu’on a mis en place parce que le dynamisme de Python te rattrape et rend ton code imbitable sans) "oui oui, je suis bien un Mapping, tu peux te servir de moi à la place d’un dict dans plein de contextes différents".

Là où Rust, pour écrire du code générique — bien que proposant des struct proches des classes — fait un choix plus intelligent avec les Trait. Essentiellement, quand tu implémentes un Trait pour un struct, tu dis "mon type a telle interface" (et le code générique appellant à juste besoin que tu implémentes le Trait dont il a besoin), donc sur le fond c’est là même idée qu’hériter d’une ABC, sauf que tu n’as pas le problème du MRO potentiellement bordélique par dessus (et si tu compares Python à Rust, le compilo garantit que l’interface est effectivement la bonne, du moins en terme de typage (la signification reste à la charge de l’implémentation), pas en Python avant le runtime).

Les langages fonctionnels, eux, ont souvent une stratégie qui ressemblent aux traits de Rust, par exemple les typeclasses de Haskell. Il y a des différences sur quelques points entre les deux, mais à l’usage l’idée derrière est la même : garantir des comportements sur les types qui ont tel trait ou telle classe de type.

Édité par adri1

I don’t mind that you think slowly, but I do mind that you are publishing faster. — W. Pauli

+0 -0

Alors, je pense qu’une grosse chose que les gens n’aiment pas dans la POO "habituelle" (on y reviendra après) c’est la cérémonie.

Déclarer des classes, signifier qu’on est en train de surcharger des méthodes, empêcher d’en surcharger certaines, gérer la visibilité (qui va forcément évoluer au fur et à mesure du projet) mais aussi l’arbre d’héritage…

Le truc c’est qu’en 2020 on a les moyens de prendre partie des concepts objets en se passant de ça. Regarde go et ses struct/interface et l’implémentation de l’interface sans cérémonie, ça résoud à peu près tous les problèmes communs de la POO sans passer par la cérémonie.

L’autre aspect c’est que beaucoup de personnes confondent "POO" avec "utiliser des classes". Résultat plusieurs langages qui ont dans leur passé utilisé des classes sans casi rien permettre des principes de la POO (PHP < 5.3 je pense à toi, par exemple) ont vraiment fait du mal à la POO.

Autre aspect, plus européens, une grosse partie de la POO est apprise avec des méthodologies dépassées et se basant sur C++ mal enseigné. Le résultat c’est une mauvaise compréhension des patrons basés sur la délégation ou la substitution de type.

+2 -0

Bonjour,

Je ne connais personne qui ait déjà eu un projet avec de la vraie complexité structurelle à gérer et qui ait déjà douté de l’utilité des motifs orientés objet.

Tous les systèmes d’exploitation, moteurs de rendus ou autres systèmes complexes répandus actuels utilisent une forme d’orienté objet et ce n’est pas pour rien. Linux y compris qui utilise largement de l’orienté objet imité avec du C.

Rust est un langage très orienté objet, ses concepteurs ont juste réinventé la roue en renommant de nombreuses choses (classe en structure, corps de méthodes en implémentation de structure, classe abstraite ou interface en trait…) ce qui perd les gens pour peu d’intérêt pratique.

De manière générale, la notion d’orienté objet (qui revient ni plus ni moins à englober des variables et des pointeurs de fonctions dans une structure allouée dynamiquement) revient à donner un moyen à ton programme d’être composé d’un nombre variable et adaptable d’agents qui remplissent des fonctions abstraites et interagissent entre eux, cela avec un niveau presque illimité d’abstraction, et c’est très analogue à ce que tu vois dans l’organisation des autres systèmes complexes de la « vie réelle » : les gens sont englobés dans des sociétés, ou dans des administrations qui peuvent se porter concurrence, interagir entre elles ou porter des actions bien définies, les sociétés et les États dans des pays, les cellules dans des êtres vivants…

Édité par r0anne

Dans le domaine du jeu vidéo, on reproche couramment à la POO la performance, tout simplement. En vrac :

  • le coût d’un appel virtuel en C++ est simplement trop élevé pour y recourir à un niveau fondamental ;
  • le coût d’un dynamic_cast en C++ est trop élevé, donc tout le monde compile sans RTTI et réimplémente un mécanisme basique si il le faut, compliquant un peu le design autour de l’objet ;
  • il est très difficile d’optimiser et paralléliser un grand nombre d’objets différents interdépendants sur un modèle objet ;
  • et surtout, l’approche moderne du design structurel d’un jeu vidéo est complètement incompatible avec un modèle objet. Je développe au paragraphe suivant…

Tous les systèmes d’exploitation, moteurs de rendus ou autres systèmes complexes répandus actuels utilisent une forme d’orienté objet et ce n’est pas pour rien.

r0anne

Typiquement dans le jeu vidéo ou le rendu, on a une approche orientée données, avec par exemple un modèle dit ECS (entité, composant, système) dans lequel le principe est d’appliquer des opérations (les systèmes) en grand volume à des données (les composants). C’est extrêmement facile à optimiser et paralléliser, et par défaut ça donne du code qui torture moins le cache à jardiner dans la mémoire comme un débile.

Alors certes, il y a toujours une notion d’objet, mais avec deux gros points qui surprennent souvent les gens venant de la POO :

  • le code et les données sont fondamentalement séparées, les données sont de simples zones mémoires qu’on peut donc allouer massivement dans des allocateurs 'pool' dédiés… et le code va naturellement être écrit comme un process avec des entrées et des sorties ;
  • il n’y a pas d’héritage mais de la composition massive - un chat n’est pas un animal spécialisé avec des moustaches, mais un animal, et des moustaches.
+2 -0

il n’y a pas d’héritage mais de la composition massive - un chat n’est pas un animal spécialisé avec des moustaches, mais un animal, et des moustaches.

c’est bien le problème : ça étonne des gens mais en informatique un chat ne devrait jamais être une spécialisation d’animal, mais plutôt un animal qui délègue son cri au miauleur, qui possède des coussinets et qui hérite de démon.

+3 -0

Voilà, on ne devrait pas confondre être un, avoir un, et avoir le comportement d’un. Des idées différentes, potentiellement des outils différents.

Assez des salamis, je passe au jambon — Je fais un carnage si ce car nage car je nage, moi, Karnaj ! — Le comble pour un professeur de mathématique ? Mourir dans l’exercice de ses fonctions.

+2 -0

il n’y a pas d’héritage mais de la composition massive - un chat n’est pas un animal spécialisé avec des moustaches, mais un animal, et des moustaches.

c’est bien le problème : ça étonne des gens mais en informatique un chat ne devrait jamais être une spécialisation d’animal, mais plutôt un animal qui délègue son cri au miauleur, qui possède des coussinets et qui hérite de démon.

artragis

C’est bien normal que ça surprenne, c’est ce qui est enseigné partout, et c’est finalement une logique facile à suivre. On écrit une classe animal et puis on écrit une classe chat qui spécialise le comportement…

… Et tôt ou tard on se retrouve à implémenter le ronronnement, un comportement unique à cet animal, qui a des entrées et des sorties, et qui se retrouve invariablement avec des "si animal est chat, alors…", et on pleure. Au moins, dans un modèle ECS, on a explicitement un composant animal et un composant ronronnement, et tous les composants ronronnement sont mis à jour à un moment à partir des données du composant animal.

L’autre exemple classique de limite de la POO c’est l’héritage multiple. J’ai un animal, un chien, un chat, et un concept d’animal adoptable ; pas moyen de caser ça dans l’arbre d’héritage sans définir aussi quels animaux sont adoptables dans le design même, aussi pratique que ça soit.

+3 -0

À titre personnel, je pense que ce qu’on reproche à la programmation orientée objets, ce n’est pas son principe, mais plutôt son mauvais usage.

Parfois, on se force à l’utiliser alors que le problème se modélise naturellement avec d’autres techniques. Je pense notamment aux compilateurs, qui tiennent plus de la succession de transformation que d’interactions entre entités. Ils se retrouvent souvent à être plus facile à écrire avec un langage fonctionnel que de se forcer à trouver de bons objets.

D’autres fois, c’est parce que c’est mal utilisé, comme déja discuté plus haut. Les modélisations naturelles ne sont pas forcément les plus adaptées. Les modélisations adaptées sont plus dures à faire faute d’habitude ou de connaissances.

En fin de compte, c’est plus un choix d’outils adaptés et bien utilisés qu’il faut faire plutôt qu’avoir un marteau et planter les clous de travers ou taper sur des vis avec.

+2 -0

À titre personnel, je pense que ce qu’on reproche à la programmation orientée objets, ce n’est pas son principe, mais plutôt son mauvais usage.

Quand un outil est fondamentalement difficile à utiliser correctement, c’est peut être signe qu’il pourrait être une bonne idée de le repenser. En reprenant les bonnes idées et en virant les mauvaises en les remplaçant avec un design différent. Je pense vraiment que le système de trait de Rust par exemple apporte beaucoup sur la table en horizontalisant les relations entre un type et son interface. J’ai un peu joué avec récemment, et c’est franchement un plaisir parce que toute les questions de savoir si il faut un arbre d’héritage ou déléguer ou bien séparer les objets ne se posent plus par construction. Tu veux partager un comportement, tu fais un trait. Tu veux un comportement existant, tu implémentes ce trait. C’est un peu un no brainer assez comfortable à l’usage et pratique.

Édité par adri1

I don’t mind that you think slowly, but I do mind that you are publishing faster. — W. Pauli

+4 -0

Hello,

Je vais répondre en citant en vrac.

C’est quoi le problème avec la POO, au fait ?

Les fanboys, croire que les balles en argent ça existe, un enseignement bâclé/qui n’a pas compris de quoi ça parle… Bref. Ca, et des limitations intrinsèques, comme tout paradigme qui a ses limitations intrinsèques.

L’OO a des forces (comme tout paradigme) et de nombreuses limitations (comme tout paradigme). Pour le deuxième point, plus d’un Design Pattern couvre d’ailleurs des limitations du paradigme OO ou de ses pseudos mises en œuvres par chaque langage prétendument OO.

Je rejoins Ksass, il y a d’abord des besoins. Ensuite l’OO, le fonctionnel, le procédural, le déclaratif, le générique, etc vont pouvoir y répondre de manière plus ou moins simple et efficace.

Sa limitation intrinsèque pour modéliser ce qui est complexe comme le vivant est assez classique. Les ECS et autres systèmes de traits sont effectivement une réponse propre, et probablement plus simple que la multiplication des patterns template method + strategy.

une grosse partie de la POO est apprise avec des méthodologies dépassées et se basant sur C++ mal enseigné. Le résultat c’est une mauvaise compréhension des patrons basés sur la délégation ou la substitution de type.

Ca me donne envie de troller ça. Avec la lib standard de Java 1.0 qui voyait des sortedlist dériver de list, les cours tout langage confondu qui présentent les coloredpoint comme enfants de point, l’abus de setters, EntrepriseFizzBuzz (clairement pas une émanation du microcosme C++), etc.

Après, je suis d’accord pour dire qu’il y a un problème de transmission. Mais c’est un problème simplement incrémental. On avait l’impératif, on rajoute les fonctions/procédures (-> procédural), puis un rajoute les classes et on croit faire de l’OO. Le langage au milieu est presque anecdotique in fine. Quand on a l’impression que les langages se conçoivent comme des des couches d’oignons, ben … on conçoit nos cours comme des couches d’oignons qui s’appuient sur les acquis…

Dans le domaine du jeu vidéo, on reproche couramment à la POO la performance, tout simplement. En vrac :

  • a- le coût d’un appel virtuel en C++ est simplement trop élevé pour y recourir à un niveau fondamental ;
  • b- le coût d’un dynamic_cast en C++ est trop élevé, donc tout le monde compile sans RTTI et réimplémente un mécanisme basique si il le faut, compliquant un peu le design autour de l’objet ;
  • c- orienté données VS orienté objets

a- C’est un faux problème. Comparer un appel virtuel à un appel qui ne l’est pas ne devrait pas être une question qui se pose. Un appel virtuel est là pour modéliser une adaptation/point de variation dynamique du programme au moment de l’exécution. S’il y a quelque chose auquel cela a du sens de les comparer, c’est à des if elseif elseif... else, des switch, des tables de pointeurs de fonctions, etc.

b- Le downcasting est tout sauf une approche OO. Quand on downcaste, on ne réfléchit pas à une séquence générale mais seulement à des cas particuliers. Nous sommes en pure violation du Principe Ouvert-Fermé. Un intérêt de l’OO est d’apporter une solution propre à cette problématique de points de variation dynamiques avec les liaisons dynamiques (appels virtuels pour en revenir au C++)

c- Tout à fait, l’orienté données offre de meilleures performances car il permet de traiter massivement des données identiques: meilleure utilisation du cache, meilleures possibilités de vectorisations, etc. On en revient au StructOfArrays qui a de meilleures perfs que l’ArrayOfStructs. Nativement, les langages OO nous mettent dans un monde de AoS.

Pour en revenir à la question OO VS fonctionnel, un argument que je vois régulièrement, et que je n’ai pas retrouvé dans la discussion, c’est que le fonctionnel s’adapte bien mieux aux problématiques de parallélisation. Essentiellement c’est une conséquence que le non-mutable "élimine" les problématiques de synchronisation de données partagées, et que l’OO c’est une incitation à l’utilisation de données mutables.

Édité par lmghs

+6 -0

Quand un outil est fondamentalement difficile à utiliser correctement, c’est peut être signe qu’il pourrait être une bonne idée de le repenser.

Après j’ai pas mal l’impression que les outils modernes tels que go ou kotlin (et même dans une certaine mesure java 8 ou même 12 et ses Record) rendent cette aspect "difficile à utiliser correctement" beaucoup plus… complexe.

Dans mon boulot on a typiquement le cas en ce moment qui nous permet d’avoir des comparaisons assez claires : notre entreprise a créé une application qu’on considère aujourd’hui comme legacy avec la logique "POO à la zobe un chat est un animal spécialisé" et chaque comportement spécifique se retrouve justement basé sur le "if (document instanceof TypeDocument1){/*bla*/}".

Comme l’application devenait difficile à maintenir ils ont lancé il y a quelques années un chantier de refonte et là on arrive à la "migration" du module qui possède le plus de cas complexe. Depuis le début notre refonte utilise massivement la POO à coup de pattern bien réfléchi, de substitution de type, de visiteur quand c’est nécessaire et résultat, non seulement on process plus vite que l’ancienne appli (pas de if/else partout) mais en plus ce matin j’ai dû écrire un mail en disant que comme on utilisait justement ce pattern, implémenter un bout de fonctionnalité dans ce module implémenterait le bout de fonctionnalité similaire dans un autre module de l’application sans aucune ligne de log supplémentaire, ce qui aura un effet sur notre roadmap.

J’ai eu l’occasion de toucher à des langages qui se centrent sur la donnée pour la traiter ensuite en OO (go est un cas typique) et l’OO a vraiment un gros avantage quand il faut reprendre le code de quelqu’un et intégrer des modules ensemble.

Après l’OO ça vient avec une foultitude de problématique qui dépendent de l’implémentation dans le langage de programmation. En java on a la cérémonie certes mais on a aussi les antipattern avec les getter/setter. Tu regardes C#, concurrent direct de java, tu as pas ça. En C# tu as aussi linq qui est un DSL qui se sert de la substitution de type pour exister.

C’est pas la première fois que je vois aussi les gens louer les "traits", que ça soit en ruby, en PHP, en rust même si comme je ne connais que ceux de PHP je suis à peu près certain que chaque langage a sa philosophie sur les "traits". Mais globalement ça permet surtout de sauver la POO du coûteux problème de la "réutilisation de code" qui force les gens à faire des héritages qui n’ont aucun sens à la base.

c’est ce qui est enseigné partout, et c’est finalement une logique facile à suivre

c’est en effet un problème que ça soit enseigné partout. En vrai c’est intuitif de dire ça comme ça, mais les trois quarts du temps tu te retrouves immédiatement à dire "oui mais", preuve que c’est pas si "facile à suivre" que ça comme logique.

Édité par artragis

+0 -0

De mon point de vue, le principal problème avec la programmation orientée objet, c’est qu’elle est mal comprise.

La conséquence, c’est qu’elle est mal utilisée (y compris au sein de langages qui sont censés la mettre en valeur), mal enseignée, et qu’on essaie de faire tout et n’importe quoi avec. Alors que c’est un outil, et que comme tout outil, la POO a un cadre d’utilisation, et l’utiliser hors de ce cadre est au mieux non optimal.

À propos de la POO

C’est quoi la POO ?

En programmation orientée objet, on a des composants de base qui sont les objets (merci Captain Obvious). Les objets ont un état interne et un comportement qui leur permet d’interagir entre eux.

Et… c’est tout.

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.

Ce que n’est pas la POO

Y’a plein de trucs qui sont souvent considérés comme « indispensables à la POO » alors qu’en fait pas du tout. Comme les classes, le typage statique et/ou fort, les interfaces, etc.

Pourquoi la POO ?

La POO permet à l’origine de répondre à deux problèmes :

  1. La réutilisation « simple » du code (via la réutilisation des objets)
  2. 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.

La recherche de performance, par contre, n’est pas une priorité.

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

« 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…

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 plus de 10 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)
  • Des chaines d’héritage qui n’ont aucun sens et qui conduisent à des comportements aberrants1
  • 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 main).
  • Des objets planqués sous le tapis (dans les Enum par exemple)

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

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.

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’état interne (donc non exposé à l’extérieur). Un getter ou un setter ne devrait être créé que si c’est pertinent, et ne pas correspondre obligatoirement à un état interne réel.
  • 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.
  • 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.

Et donc, on fait quoi ?

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 deux 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 immutables, 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 confusion.

  1. 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 si tu les appelle.

On se rejoint pas mal, mais je vais chipoter sur un point.

Pourquoi la POO ?

La POO permet à l’origine de répondre à deux problèmes :

  1. La réutilisation « simple » du code (via la réutilisation des objets)
  2. 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.

Source:SpaceFox

J’ai toujours considéré la réutilisation comme un argument marketing totalement pipeauté: on réutilise du code depuis les sous-programmes/routines/procédures/fonctions.

Entièrement d’accord sur le second point que je tends à décomposer: d’abord avoir des unités cohérentes (on rejoint la fameuse encapsulation, et très souvent indirectement l’abstraction). Après, mon point majeur: c’est l’OCP (principe ouvert fermé), ou encore les points de variation dynamiques au sein de code commun. Certes, il y a une sorte de réutilisation. Mais ce ne sont pas des routines autonomes qui seraient l’élément typique OO qui pourrait enfin être réutilisé, mais des séquences complexes (les commonalities) qui reposent sur des points de variations dynamiques qui permettent d’adapter le comportement — entre les commonalities et les VP, il y a au fond ces contrats qui permettent de cadrer les spécialisations légitimes. Appelez ça sous-typage, polymorphisme d’inclusion dynamique, fonctions virtuelles… c’est pour moi la grosse spécificité de l’OO. Les Types Abstraits de Données, adressent déjà à leur façon abstraction et encapsulation.

On rejoint la pseudo définition d’Aan Kay qui a la paternité du terme orienté objet — même si au fond il est bien plus laxe dans son expression relativement aux contrats attachés aux messages je trouve

OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. — http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en

Ce qui est enseigné, et utilisé, est très loin de l’OO des origines.

Édité par lmghs

+2 -0

Je profite de ce sujet parce que je fais partie des gens qui ont appris la mauvaise manière de faire de la POO (le chat est un animal spécialisé). Si vous avez des ressources sur les bonnes façons de faire, je suis très intéressé. :)

Il y a bien des façons de passer à l’acte. Se taire en est une. Attribué à Jean-Bertrand Pontalis

+3 -0

Je profite de ce sujet parce que je fais partie des gens qui ont appris la mauvaise manière de faire de la POO (le chat est un animal spécialisé). Si vous avez des ressources sur les bonnes façons de faire, je suis très intéressé. :)

Gabbro

C’est tout au fond de la bibliothèque, à droite dans la petite armoire : https://zestedesavoir.com/forums/sujet/447/javaquarium/ (oui 5–6 ans déjà :p ).

Edit : juste pour préciser, informaticienzero partage des liens à la page 2 et l’intérêt c’est que beaucoup de gens ont appliqué le pattern ECS pour résoudre l’exercice.

Édité par Yarflam

Tant de choses, tant de vies, tant de possibilités.

+3 -1

Merci @Yarflam. Je vois que c’est beaucoup de choses sur l’ECS. Mais quid du reste ? De ce que je comprends de la discussion, les traits, c’est autre chose ?

(Désolé si je HS, ça fait littéralement des années que je me dis qu’il faut que j’étudie sérieusement le sujet, et je reporte toujours à plus tard, alors j’en profite).

Il y a bien des façons de passer à l’acte. Se taire en est une. Attribué à Jean-Bertrand Pontalis

+1 -0

Je profite de ce sujet parce que je fais partie des gens qui ont appris la mauvaise manière de faire de la POO (le chat est un animal spécialisé). Si vous avez des ressources sur les bonnes façons de faire, je suis très intéressé. :)

Gabbro

En restant pur OO — le javaquarium est un excellent exo qui montre vite les faiblesses de l’OO, même si on peut le résoudre dans un cadre OO — je dirais:

  • J’avais trouvé que Design Patterns: Tête la première partait dans une excellente direction dans ses premiers chapitres — je n’ai pas lu la suite.
  • Sur divers points le tuto C++ de ZdS (dans la version béta seulement pour l’instant), pose des bonnes pratiques sans pour autant sortir des gros mots (SOLID, Déméter…) dans mes souvenirs. C’est de l’initiation qui suit des bonnes pratiques, pas de la référence sur les bonnes pratiques, etc.
+3 -0
Auteur du sujet

Merci pour toutes vos réponses, ça m’a permis de remettre un peu le truc en perspective (et de me dire qu’il faudrait que j’aie explorer un autre langage pour voir comment c’est fait au delà de C++/Java/python). Faudrait aussi que je m’intéresse sérieusement1 à la programmation fonctionnelle un de ces jours: j’ai bien compris qu’on pouvais faire "les deux en même temps", mais au delà du "paradigme" map/reduce (et de ces petits copains, genre all, any, …) j’en connais pas trop.2


  1. C’est à dire pas pour le plaisir d’en faire, mais pour comprendre la philosophie derrière, comme la POO. Même si vous avez réussi à me faire douter de mon cours de POO, du coup.
  2. À part JS, qui t’invite fortement à mettre des lambda function partout, de ces temps-ci.

#JeSuisToujoursArius • Doctorant et assistant en chimiedev' à temps partiel (co-réalisateur ZEP-12, recherche et template LaTeX)

+0 -0

Tiens, je vais faire presque du HS, mais puisque plusieurs personnes parlent de Trait ici (rust, etc), je ne connais que les Trait de scala et pour moi ils ont le même comportement que celui qu’a les interfaces (en Java du moins).

Du coup vous m’avez mis un doute, il y a une vrai différence entre Trait et Interface ?

+1 -0
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