mardi 1 juillet 2008

Grandeur et décadence de l'objet 1/2


Où je raconte encore ma vie

Les quelques (rares) lecteurs qui fréquentent cet endroit doivent se dire que ce mec n'a rien à raconter à part ses sorties ciné et ses soirées télé.
C'est pas faux mais de temps en temps comme tout un chacun, j'aime aussi penser et réfléchir à des choses moins terre-à-terre.
Jusqu'à tout récemment, tant professionnellement que personnellement, j'avais complètement laissé tomber en friche ma culture de langages informatiques jusqu'à ce que deux événements sans aucun rapport entre eux surviennent à quelques jours d'intervalle.

D'abord, je suis tombé sans le chercher précisément sur un numéro hors-série du magazine Tangente qui traitait des nombres et parmi eux, deux nombres rigolos, petit oméga et Grand Oméga (voir ce lien).
Pour faire simple, sachez que ces deux nombres ont un rapport avec la probabilité d'un programme s'arrête ou boucle indéfiniment.
Dans l'article, l'auteur expliquait que ces deux nombres ne pouvait être calculés, c'est à dire qu'il n'existe aucun programme qui permette d'en donner une suite plus ou moins longue de décimales (alors que pi ou racine de 2 peuvent être calculés comme on le verra dans un prochain article)

J'ai trouvé cet article très intéressant d'autant plus qu'il éveilla en moi un vieux souvenir d'informatique. A ce niveau, un petit aparté s'impose. Quand je dis informatique, je parle de la science qui étudie les algorithmes et les structures de données. Comme le disait d'ailleurs Edsger Dijkstra : « L'informatique n'est pas plus la science des ordinateurs que l'astronomie n'est celle des télescopes. »

Ce souvenir avait rapport avec la calculabilité et la décidabilité, deux notions étudiées dans le cadre d'une auto-formation à la Théorie de la complexité de Kolmogorov.
L'informatique, à ce niveau d'abstraction là, ne peut supporter que des langages très abstraits. Le premier de ces langages est une sorte d'assembleur conçu pour une machine très simple : la machine de Turing. Le second langage (qui est équivalent au premier) s'appelle le lambda-calcul.
Je reviendrai sur ces deux choses étranges plus tard.

Le second événement a été un message d'un contributeur du Scriptorium qui, cherchant à illustrer la notion de paradigme, prit l'exemple des langages informatiques et commença d'en citer quelque uns.

Les deux événements furent à la source d'une association d'idées qui aboutit à une sorte de petite révélation (faut pas exagérer non plus). J'allais me remettre à m'intéresser aux langages informatiques.

La popularité des langages
Après quelques heures de surf sur l'Internet pour avoir un état de l'art des langages utilisés, je tombe sur cette page : paf !

On peut voir que Java tient le haut du pavé, suivi par C et C++.
On peut voir que les langages à objets et les langages procéduraux sont les plus plébiscités (par rapport aux langages fonctionnels et aux langages logiques)
On peut aussi voir que les langages à typage statique sont préférés aux langages à typage dynamique.
Là, le lecteur de Pougues-les-Eaux est complètement largué et se dit qu'il va retourner lire les vieux articles sur la Nouvelle Star. Non, restez, je vous prie, j'explique.

Tout d'abord, il est facile de comprendre pourquoi les langages à objets et procéduraux d'une part et les les langages à typage statique d'autre part tiennent le haut du pavé.
Parque que Java, C++ et C qui représentent 45% du total de la liste des langages sont des langages à typage statique et orientés objets (beaucoup trop pour Java, juste assez pour C++ et peu pour C)
Là, les lecteurs qui sont des intégristes de la profession s'insurgent : "Comment, ce voyou met sur le même pied Java, C et C++ et il s'en moque ! C'est n'importe quoi, tout le monde sait que Java (ou C ou C++, rayez la mention inutile) est le meilleur langage du monde !"

A ce stade du débat, afin de calmer les esprits avant de tailler dans le vif plus encore, une petite digression factuelle et objective s'impose.

Qu'est-ce qui caractérise un langage orienté objets ?
Il faut savoir qu'un objet est un être abstrait qui se compose de trois choses : une identité, un état et un comportement.
L'identité dit que chaque objet est unique (c'est un peu à l'objet ce que le numéro de sécurité sociale est à l'administration, une manière de distinguer les individus des autres)
L'état de l'objet est, plus encore que son identité, ce qui fait l'objet (on vous appelle plus souvent par votre prénom ou votre nom que par votre numéro de SS)
Le comportement est, plus encore que son état, ce qui distingue encore l'objet (bien qu'Einstein et moi ayons en commun une moustache et les cheveux en bataille, je n'ai jamais contribué de manière significative à l'avancée de la Physique)

Un langage orienté objets permet la manipulation de variables qui sont des objets.
Et, dans ce cadre, le C est bien un langage orienté objets:
C'est comme ça que, personnellement, je jouais à "faire de l'objet" avec le vieux compilateur C de Borland. Et c'est comme ça que je continue à faire un peu d'objet avec Perl (mais beaucoup plus de facilité)

Il y a d'autres choses qui font qu'un langage orienté objets est digne, d'après les gourous de l'objet, d'être considéré comme un "bon" langage à objets.
Le langage doit supporter l'encapsulation (c'est dire qu'on ne doit pas pouvoir fouiller l'intérieur des objets, accéder ou modifier l'état de ceux-ci)
Le langage doit supporter l'héritage (c'est dire qu'un objet est une sorte d'autre objet quand ils partagent un état et un comportement commun)
Le langage doit supporter le polymorphisme (c'est dire que celui qui manipule un objet n'en connaît pas la nature exacte a priori mais fait comme si)

Vertu cardinale de l'objet : l'Encapsulation
L'encapsulation empêche le code principale d'accéder directement aux variables qui constituent l'état de l'objet

#include

class PERSONNE
{
public:
char* nom;
private:
char* prenom;
};

int main(void)
{
PERSONNE mezigue;
char nom[] = "SandChaser";
char prenom[] = "";
mezigue.nom = nom;
mezigue.prenom = prenom;
}

Le compilateur (g++ dans mon cas) râle parce que le membre prenom est déclaré comme privé.

La raison d'être de l'encapsulation est le respect de la cohérence interne des objets.
Par exemple, tous le monde sait que le numéro de SS contient une indication sur le sexe de la personne immatriculée.
Imaginons un instant un objet dont l'état est composé d'un numéro de SS et d'un booléen (masculin/feminin) représentant le sexe.
Si les membres de cet objet étaient publics, n'importe qui pourrait modifier sexe et numéro de SS de façon indépendante au risque d'arriver à des incohérences.

Vertu cardinale de l'objet : l'Héritage
L'héritage est une sorte de spécialisation du rôle des objets. Admettons que l'on a un objet CONVERTER qui réalise une conversion d'un texte mis en page avec LATEX vers Postscript.
Si les spécifications de LATEX ou de Postscript viennent à changer, il faudrait modifier notre objet. Ca prend du temps et la nouvelle version ne serait plus compatible pour les anciens formats.
Qu'à cela ne tienne, spécialisons notre objet en disant qu'il est une spécialisation de CONVERTER:

#include

class CONVERTER {
void* convert(char*);
};

class CONVERTER_V2 : CONVERTER {
void* convert(char*);
CONVERTER* get_compatible(void);
};

Un client de notre objet utilisera la nouvelle version et appellera la fonction get_compatible pour objetnir un CONVERTER ancien modèle.

La raison d'être de l'héritage est de factoriser le code pour le réutiliser dans des cas spécifique sans avoir besoin de le recoder.

Vertu cardinale de l'objet : le Polymorphisme
Cet exemple du CONVERTER va aussi me permettre d'aborder le polymorphisme. Le client de notre CONVERTER doit encore faire lui-même le choix du type d'objet à créer en fonction de ce qu'il reçoit et de qu'il veut obtenir.
Imaginons que le CONVERTER doive aussi produire du flux PDF à partir du texte LATEX, nous pourrions créer un autre objet PDF_CONVERTER et renommer les deux premiers en PS_CONVERTER

class PS_CONVERTER
{
public:
void* convert(char*);
};

class PS_CONVERTER_V2 : PS_CONVERTER
{
public:
void* convert(char*);
PS_CONVERTER* get_compatible(void);
};

class PDF_CONVERTER
{
public:
void* convert(char*);
void set_pdf_header(void*);
};

int main(void)
{
int c;
void * pConverter;
switch(c) {
case 0 : pConverter = new PS_CONVERTER_V2; break;
case 1 : pConverter = new PDF_CONVERTER; break;
default : return(1);
}
/* utiliser le pConverter */

switch(c) {
case 0 : ((PS_CONVERTER*)pConverter)->convert(0); break;
case 1 : ((PDF_CONVERTER*)pConverter)->convert(0); break;
default : return(1);
}

}

A chaque fois qu'on est amener à se poser la question de savoir quel type de document je veux en sortie, il faudra utiliser un switch et du type cast (bouh que c'est laid !).
Heureusement que le polymorphisme peut changer cela :

class CONVERTER // classe qui spécifie une interface de commande avec deux fonctions
{
public:
void* convert(char*);
void set_header(void*);
}

/*
chaque classe de convertisseur hérite maintenant des deux fonctions de l'interface CONVERTER
*/
class PS_CONVERTER : CONVERTER
{
public:
void* convert(char*);
};

class PS_CONVERTER_V2 : PS_CONVERTER
{
public:
void* convert(char*);
PS_CONVERTER* get_compatible(void);
};

class PDF_CONVERTER : CONVERTER
{
public:
void* convert(char*);
void set_header(void*);
};

int main(void)
{
int c;
CONVERTER * pConverter;
switch(c) { // on choisit ici une bonne fois pour toutes quel type de convertisseur on veut
case 0 : pConverter = new PS_CONVERTER_V2; break;
case 1 : pConverter = new PDF_CONVERTER; break;
default : return(1);
}
/* on utilise pConverter sans avoir à se demander de quelle sorte il est*/
pConverter->convert(0); // l'appel à convert est polymorphique !
// jouez chez vous à changer la valeur de c entre 0 ou 1
// implémentez les fonctions des objets et constatez que c'est la bonne fonction
// qui est appelée dans chaque cas.

}

La raison d'être du polymorphisme est de permettre à l'utilisateur d'un objet de lui envoyer des messages (en exécutant des fonctions) sans avoir à faire des hypothèses (parfois fausses) sur la nature réelle de cet objet.

Grandeur des langages à objets
Les langages à objets sont très à la mode depuis que Java a vu le jour. Il n'en était pas de même avant ces années fastueuses où l'on ne jurait que par le procédural.
Les langages à objets ont popularisé l'approche objet (union identité, état, comportement + encapsulation + héritage + polymorphisme) grâce à leur syntaxe adaptée.
Il est possible de programmer en style objet en C ( on vient de le voir) et en Pascal. On améliore ainsi la modularité du code au détriment de sa lisibilité.

La finalité de l'approche objet est de capturer dans le code de meilleures abstraction des concepts réels (par la définition des objets par identité+état+comportement), modulaires et complètes. L'encapsulation a pour finalité d'éviter l'accès à distance de l'état de l'objet qui a pour effet pernicieux de produire des incohérences d'état et, par conséquent, des bugs.
L'héritage a pour finalités la réutilisation du code et la composition du comportement (ces deux choses ne sont pas l'apanage de l'approche objet d'ailleurs, contrairement à ce que beaucoup disent)
Enfin, le polymorphisme aide l'héritage en garantissant que le bon comportement sera déclenché même si l'utilisateur ne sait pas quelle est la nature précise de l'objet qu'il manipule.

Mais les langages à objets ne sont pas exempts de défauts. En fait, la plupart de gens font de l'objet parce que :
1) c'est à la mode (donneur d'ordre, architecte)
2) le chef leur a dit de faire (développeur de base)
3) ils ne connaissent que ça (architecte, développeur de base)
4) il leur serait indigne de ne serait-ce penser qu'il puisse exister d'autres paradigmes (gourou)

Sur ces bonnes paroles, à la semaine prochaine, on verra pourquoi le tout objet est idiot.

Aucun commentaire:

Enregistrer un commentaire