Pensées sur le modèle OO

Les réflexions qui suivent tiennent évidemment de ma perception de ce que signifie être OO.

L'un des informaticiens les plus célèbres de l'histoire de cette jeune science, Edsger W. Dijkstra, aurait envoyé à propos de l'approche OO :

« Object-oriented programming is an exceptionally bad idea which could only have originated in California »

Je n'ai pas la citation originale, alors ça pourrait n'être qu'une rumeur, mais elle est divertissante. Merci à mon collègue et ami Vincent Echelard pour cette perle.

Le modèle orienté objet (OO) est un modèle intéressant à plusieurs égards, mais il n'est évidemment pas parfait (quel modèle le serait?), et il faut réfléchir à certaines de ses carences. Étrange, sans doute, pour des un site Web s'intéressant autant à la POO que celui-ci que de mettre en relief que la POO est une approche valable, pas la seule approche valable, pour plusieurs aborder problèmes.

Dans bien des cas, la POO s'inscrit dans un amalgame complexe de stratégies et permet de faciliter la définition de schémas de conception, d'algorithmes génériques, d'idiomes particuliers et ainsi de suite. Nous ne voudrions sans doute pas revenir en arrière, vers une époque où la POO était à la fois peu connue et peu comprise, mais avec la maturité vient le recul. Mieux vaut comprendre les forces et les faiblesses d'une approche pour l'utiliser au maximum de son potentiel que de se faire croire que le marteau doré[1] a enfin été trouvé.

La POO est complexe à définir de manière universelle

Il est difficile de définir ce que signifie être OO de manière satisfaisante pour tous les intervenants impliqués dans le domaine et intéressés par le sujet. La publicité est imposante, les intérêts commerciaux sont omniprésents et les dogmes s'entrechoquent fréquemment.

L'article Que signifie être OO? propose un tour d'horizon des caractéristiques propres à divers modèles OO, car il y en a plusieurs, et discute (de manière succincte) de certains des tenants et aboutissants des divers choix philosophiques possibles. Malgré les meilleurs efforts des penseurs, définir ce que signifie être OO entraîne inévitablement des guerres de clochers. La réponse des uns comme des autres tend à être : être OO, c'est faire ce que mon langage préféré (ou ma plateforme favorite) permet de faire. Exprimé autrement : « les lacunes de mon modèle de prédilection sont moins importantes que ses forces ».

Selon le poids accordé aux diverses caractéristiques jugées importantes ou essentielles au modèle OO par une plateforme ou un langage donné, le modèle OO résultant offrira des avantages et des inconvénients différents. La question est donc guidée par les besoins d'entités commerciales et de groupes d'intérêt soucieux de voir leur manière de pensée (et la couverture de leurs besoins propres) dominer le portrait et prendre force de loi[2].

Le principe SOLID

Plusieurs associent l'approche OO non pas à des concepts langagiers mais bien à un groupe de « principes de sain design ». Ce groupe de principes est associé à l'acronyme anglais SOLID, un acrostiche pour les principes suivant :

La difficulté de déterminer ce qui doit être considéré essentiel ou fondamental au modèle OO fait partie du groupe de facteurs qui rendent difficiles la conception de SGBD OO vraiment universels, et qui compliquent la conception de métalangages capables de rejoindre l'idée d'objet de manière telle qu'ils pourraient servir de pont technologique entre les diverses philosophies.

La POO ne propose pas toujours la meilleure approche

Pris au sens strict, le modèle OO ne se prête pas à certaines opérations purement algorithmiques et dissociées de toute subjectivité. Une opération comme min(a,b), par exemple, est plus une fonction appliquée aux paramètres a et b qu'une méthode de a ou de b. Exprimé autrement : c'est une opération applicable à deux objets, plus qu'une opération qu'un objet appliquerait sur l'autre. Évidemment, je parle ici de l'acception classique de min(a,b); il est bien sûr possible d'exprimer cette idée sous forme de méthode d'instance, mais plusieurs seraient d'avis que cela manquerait... d'élégance (quelque chose comme a.minAvec(b) peut-être? La difficulté de nommer l'opération est sans doute la manifestation d'un problème conceptuel plus profond).

Les langages se voulant si purement OO qu'ils ne permettent pas même de créer des fonctions globales doivent appliquer des rustines (des patches) à leur modèle conceptuel, souvent par l'emploi de méthodes de classe un peu bidon, pour exprimer des idées aussi simples. Dans une expression comme Math.min(a,b), l'utilité de Math en tant que classe est discutable[3] – entre autres, est-ce vrai que l'algorithme min() est de nature strictement mathématique? Ne s'agit-il pas d'un algorithme plus général, qui pourrait permettre entre autres de comparer des chaînes de caractères?

POO et découvrabilité

Un de mes anciens étudiants, Pierre Boyer, a relevé, dans son essai de maîtrise, que l'approche OO est souvent moins intuitive qu'une approche procédurale pour des programmeuses débutantes ou des programmeurs débutants. En effet, le monde est peuplé d'objets inanimés sur lesquels les individus opèrent; l'approche OO tend à nous faire penser ces objets en terme d'entités actives (à la limite autonomes) avec lesquels nous dialoguons.

Admettons qu'il paraît plus simple, pour la majorité des gens, de penser tourner les pages du journal que de demander poliment au journal de tourner ses propres pages.

En retour, pour les IDE, l'approche OO tend à simplifier la présentations d'options pour compléter le code : en associant des services à un type, il est plus simple pour l'outil de trouver les options possibles à un point donné dans l'écriture d'un programme, ce qui peut aider à la productivité de celles et ceux qui ont choisi le type dont elles ou ils pensent avoir besoin (pensez à ces outils qui, lorsqu'on suit un nom d'objet d'un '.', proposent automatiquement les membres accessibles de cet objet).

La POO ne se prête pas toujours au format de documentation le plus utile

Le fait que le modèle OO demande une adaptation dans le mode de pensée des gens venant d'un monde structuré implique aussi une modification des modes de recherche de ces gens. La documentation générée par des produits comme Javadoc ou Doxygen se présente sous une forme « classe par classe », et demande donc de réfléchir d'abord à qui, puis à quoi.

Dans certains cas, ce saut se fait simplement. En Java par exemple, plusieurs penseront d'abord à la classe String pour chercher la liste des constructeurs en quête d'un constructeur permettant, par exemple, de créer une chaîne contenant 50 fois le symbole '*'.

Quand, par contre, le quoi n'est pas évident, la question de savoir chercher efficacement la bonne classe ou la bonne méthode devient une question beaucoup plus difficile, s'adressant en fait d'abord à des initié(e)s du modèle OO et du langage choisi.

Des cas en apparence simples comme chercher la méthode permettant de concaténer deux chaînes en Java ou en C# demandent des connaissances de niveau méta quant au langage. Par exemple, il faut savoir que la classe String y est immuable, et que la concaténation demande de créer de nouvelles chaînes ou d'utiliser une autre classe (StringBuffer), au choix. Même pour des informaticien(ne)s d'expérience, ce bond est complexe et chargé de culture locale. Le monde .NET n'a pas fait mieux, reprenant la même forme avec les classes string et System.Text.StringBuilder.

Une carence fondamentale

Les modèles préconisés par les plateformes Java et .NET souffent tous deux d'une carence de fond : l'incapacité de créer de réelles instances constantes. Ceci implique qu'il est impossible pour un objet de faire quelque chose d'aussi simple que de retourner l'un de ses attributs – si cet attribut est une instance d'une classe quelconque – à travers l'une de ses méthodes sans causer un bris d'encapsulation, l'objet étant incapable d'empêcher l'appelant de la méthode de modifier l'objet retourné. Les constantes constituent un élément on ne peut plus important d'un véritable langage OO.

Il est possible de contourner cette carence :

Les deux premières options impliquent un accroissement massif du nombre d'appels aux mécanismes d'allocation dynamique de mémoire, et sont donc lents (en fait, la première peut être rapide mais demande un important effort de développement). La troisième est carrément dangereuse et ne peut être utilisée qu'en milieu fermé – et encore!

Il est triste (mais compréhensible) que la notion de constance soit absente au sens des objets dans ces langages, surtout si l'on prend en considération que la protection de constance est relativement simple à insérer dans un langage, si la plateforme sous-jacente le supporte (ce qui est le noeud ici). La validation du respect des contraintes peut presque toujours être ramenée à une vérification locale, réalisable à la compilation, et qu'elle permet à un compilateur de réaliser certaines optimisations supplémentaires. Voir cet article pour plus de détails.

Dans des cas où l'équipe de développement cherche à réaliser des opérations, la documentation basée classe de la POO typique tend à forcer la recherche sur une base structurelle, à partir du nom de la classe alors qu'en fait, c'est souvent là l'inconnue.

À tout hasard, illustrez par vous-mêmes cette situation en examinant l'aide en ligne standard de Java (qui est en soi un excellent outil pour la majorité des cas) en cherchant à trouver le bon type de panneau ou le bon type de zone de texte[4] pour permettre l'édition de code colorié. Il y en a plusieurs, et certains sont meilleurs que d'autres, mais la recherche par opération n'est pas naturelle et vous devrez plutôt chercher par nom de classe et par parenté.

Avis aux intéressé(e)s : la recherche dans MSDN pour le même type d'information n'est pas plus simple. Essayez de trouver comment faire en sorte qu'un programme puisse démarrer un programme externe à partir des outils de l'infrastructure .NET... Avez-vous les mêmes intuitions que les concepteurs de cette infrastructure?

La POO dépend d'une relation implicite de confiance

La POO repose sur un contrat de confiance entre les développeurs d'une classe, d'une hiérarchie de classes ou d'une bibliothèque toute entière, et les utilisateurs de ces classes. Cette confiance est, dans la majorité des cas, méritée :

Cela dit, indiquer qu'un objet assure sa pleine et entière intégrité, qu'il garantit le respect de ses invariants, ou encore indiquer qu'il assure la stabilité de ses états du début à la fin de sa vie, n'est pas toujours chose suffisante. Il arrive en effet qu'on veuille tenir compte de l'état initial d'un objet et qu'on se demande ce que représente un état stable pour un objet donné.

Quelques exemples concrets :

  • Un flux nouvellement créé est-il nécessairement ouvert?
  • Une chaîne de caractères par défaut a-t-elle un contenu de taille zéro?
  • Quel est l'état d'une matrice par défaut? S'agit-il d'une matrice plein de zéros ou d'une matrice identité?
  • Devrait-on fermer explicitement chaque fichier ou se fier au destructeur d'un objet représentant ce fichier pour réaliser cette tâche[5]?

La maxime de Scott Meyers, à l'effet qu'il soit sage de rendre simples les opérations saines et difficile les opérations qui ne le sont pas, s'applique ici comme ailleurs. Une bonne interface devrait nous inviter à travailler correctement.

De même, nous devrions examiner les possibilités que nous offre un objet avant de s'en servir : par exemple, si un objet expose une méthode permettant de savoir s'il est vide, nous devrions la préférer à une approche indirecte pour en arriver au même constat (p. ex : vérifier si la taille est zéro), car de manière générale, la présomption du code client devrait être que, si un objet offre un service, alors ce service est probablement au moins aussi bien implémenté à l'interne que ce que nous pourrions faire nous-mêmes de l'extérieur.

Ici comme ailleurs, il y a bien sûr des nuances...

Les questions sont nombreuses et trouvent toutes une réponse valide et (typiquement) unique dans un langage de programmation donné ou pour une bibliothèque donnée. Le problème, surtout pour qui débute en POO, est que chaque réponse est en grande partie culturelle, et que l'état initial, tout comme les actions finales d'ailleurs, attendus ou présumés d'un objet donné sont en général facilement devinés par les programmeurs OO d'expérience, mais le sont beaucoup moins facilement par les programmeurs OO occasionnels ou récents.

L'informaticien(ne) doit faire confiance aux objets, mais pour ce faire il lui faut les comprendre et bien saisir leurs rôles et responsabilités. Avec l'encapsulation stricte (et avec l'héritage, le polymorphisme, l'abstraction et ainsi de suite), la véritable confiance ne se peut qu'à l'aide d'une documentation très claire.

Une erreur dans l'utilisation d'objets clés[6] peut mener à des problèmes importants. Une classe exigeant que ses ressources soient libérées explicitement, comme c'est souvent le cas avec des objets .NET ou Java[7], mènera à des programmes dont l'exécution entraîne une dégradation des ressources disponibles si un programmeur se trompe dans sa compréhension du comportement attendu de ses instances.

Documenter rigoureusement les méthodes clés d'un objet diminue le niveau d'incertitude dans chaque cas, mais on souhaite que l'utilisation d'objets soit maximalement intuitive. C'est là un but souhaitable mais qui n'est qu'approchable, pas atteignable, dans l'absolu.

La POO est plus riche que l'usage qu'on tend à en faire

Les informaticien(ne)s tendent à avoir recours à des recettes qui ont fait leurs preuves. Les schémas de conception et les idiomes de programmation sont des illustrations claires de la valeur intrinsèque de cette habitude. Comme le diraient les anglophones : If it ain't broke, why fix it?

Comme dans toutes les approches de programmation, cela dit, la créativité et l'innovation ont leur place, et une place importante par-dessus le marché. Il existe souvent plusieurs manières de résoudre un problème, de concevoir une solution, de rédiger une classe ou un algorithme.

L'enseignement de la POO, surtout en début de parcours, tend à proposer les recettes, les façons de faire éprouvées. L'industrie s'attend, chez l'informaticien(ne) en début de carrière, à un vocabulaire qui comprend ces recettes et s'attend à ce que ces informaticien(ne)s soient capables de les appliquer. L'informaticienne moyenne et l'informaticien moyen comprendront ces recettes et les mots par lesquels elles seront exprimées. Faciliter la communication et la compréhension est productif.

On sous-utilise la POO. Trop peu nombreuses et trop peu nombreux sont celles et ceux qui ont le réflexe de penser à plusieurs petits objets collaborant entre eux plutôt qu'à des hiérarchies un peu trop chargées de classes qui accumulent un bagage trop lourd pour leurs besoins.

Plus une classe fait de choses et moins elle est utile, du fait qu'elle est alors fortement associée à un domaine spécifique et, par conséquent, utilisable dans un moins vaste éventail de champs d'application. Une bonne classe, en général, fait peu de choses, mais ce qu'elle fait, elle le fait bien.

L'objet idéal est petit, souvent même vide, et représente un concept clair et univoque. L'héritage et le polymorphisme sont des outils puissants et importants, mais l'association, la composition et l'agrégation (je dirais même plus : la collaboration!) sont plus importants encore. Penser petit pour faire de grandes choses.

L'équilibre entre pédagogie traditionnelle de l'approche OO (enseigner ce que tout le monde dans l'industrie sait et s'attend à ce que l'on sache) et approche OO saine (penser petit, modulaire, assembler, déterminer les relations) est difficile à trouver. Même dans mes propres notes de cours, malgré ma propre conscientisation quant à cette problématique, les classiques et la tradition me servent encore aujourd'hui de rampe de lancement. Construire, puis déconstruire pour mieux reconstruire. Plusieurs articles sur h-deb déconstruisent dans plusieurs directions, ce qui peut être choquant, être hors normes.

L'approche OO est un outil intellectuel d'une puissance inouïe, mais la créativité qui mène à en tirer le maximum est difficile à enseigner et à proposer en milieu industriel. La résistance est forte pour qui souhaite sortir du cadre.


[1] Celui qui fait en sorte que tous les problèmes ressemblent à des clous. Pour paraphraser Alisdair Meredith dans https://twitter.com/AlisdairMered/status/523628373768028160 : « ...mais quand ce marteau est C++, tous les problèmes ressemblent à des doigts ».

[2] Les choix pédagogiques des professeurs et des maisons d'enseignement teintent aussi, clairement, cette définition.

[3] En Java comme en C#, tous les membres de la classe Math, sans exception, sont des membres de classe... La pertinence de l'introduction d'une classe dans un tel cas se limite au regroupement des opérations et des constantes sous un même chapiteau, mais la classe Math hérite de tous les membres de Object (ou object, selon le langage) son parent, incluant des outils de synchronisation et de sérialisation qui, pour elle, sont superflus. Un paquetage ou un espace de noms suffirait amplement. D'ailleurs, C# offre maintenant ce que ce langage nomme des « classes statiques », qui ne peuvent être instanciées... et qui sont, en pratique, bien plus des espaces nommés que des classes (mais C# ne permet pas de placer des fonctions hors de classes... même si certains classes ne sont alors que des artifices).

[4] ...et je vous aide ici en vous donnant un indice sur les types de contrôles les plus susceptibles de vous être utiles; la recherche est plus ardue quand on ne possède pas au préalable ce genre d'information.

[5] La réponse varie d'un langage à l'autre, ce qui complique l'écriture de bibliothèques accessibles à travers du code client écrit à l'aide de plusieurs langages de programmation distincts.

[6] Ceux représentant les entrées/ sorties ou les ressources externes à un programme sont de bons cas d'étude.

[7] ...à cause de l'emploi de moteurs de collecte automatique d'ordures et, en Java, à cause de l'absence de véritables mécanismes de finalisation.


Valid XHTML 1.0 Transitional

CSS Valide !