Notes sur la programmation orientée objets

Dans la plupart des écoles d’informatique, il y a un cours qui s’appelle généralement POO. S’il ne s’apelle pas comme ça, il s’appelle alors plus honnêtement Java ou C♯. On y apprend alors généralement que le Java, le C♯ ou le C++ sont les langages dit orientés objets. Certains profs pourrons même aller jusqu’à donner une définition d’un objet en disant que c’est un ensemble de données (comme les structures en C) et de méthodes de traitement sur ces données.

Sauf que partant de cette définition, on peut parfaitement définir le C comme un langage orienté objet car il suffit de rajouter à une structure des pointeurs sur fonction, de les assigner lors de l’allocation et hop ! Un objet ! Bon c’est un peu bancal alors certains profs prendront quand-même la peine de préciser que pour faire de l’orienté objet, il faut pouvoir faire de l’héritge d’objets, bla bla bla…

Mais il faut se rendre à l’évidence : Java, C++, C♯ ne sont pas vraiment des langages orientés objets.

Qu’est-ce que la POO ?

Traditionnellement, on nous apprend que la programmation orientée objets, c’est une manière de programmer selon laquelle on travaille avant tout avec des objets que l’on peut grossièrement résumer comme un ensemble de données (des chiffres, des chaines de caractères, etc.) et de traitement sur ces données (les fonctions que l’on appelle des méthodes). La subtilité, par rapport à un langage comme le C, c’est que les fonctions du C nécessitent toujours que l’on précise les données sur lesquelles travailler en les passant en paramètre. Dans le cas d’un objet, ce n’est pas nécessaire, car les méthodes sont en quelque sorte contenues dans l’objet et peuvent accéder aux données sans avoir besoin de les passer en paramètre. Ainsi, pour convertir un entier en une chaine, je dois écrire en C :

int number = 42;
char str[15];
sprintf(str, "%d", number);

En java, j’écrirais :

Integer number = new Integer(42);
String str = number.toString();

La méthode

toString()

n’a pas besoin d’argument car elle a déjà accès à la donnée car elle est contenue dans l’objet.

Pour construire des objets, on définit alors une classe qui possède un méthode spéciale : le constructeur. La classe est comme une sorte de plan d’architecture sur la base de laquelle on va construire un objet qui sera différent d’un autre objet construit sur la base de la même classe.

Cette façon de voir la POO est née avec le succès du langage Java qui reprenait dans son lexique la notion d’objets pour désigner ce qui est produit à partir d’un classe. La classe de base de Java est d’ailleurs la classe

Object

et toute classe en hérite implicitement. Cette conception n’est pas fondamentalement fausse mais représente une vision tronquée du concept original imaginé par Alan Kay dans les années 1970.

La vision d’Alan Kay

Alan Kay est souvent considéré comme l’inventeur de la POO. Mais comme toujours dans les sciences, une invention ne jaillit jamais d’elle-même du cerveau d’un seul inventeur brillant et les travaux d’Alan Kay comme tous les travaux de l’histoire de l’humanité, reposent en partie sur ceux de prédécesseurs. C’est pourquoi vous pourrez sûrement voir sur le net des gens clamer que Kay n’est pas l’inventeur original du concept, que c’est quelqu’un d’autre, etc. mais ce point ne m’intéresse pas.

Dans la vision originale d’Alan Kay, la programmation orientée objet consiste tout simplement en une méthode de développement dans laquelle on ne considère plus les données elle-mêmes mais des objets. Tout est objet en POO et un objet est un concept, une brique logicielle. Ça peut-être un ensemble de données rassemblées dans un conteneur, comme en Java, mais ça peut-être n’importe quoi de beaucoup plus large. On peut considérer ainsi un programme installé sur un serveur que l’on interroge en RPC comme un objet. Ce qui se cache derrière la définition d’objet et comment on le représente n’a aucune importance puisque l’on parle d’un concept. Il s’agit de n’importe quelle brique logicielle.

Ces objets s’échangent des messages. Encore une fois, peu importe la méthode d’échange des message et leur contenu, puisqu’il s’agit d’un concept. On peut donc considérer que lorsque vous avez un script en JS qui envoie un message AJAX à serveur, vous faites de la POO : le serveur est un objet, votre script en est un autre et ces deux objets communiquent par l’envoi de messages.

C’est surtout sur ce point que Kay insistera après le succès de C++ et de Java lorsqu’il dira que ces langages ne sont pas ce qu’il avait en tête lorsqu’il a donné une formalisation de la POO. Si, par la suite les développeurs ont surtout retenu la notion d’objets, l’idée importante que lui avait était la notion de messages.

Et c’est là que le bât blesse : Java et C++ ne sont pas des langages orientés objets selon la définition d’Alan Kay. Ces deux langages ont effectivement quelques capacités de POO mais passent complètement à côté de l’idée importante de messages. En effet, ces deux langages continuent de se trimbaler une distinction nette entre les types naturels (

int

 ,

char

 , etc.) et les objets issus de classe. Et les types naturels ne sont pas des objets auxquels ont peu passer des messages.

La conséquence principale de cette oblitération de la notion de message est que la seule manière de passer des messages en POO moderne est l’appel de méthode et c’est un peu dommage car c’est une vision très réductrice du processus. Ceux qui ont déjà fait de la programmation évènementielle en Java savent à quel point implémenter un pattern observeur/observable est chiant à mourir et il est impossible de faire naturellement de l’appel de callback. C’est une vision d’autant plus réductrice qu’en Java et en C++, l’appel d’une fonction oblige à passer des objets d’un certain type ce qui fait que l’on se retrouve le plus souvent à faire des conversion dégueulasses avant l’appel de fonction. Ils ont bien tenté de résoudre le problème en implémentant les méthodes génériques, mais ça n’a fait que complexifier la syntaxe en la rendant hardore :

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) {
        c.add(o); // compile-time error
    }
}

La signature de la méthode indique qu’on s’en tape un peu du type d’objet qu’on passe, du moment que quelque part dans la hiérachie d’héritage on retrouve un objet qui implémente l’interface

Collection

. C’est un écueil que j’ai d’ailleurs déjà eu l’occasion de dénoncer, l’invasion des interfaces en Java. Alors que finalement, l’interface n’a absoluement aucune autre utilité que de s’assurer qu’un objet implémente bien une méthode…

Python et Groovy font les choses bien de ce côté-là en privilégiant l’approche duck-typing. Cette approche est la suivante : si ça caquette comme un canard, que ça ressemble à un canard, alors c’est un canard. L’idée est simple : lors du passge de message, il n’y a pas de vérification du type de l’objet parce qu’on s’en fout, après tout. Tout ce qu’on lui demande, c’est d’avoir la méthode qui va bien, c’est à dire, d’avoir l’air d’un canard.

Groovy s’en sort de manière très gracieuse sur ce point : lorsqu’une méthode d’un objet est appelée, le langage vérifie juste que la méthode existe quelque part dans la hiérarchie d’héritage. Si elle n’existe pas, alors une exception

MissingMethodException

est jetée. En Python, c’est le même principe sauf que l’erreur s’appelle

NotImplementedError

.

En fait, la forme la plus pure de POO que l’on peut trouver dans les langages classiquement cités (C++, C, Java, JavaScript, etc.) est l’approche signaux/slots que l’on retrouve dans Qt. Le principe est simple : une classe définit des types d’evènement qui peuvent survenir, sous la forme d’une signature de méthode (un signal) ainsi des traitements qui seront effectués pour réagir à l’évènement (un slot) :

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

Après avoir connecté un slot d’un objet au signal d’un autre (un pattern observeur/observable) je peux alors passer des messages à pleins d’objets à la fois. Et la syntaxe est d’ailleurs très expressive :

emit valueChanged(value);

J’emet un message.

Le C♯ est le seul langage où j’ai pu retrouver un mécanisme de ce type avec les event handlers.

Les conséquences

Le problème d’avoir oublié l’aspect passage de messages de l’idée originale d’Alan Kay est que les langages de POO à succès d’aujourd’hui se retrouvent avec des problèmes insolubles en temps normal et qui avaient été résolus par l’approche qu’Alan Kay avait mise en œuvre dans SmallTalk. En souhaitant résoudre ces problèmes, les concepteurs de langages (Java en tête) les cabossent au point de les rendre attrocement complexes (comme nous l’avons vu au-dessus avec la solution des méthodes génériques). Le problème de s’être concentrés sur l’approche objets plutôt que l’approche messages est que, très vite, ils ont beaucoup trop insisté sur l’idée de type et d’héritage. À force de vouloir empêcher le développeur de faire des erreurs en l’obligeant à définir et respecter ses types, ils ont perdu de vue qu’ils empêchaient leur langage de résoudre élégemment des problèmes de programmation plus exotiques. Lorsqu’ils s’en sont rendus compte, ils ont à nouveau choisi le mauvais chemin avec la notion d’interface allant jusqu’à se dire qu’elle se suffisait à elle-même et que l’héritage, finalement, c’était surfait.

Mais c’est oublier une règle fondamentable de la programmation : ce n’est pas au langage de décider ce que le programmeur peut ou ne peut pas faire, mais au programmeur d’être conscient de ce qu’il fait, et de ce que ça implique.

Déjà 7 avis pertinents dans Notes sur la programmation orientée objets

  • cabernet138
    « d’avoir oublié l’aspect passage de messages de l’idée originale d’Alan Key »

    Si cette idée de message était si fondamentale au départ, pourquoi les fondateurs eux-même n’ont pas parlé de « programmation par message » mais bien de « object oriented programming » ?

    Simple question.

  • Cascador
    On fait tous des erreurs, c’est comme ça qu’on progresse et puis c’est de l’inattention rien de gravissime.

Les commentaires sont fermés.