Affichage des articles dont le libellé est informatique. Afficher tous les articles
Affichage des articles dont le libellé est informatique. Afficher tous les articles

lundi 8 septembre 2008

Démonologie Informatique 2/2

Il s'est passé quelques semaines depuis le dernier billet sur le sujet, veuillez m'en excuser. J'étais en vacances, sans Internet et avec quelques autres petites choses à faire.

On en était resté à ce qui distingue les langages informatiques entre eux par rapport au fait de déclarer le type des variables. Pour celles et ceusses qui ont la mémoire qui flanche et que le sujet intéresserait, allez .

Je pensais écrire la seconde partie du billet dans la même veine que la première quand un commentaire d'un lecteur m'a fait réagir. Ce commentaire dit en substance : "Comment gérer des structures de données complexes avec un langage dynamique ?"

D'abord, qu'est-ce qu'on entend par structure de données complexe ?
Et d'abord, ce quoi une structure de données ?

En informatique, l'un des problèmes de base qu'un concepteur a à résoudre est celui de la représentation des données. Tout le monde a une idée assez précise de ce que c'est qu'un nombre entier (il en existe même quelques définitions mathématiques assez formelles) mais comment le représente t'on ?

Pour un humain standard et normal (ni informaticien, ni mathématicien), la question ne se pose pas, il écrit le nombre entier avec des chiffres décimaux, c'est ce qu'on appelle la représentation positionnelle en base décimale d'un nombre.
La représentation décimale est tellement courante que l'on est arrivé à confondre l'essence d'un nombre et sa représentation. Mais ces deux notions sont distinctes.

Prenons par exemple l'entier N représenté communément par l'écriture décimale suivante : 10.
N peut aussi s'écrire 5 x 2 ou comme la somme des 4 premiers entiers naturels non nuls : 1+2+3+4. A chaque fois, on parle du même nombre, du même être mais sous des représentations différentes. On peut aller encore plus loin en disant que ce nombre entier N est unique mais qu'il lui est associé une infinité de représentations (ça se démontre facilement en disant qu'un entier N peut être représenté dans toute base de numération p où p est un entier, comme il existe une infinité d'entiers p, N peut être représenté d'une infinité de façons)

Quel rapport avec notre choucroute de structure de données ? Et bien, le voilà : une structure de données est la représentation d'un type.

Dans notre exemple avec l'entier, 10 est une structure de données qui représente de manière numérique le nombre en question de type entier. Le type est entier, le nombre est une réalisation du type, un être si on veut et l'écriture décimale, sa représentation (très efficace pour le calcul manuel ou mental sur les entiers).
Une autre représentation du même être est Dix. Cette représentation est lexicale, dépendante d'une langue nationale. Peu pratique pour le calcul ou même la communication...

Dans le premier cas, on exprime notre élément de type (on dit instance ou réalisation) par une structure de données qui est une séquence de chiffres de 0 à 9.
Dans le second cas, on exprime le même être par une structure de données qui est une séquence de lettres d'un alphabet qui suit les règles de vocabulaire et de syntaxe d'un langage (en l'occurrence, une langue naturelle, le français).

Maintenant, qu'on voit peut-être un peu mieux ce qu'est une structure de données et à quoi ça sert, on va pouvoir répondre à la première question : en quoi une structure de données peut être complexe et si les langages dynamiques sont mieux à même de les gérer.

Avant toute chose, il faut vous prévenir que ce n'est pas parce qu'un langage ne force pas le développeur à utiliser des types que ce langage n'en possède pas.
En Perl (langage à typage dynamique faible), vous ne voyez jamais quelquechose du style my int $n = 10;
Et pourtant, écrire quelque chose comme my $n = 1+2+3+4; printf "%d\n",$n; donnera bien 10.
Perl possède bien en interne un type Entier mais il décharge le développeur du souci de s'en préoccuper en inférant le type des variables à l'exécution. En clair, Perl contient un petit démon qui regarde l'intérieur des boîtes pour mettre tout le temps la bonne étiquette dessus.

Et nos structures complexes alors ? Et bien, on s'en fiche. Pouvoir créer des structures complexes n'a rien à voir avec les types supportés d'un langage mais avec sa syntaxe.
Quand on parle de structure de données complexes, on parle en fait de représentations suffisamment sophistiquées pour représenter des types de plus en plus abstraits.

Représenter un entier est une chose, représenter un une carte géographique et un itinéraire dans cette carte (comme Google Maps le fait) en est une autre.

Prenons une structure de données sophistiquées, la liste chaînée. Il s'agit d'une collection de variables dont on accède aux valeurs par itération de proche en proche depuis la tête de liste jusqu'à la fin.

Son expression dans un langage informatique ne dépend aucunement de la capacité dudit langage à gérer le typage de manière forte ou faible, statique ou dynamique mais uniquement de sa capacité à gérer des liens entre variables (qu'on les appelle pointeurs ou références) donc de la syntaxe.
Exemples : C (fort, statique) possède des pointeurs, on peut y définir une structure de liste chaînée. Perl (faible, dynamique) permet de gérer des références à des variables, on peut y définir une structure de liste chaînée (c'est idiot car Perl les supporte de manière native), Lisp (pas de typage) est entièrement basé sur la structure de liste (un programme Lisp est une liste). A contrario, PL-SQL (fort, statique) ne supporte ni pointeurs, ni références et n'autorise pas la définition de telles structures.

En conclusion, la capacité d'un langage informatique à gérer des représentation sophistiquées des données dépend de la puissance et de l'expressivité de sa syntaxe.
Le typage n'intervient que dans le contrôle de la syntaxe pour assurer une exécution correcte.

A noter que beaucoup de langages dynamiques possèdent une syntaxe puissante et expressive à la différence de certains langages statiques.


mardi 5 août 2008

Démonologie informatique 1/2

Voici la suite de ma saga de l'été concernant les langages informatiques. Je doute que ça intéresse beaucoup de personnes mais pour moi, l'audience n'est pas un problème, je ne suis pas rémunéré par la publicité et de toutes façons, comme on va le voir, le culturel ne pait pas.

Dans les discussions entre professionnels de la profession (le développement de logiciels s'entend), il y a pas mal de buzz words qui reviennnent tout le temps. L'un de ces mots est langages dynamiques. Là, Madame Chombier, très estimée lectrice de ce blog demeurant dans la charmante localité de Bignoules-de-Bagorre, se demande déjà ce qu'est un buzzword. Cliquez sur le mot en bleu souligné, madame Chombier. Les autres se demanderont certainement ce que c'est qu'un langage dynamique et s'imaginent peut-être une allégorie du langage portant costume Hugo Boss, Blackberry vissé sur l'oreille et iMac à la main, courant d'une altière foulée vers l'avenir tout en portant un regard à peine méprisant sur la plèbe statique rampant sur son chemin.

En fait, interrogez les professionnels de la profession et il ne s'en trouvera pas deux pour donner une définition équivalente (j'ai pas dit identique) d'un langage dynamique. Pourquoi ? Les professionnels sont-ils des ignares ? Pas plus que d'autres. Il y a simplement que, dans ce contexte, le mot dynamique a plusieurs sens qu'il convient maintenant d'examiner.

Tout d'abord, dynamique est le contraire de statique. Avant de trouver un sens au dynamique, trouvons un sens à son contraire dans le cadre des langages informatiques.
Pour qui a un peu de connaissances en la matière, le premier mot qui vient à l'esprit quand on associe langage informatique et statique, c'est typage. Le typage, kezako ?

Le typage est une opération qui s'applique à un programme informatique lors de son écriture et/ou de son exécution et qui a pour effet de confirmer la nature (le type) des éléments que manipule ce programme. Imaginez que vous vous brossiez les dents, on pourrait dire qu'il s'agit de l'exécution de votre programme de brossage des dents (routine événementielle planifiée 2 à 3 par jour selon les recommandations constructeur). Imaginez alors qu'un petit démon s'assoie sur votre épaule pendant que vous vous brossez les dents et vous confirme, à chaque va-et-vient de la brosse couverte de dentifrice moussant sur votre râtelier, que vous utilisez bien une brosse à dents et, quand bien même cela ne suffirait pas, le petit démon marque "brosse à dents" sur ledit ustensile (au cas où vous êtes trop crétin pour vous en apercevoir).

Voilà, illustrée, l'opération de typage.

Là, les lecteurs qui ont déjà fait de la programmation s'énervent car ils détestent qu'on les prennent pour des crétins (supposition de ma part). Patience, mesdames et messieurs, praticiennes et praticiennes de l'art ésotérique de la programmation, patience. Je n'en ai pas fini avec mes métaphores.

Quel est l'intérêt de typer ? De coller des étiquettes sur des objets (ça y est, le mot magique est prononcé) ? On va le voir, mais avant, il faut passer par, non pas un, mai deux passages obligés, la variable et l'affectation de valeur.

Une variable est une boîte avec une étiquette dessus. Sur l'étiquette figure un nom, celui de la variable. Une variable, c'est une boîte avec un nom écrit dessus. (après les boîtes sont rangées dans une étagère qui ne doit contenir que des boîtes avec des noms différents, mais ça c'est une autre histoire).

Une variable est faite pour contenir une valeur ce qui revient à mettre quelque chose dans la boîte. Exemple, vous avez adopté un nouveau chien et vous voulez lui donner un nom, mettons "Cosmos". Dans un langage informatique, vous prendriez une boîte, colleriez dessus une étiquette marquée "Cosmos" et mettriez le chien dedans. Le langage informatique ne permet pas de faire des trous dans la boîte pour que le chien puisse respirer, je sais c'est barbare mais c'est pas le pire. Imaginons que vous ayez un deuxième chien mais une seule boîte, rien ne vous empêche de retirer le premier chien de la boîte (la variable) Cosmos et d'y mettre le deuxième. Plusieurs chiens, un seul nom. Ridicule. Qu'à cela ne tienne, prenons une deuxième boîte, collons dessus une étiquette marquée "Neptune" (c'est une expérience de pensée, je vous assure qu'aucun chien n'a été maltraité durant le processus) et fourrons le deuxième chien dedans.

A ce stade, nous avons deux variables (deux boîtes) avec chacune un nom (l'étiquette) différent et un contenu (des chiens).

En pseudo-langage ça donne ceci :
var cosmos = new Chien();
var neptune = new Chien();

Maintenant, nous allons utiliser ces variables pour faire des trucs. Qu'est-ce qu'on peut faire avec deux chiens ? Il se trouve que j'en connaît un petit bout sur la question, étant le propriétaire des deux animaux sus-cités. Avec Neptune et Cosmos, on peut jouer à la balle.

Créons donc un programme de jeu de balle avec deux chiens.
Pour des raisons de simplicité, considérons que le lancer de balle est un événement extérieur et que le résultat du programme est la détermination aléatoire du chien qui a attrapé la balle (l'un des deux l'attrape toujours, c'est empiriquement constaté par l'expert du domaine chaque jour).

Comment dire qu'un chien a attrapé la balle ? Bah, on n'a qu'à dire que la balle va dans la boîte avec le chien. Le problème avec les boîtes (les variables), c'est que bien souvent, elle ne peuvent contenir qu'un seul truc, un chien OU une balle mais pas les deux. Et on arrive à un petit paradoxe :

function rattraper_baballe(var balle_lancee)
{
var chien_gagnant = choisir_chien(cosmos, neptune);
chien_gagnant = balle_lancee;

}


Le code ci-dessus ne veut rien dire, on s'en rend compte. Le problème c'est qu'il est parfaitement légal ! Car on a dit qu'une boîte pouvait contenir n'importe quoi, des chiens, des balles mais une seule valeur, c'est la loi. Dans notre code, la fonction choisir_chien retourne l'une des deux valeurs de chien et le programme affecte cette valeur à une variable chien_gagnant qui contient alors un chien. Le problème, c'est que la ligne du dessous affecte à cette même variable la valeur balle_lancee que nous donne l'extérieur de la fonction rattraper_baballe.

Ce faisant, on perd l'information sur le chien choisi et on se retrouve avec la seule information de la balle lancée. Plus grave, si, dans la suite du programme, on applique un autre traitement à la variable chien_gagnant en supposant que celle-ci contient un chien, on risque d'avoir une erreur (un bug) :

donne_nonosse(chien_gagnant) ;
Réponse (possible) du programme => Mauvaise valeur d'entrée !

Et oui ! La variable contient une valeur qui est une balle. Le programme a simplement vu que ça ne collait pas mais le développeur, lui, se demande ce qui se passe et, surtout, comment corriger ce danger de confondre chiens et balles dans les variables.

Le moyen que les concepteurs des langages ont trouvé s'appelle le typage. En gros, en plus de coller une étiquette avec le nom d'une variable sur une boîte, le langage impose de coller une autre étiquette avec la nature de ce que la boîte peut contenir. Là, non seulement on restreint le contenu à une chose mais aussi à une chose d'un certain type.

Notre boîte possède alors deux étiquette, une pour son nom et une pour son type. Exemple :
var Chien cosmos = new Chien();
var Chien neptune = new Chien();
var Balle baballe = new Balle();
cosmos = baballe; -- erreur de type !!!
neptune = cosmos; -- OK, même type

Les affectation sauvages entre variables de types différents vont se remarquer plus vite et, ainsi, des erreurs importantes seront évitées. Ca, c'est la théorie.

Madame Chombier, qui n'a pas les oreilles dans sa poche et à qui on ne l'a fait pas, nous fait remarquer que "il est où le rapport avec le dynamique de tout à l'heure ?".
Et bien voilà, le typage statique, veut dire en réalité : contrôle du type avant l'exécution du programme. L'erreur de type présentée dans le bout de code plus haut peut être détectée à trois moments : lors de l'écriture du code par un développeur un peu plus éveillé que la moyenne, lors de la compilation ( étape qui transforme un texte dans un langage donné en une série d'instructions absconses que l'ordinateur pourra comprendre) ou lors de l'exécution (dans cet ordre)

Le typage statique impose que toutes les informations sur le type des variables soient connues au moment de compiler le programme avant même son exécution. Ces informations permettent au compilateur (un genre de petit démon comme celui des brosses à dents) de détecter des erreurs du type de celles que nous avons évoquées.
Le typage statique impose, en plus, que les variables ne peuvent pas changer de type au cours du déroulement du programme. Le contraire signifierait que le type d'une variable est dynamique (tiens donc ?).

Est-ce que toutes les variables doivent être typées ? Ca dépend des langages. Certains autorisent des variables non typées ou possédant tous les types à la fois. pour ces boîtes là, aucun contrôle n'est effectué, le programmeur prend ses responsabilités. C'est ce qu'on appelle le typage statique faible.
D'autres langages ne supportent pas que l'on déroge aux règles et aux procédures, ils imposent un typage fort, toutes les variables doivent avoir leur étiquette.

En résumé,
typage statique = boîtes avec une étiquette indécollable écrite à l'encre indélébile.
typage dynamique = boîtes avec étiquettes décollables, écrites à l'encre sympathique.
typage faible = boîtes sans étiquette possibles
typage fort = boîtes avec étiquette obligatoires

Et notre petit démon de tout à l'heure ? Qu'est ce qu'il fait ? Eh bien, du typage dynamique, ma bonne dame. Il vous dit pendant tout le temps que vous vous brossez les dents que vous utilisez bien une brosse à dents. Et s'il vous prenez l'envie de vous peigner la moustache avec, il vous préviendrait qu'une erreur de type malencontreuse serait sur le point de se produire.

Et un petit démon qui fait du typage statique ? Celui là, il vous forcerait à écrire "brosse à dents" sur votre brosse à dents à chaque fois que vous brossez les dents. Pareil, quand vous sortez les chiens, il vous forcerait à écrire "chien" sur vos deux chiens, "balle" sur les balles des chiens avant même que vous ne puissiez mettre un pied dehors. Démoniaque, je vous dis !

Au final, nos deux démons ont la même utilité, nous prévenir quand on utilise pas le bon type pour la bonne opération et vice-versa. L'un nous le dit gentiment et ne nous empêche pas toujours de le faire et l'autre a exactement le même comportement mais il nous a forcé à étiquetter toute notre maison, nos animaux domestiques, voire pire (imaginez vous allez aux toilettes avec un démon de ce type).

Rendez-vous la semaine prochaine avec la présentation de certains de ces démons.

dimanche 20 juillet 2008

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

Dans l'article précédent, j'avais évoqué la grandeur de l'approche objet. En des temps reculés que les plus jeunes développeurs ne pouvaient pas connaître, Java n'existait pas (et si, ça a existé, il n'y avait pas l'Internet non plus), la plupart des applications de gestion étaient développées en C, voire en C++ ou en Visual Basic.
Quelques chanceux pouvaient utiliser des langages dynamiques comme LISP ou Perl. D'autres étaient confinés dans les affres des RAD tels que Delphi ou WinDEV.

C'est à cette époque que j'ai commencé ma carrière professionnelle comme développeur C++. J'avais découvert ce langage en école, par moi-même, aidé en cela par un bon niveau en C et l'étude de la théorie des types.
J'avais déjà l'habitude de coder de manière modulaire en C, en fait, je faisais de l'objet sans le savoir.

Mon premier job en tant que développeur (j'avais été recruté pour ma connaissance de C et C++) fut de maintenir une application écrite en VB 4.0, un langage purement procédural dont la syntaxe était conçue pour les mauvais développeurs, ceux qui ont séché les cours de pointeurs, de structures de données et d'algorithmique de la faculté d'informatique.

Comme il arrive neuf fois sur dix dans ce genre de cas, le code était énorme, peu modulaire, avec beaucoup de motifs répétés (par la fameuse méthode de développement dite du "copier/coller"). En vertu des lois de la statistique, plus il y a de lignes de code (et, a fortiori de lignes mal écrites), plus il y a de chances que des bugs apparaissent.
Force fut de constater que la loi s'appliquait parfaitement à la situation, maintenir cette application devenait un cauchemar. Les développeurs étaient démotivés, les chefs de projet n'avaient plus assez de ressources pour travailler sur des produits plus innovants.

Nous n'avons eu alors de cesse que de préconiser l'approche objet et le passage à C++ pour le coeur de l'application, pensant que l'utilisation d'un autre paradigme enlieu et place du (mauvais) procédural allaient arranger nos problèmes.

Dans un premier temps, je me suis chargé d'appliquer les méthodes de l'approche objet au code VB 4.0. En pratique, c'est revenu à porter l'intégralité du code vers VB 5.0 et à réarchitecturer certains modules en taillant dans le code mort, en identifiant et factorisant les répétitions.
De 150000 lignes, les programmes sont passés à 60000 lignes. Parallèlement à ça, les nouveaux projets commençaient directement à être écrits en C++ en se basant sur les MFC (Microsoft Foundation Classes) et la STL (Standard Template Library).

Et puis, tout s'est accéléré. L'approche objet tendait à se généraliser, les ouvrages sur le sujet devenaient plus accessibles. UML est apparu.

Au bout de quelques mois, on s'est retrouvés à produire plus de modèles que de code, à définir des architectures de produit et à parler en termes de motifs de conception (Design Patterns).
Dans le même temps, les développeurs essayaient de transposer sous forme de code C++ le résultat de ces conceptions et, force fut de constater là encore, que le résultats n'étaient pas à la hauteur de nos attentes.

Le code se retrouvait aussi compliqué, touffu et verbeux que s'il avait été écrit en procédural. Plus encore, des choses simples se trouvaient artificiellement complexifiées à cause du respect zélé que nous avions de l'approche objet généralisée.

La première conclusion de cet exemple est que l'approche objet généralisé n'est pas adaptée à tous les contextes. En gros "trop d'objet tue l'objet".

Et pourtant, C++ n'étant qu'un "meilleur C" suivant les termes de son concepteur Bjaarne Soustrup, il restait possible de coder simplement à base de fonctions et de structures de données simples les choses simples sans avoir à fournir un substrat objet simplement pour respecter des normes de développement qui devenaient inadéquates.

Dans le même temps, la syntaxe de C++ héritait celle de C avec son cortège de pointeurs, de chaînes à zéro terminal et autres joyeusetés qui sont des portes ouvertes aux bugs de toutes sortes pour qui ne les maîtrisent pas.
Dans le même temps, rien n'imposait aux développeurs d'utiliser l'approche objet en C++ et certains d'entre eux continuaient à penser et à coder en procédural.

La chose est impossible en Java. A cette époque, Java quittait les labos de Sun et entrait par la grande porte dans les SI des entreprises. Et Java était une révolution.

En Java, il n'y a pas de pointeurs, juste des références anonymes (en fait ce sont des pointeurs de pointeurs qui ne sont pas déréférençables).
En Java, tout est objet (ce qui n'est pas en soi une révolution car SmallTalk fut, dans les années 80, le premier langage pur objet).
En Java, tout se détermine par la classe. Impossible de coder ne serait-ce qu'une fonction hors d'une classe même main, le point d'entrée d'un programme.
En Java, plus besoin de se forcer à libérer les références allouées en mémoire, il y a un ramasseur de miettes qui nettoie les saletés des développeurs.

Java a été conçu pour être le langage standard de l'approche objet avec la ferme d'intention de forcer l'utilisation de l'approche objet pour l'écriture de n'importe quel programme.
Voici le programme iconique "Hello World !" écrit en Java (à titre de comparaison, sa version C++/STL)

En Java:
public class HelloWorld {
public static void main(String[] args) {
System.out.print("Hello world!");
}
}


En C++:
#include <iostream>
using namespace std;

int main()
{
cout << "Hello world!" <<>
return 0;
}


Les deux programmes font respectivement 91 (en Java) et 45 (en C++) caractères pour effectuer la même chose. Soit le double pour Java.
Si Java est verbeux pour quelquechose d'aussi simple que d'afficher une chaîne de caractères littérale, imaginez ce que ça doit être pour trier une liste d'entier.
Je ne fournirai pas le code en Java (Google est votre ami) pour cette tâche mais à titre de teaser voici comment on fait en Perl:

use strict;
my @list = (4, 10, 56, 78, 12, 2, 6);
print join(',',sort { $a <=> $b } @list);


La sortie écran donne : 2,4,6,10,12,56,78

Sans fonctions statiques, avec du tout objet (enfin presque), les développeurs d'applications, les concepteurs de bibliothèques de classes et de frameworks se sont mis à créer des objets sans état avec juste du comportement pour imiter le fonctionnement (simple) des fonctions des langages procéduraux.
Et, en bons adeptes de Design Patterns, ils firent collaborer ces classes dans des modèles de conception jusqu'à aboutir à des constructions intellectuellement très satisfaisantes mais parfaitement impraticables sans une bonne dose de self-control.

Et voilà ce que ça peut donner avec l'exemple du tri :

import framework.algorithmes.* // bibliotheques d'objets fonctions qui implémentent les algorithmes sur les structures de données (tri, parcours, recherche)
import framework.structures.* // bibliotheques d'objets qui implémentent les structures de données de base (listes, piles, tableaux, arbres)
public class TRiListeDEntiers {
public static void main(String[] args) {
int[] tableau = {4, 10, 56, 78, 12, 2, 6};
ListeDEntiers liste = new ListeDEntiers(tableau);
TriNumerique operateur_tri = new TriNumerique(TriNumerique.TriRapide);
ListeDEntiers liste_triee = operateur_tri.trier(liste);
System.out.print(liste_triee.EnChaine(","));
}
}


On peut se plaindre de Java mais C++ et la STL ont des concepts similaires :


#include <iostream>
#include <algorithm>
using namespace std;

int main() {
int tableau[7] = {4, 10, 56, 78, 12, 2, 6};

sort(tableau, tableau+7);

for (int i=0; i<7; i++) {
cout << a[i] << " ";
}
}


L'idée derrière tout mon propos est que pour faire des choses simples qu'on fait tous les jours quand on développe des programmes, il faut des langages qui permettent de le faire en un minimum de signes.
Java (et C++) autorisent beaucoup de choses et permettent de faire de grandes choses d'ailleurs mais pour les petites choses du quotidien, ils sont absolument impraticables.

C'est la décadence de l'objet : devoir écrire des choses de plus en plus baroques et artificielles pour des choses simples qu'un étudiant de première année sait coder en Pascal ou en Scheme.
C'est à dire : des structures de données (paire, listes, tableaux, vecteurs, cartes, chaînes de caractères, arbres, graphes) et des algorithmes (insertion et suppression d'élément, accès aléatoire, parcours, recherche et tri)

Je vois les langages à objets historiques comme des empereurs romains. Après avoir connu la gloire des triomphes sur les hordes barbares procédurales (l'immonde Basic en tête), les langages à objets ont sombré dans une lente et inexorable décadence, devenant de plus en plus boursouflés, encombrants et maladroits.

Péché capital de l'objet : Un paradigme omniprésent mais incomplet
Difficile de passer à côté de l'héritage, de l'encapsulation et du concept de classe quand on code en Java (ou en C++). Et pourtant, certaines constructions du langage qui auraient mérités d'être des objets ne le sont pas (le type int, les structures de contrôles).
D'une manière générale, l'approche objet est criticable car elle ne repose pas sur des fondations formelles précises qui permettraient de démontrer les capacités des langages qu'elle inspire.
D'un autre côté, il existe un fort engouement pour les langages à objets, engouement qui confine à l'irrationnalité et à la pensée magique. Certains pensent que passer à l'objet va magiquement résoudre leurs problèmes de cycle de développement, de couverture de code ou de spécifications fonctionnelles. En aucun cas.

Il existe des tentatives de définition formelle des langages à objets, l'une des mieux abouties est présentée dans ce livre (A Theory of Objects).
Cette définition formelle n'est pas unique malheureusement, et les différentes théories n'ont pas pu être prouvées équivalentes.

Péché capital de l'objet : Des langages verbeux
La base de la théorie de la programmation repose sur les structures de l'information et les algorithmes de traitement. La productivité logicielle se mesure par le rapport entre la qualité du produit et le nombre d'heures de réalisation.
La plupart des langages à objets manquent de ces structures et de ces algorithmes. Ils peuvent se voir étendus, certes, par des bibliothèques.
Mais ces modules complémentaires sont difficiles à concevoir et à mettre au point en raison d'un système de typage très évolué, généralement strict et statique.
Il en résulte que chaque structure classique comme une liste ou un vecteur doit être construit comme un type dérivé basé sur un modèle (de liste ou de vecteur) et affublé du type des éléments contenus.
Pareillement, les algorithmes classiques (tri rapide, recherche dichotomique ou parcours infixe) sont implémentés dans des objets sans états.
Résultat ? Il est quasiment impossible de concevoir simplement une liste opérationnelle capable de contenir des éléments hétérogènes et de la parcourir aisément. Chose qu'il est parfaitement possible de faire dans des langages comme LISP ou Python.

Péché capital de l'objet : La difficulté de faire simple
Vous avez déjà regardé l'implémentation du type String en Java ? (ou du type CString en C++/MFC) ? C'est abominable !
Pour quelque chose d'aussi simple et de consensuel qu'une chaîne de caractères, Java et C++ englobent un tableau de caractères ou d'entiers non signés dans une enveloppe très épaisse qui donne l'illusion au programmeur de manipuler une entité très simple.
En soi, il est vrai que manipuler une variable de type String ou CString est simple mais c'est au prix d'une perte de performance.
Heureusement, les concepteurs de Java ont pensé à fournir aux développeurs une bibliothèque de types simples comme String ou BigDecimal. Mais quand le développeur a à représenter le contenu d'une table relationnelle dans un objet, ou pire à rendre persistant l'état d'un objet, il s'arrache les cheveux, il vient de rencontrer le problème de l'adaptation objet-relationnel (Object-relational impedance mismatch en anglais)
Plutôt que de développer ce concept de manière exhaustive, j'invite le lecteur à consulter ces deux liens (en anglais)
The Object-Relational Impedance Mismatch
The Viet-Nam of Computer Science

Cet exemple particulier illustre le fait que l'approche objet met en exergue des éléments structurels statiques (architecturaux on peut dire) alors que le modèle procédural (et le modèle relationel jusqu'à un certain point) se concentre sur la dynamique des systèmes et les changements d'états.

L'approche objet est d'un grand secours quand il s'agit de concevoir et de maintenir l'architecture d'un ensemble logiciel (comme on le voit à l'heure actuelle avec les Entreprise Service Bus, Entreprise Application Integration et autres Services Oriented Architecture)
C'est un outil pour les concepteurs et les architectes logicielles misant sur des concepts industriels tels que réutilisabilité, sécurité, extensibilité, compréhension, etc.
Mais dès que la granularité devient faible ou que les spécifications se transforment en garanties de comportement, l'approche objet atteint ses limites, manque d'agilité et la respecter à la lettre devient contre-productif.

Voilà pourquoi le tout objet est idiot.

"Et quand on ne dispose que d'un langage à objets ?" me demanderons les lecteurs qui n'ont pas encore décroché.
Nous allons voir que les langages à objets qui subsistent aux pieds de Java dans le monde de l'industrie du logiciel sont beaucoup moins impérialistes et décadents que leur illustre ancêtre.

A la semaine prochaine, à la découverte des langages dynamiques.

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.