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.

4 commentaires:

  1. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  2. Ah, je l'attendais celui-là :D

    En introduction à me commentaire, je soulignerai que je ne suis pas un taliban-objet : dans l'idée générale, je suis d'accord sur le principe du "à chaque langage son domaine". Mais il y a certains points sur lesquels je suis obligé de tilter... Mes propos seront davantage en rapport avec Java, que je maîtrise plus que C++.

    Notamment sur tes exemples concernant la verbosité de Java. Difficile de dire qu'un "Hello World" est représentatif de la verbosité d'un langage, car mis à part l'étudiant sur sa première heure de cours, qui va nous pondre un "HW" ? Un exemple plus concret - plus objet peut-être ? - aurait été à mon goût plus pertinent. Je ne dis pas que les scores auraient été inversés, mais ne seraient-ce que les prototypes de méthodes du C++ (et ses deux fichiers .h et .cpp ! A prendre en compte aussi !) prenennt de la place.

    Concernant les tris, impossible de ne pas être d'accord sur l'efficacité de Perl sur une liste d'entiers. Seulement, afin encore une fois ton exemple Java n'est pas bon ! Tu nous fais un code spaghetti inutile, alors que la méthode Arrays.sort() permet de trier directement des tableaux "à l'ancienne", ici pas besoin de structures de données inutiles !

    import java.util.Arrays;
    public class Main {
    public static void main(String[] args) {
    int[] t = {2, 5, 1, 54, 12};
    Arrays.sort(t);
    for (int i = 0; i < t.length; i++) {
    System.out.print(t[i] + ", ");
    }
    }
    }

    Alors, oui, l'ensemble n'est pas aussi compact qu'en Perl... mais au final, ce qui nous intéresse, le tri, ne fait aussi qu'une seule petite ligne ! Très similaire au C++... et même un poil moins verbeux !

    Ensuite... Je trouve l'argument "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" car :
    - qui va faire un programme Java/C++ uniquement pour trier une poignée d'entiers ? Ces tris sont intégrés au sein d'autres algos...
    - longueur de l'algo de tri : une ligne, comme en Perl
    - "sort { $a <=> $b } @list" est quand même moins explicite que "Arrays.sort(tableau)"

    "La plupart des langages à objets manquent de ces structures et de ces algorithmes. Ils peuvent se voir étendus, certes, par des bibliothèques." Alors là, je me dois de réagir. Java (je vais rester sur celui-ci, que je maîtrise le mieux) est conçu pour se reposer sur des bibliothèques afin d'étendre facilement le langage. C'est un peu comme si tu me dis "Arf, pas mal Firefox, mais y'a rien dedans faut rajouter des extensions". Là, ce n'est pas attaquer le langage sur un défaut, mais sur une philosophie. Alors oui, Java est pauvre sans ses biblothèques, mais c'est fait pour.

    D'autant plus qu'il faut définir le concept de "Java nu" : pour moi, le Java "de base", c'est ce qui est contenu dans la JRE, bibliothèques incluses, et est donc le minimum exécutable par tout un chacun.

    "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." Pas d'accord non plus. Il est conseillé de typer ses conteneurs, mais ce n'est pas obligatoire. De plus, des objets hétérogènes d'une liste auront quasiment forcément une "racine" commune, accessible par ploymorphisme, et donc plus de problème. Et quand bien même on veut mélanger des truelles, du cassoulet et un tapis, on fait un controleur d'Object et ça roule (mais c'est une aberration source quasi-certaine de bug à mon humble avis).

    Les problèmes de persistance... au round 2, je n'ai pas le temps de lire les articles maintenant :P Cela dit, en guise d'intro à mon commentaire, je dirais simplement que de nombreux frameworks de persistance qui masquent ces problèmes au développeurs, qui n'ira pas réinventer la roue. C'est le prix à payer pour ne plus disposer que d'objets bruts. Faible coût, vue la facilité d'utilisation derrière...

    Donc, je le répète, je suis d'accord sur l'idée de l'article. Cependant, l'objet n'est pas aussi largué et repoussant que le laisse supposer l'article. D'autant qu'il est volontairement placé dans une situation où il n'excelle pas.

    En attendant la suite...

    (Précédent commentaire supprimé, boulette de formatage !)

    RépondreSupprimer
  3. Whaouh ! Ca c'est du commentaire ! Mon article est volontairement subjectif et transpire la mauvaise foi à dessein.
    Mais les problèmes évoqués sont eux parfaitement réels et objectivables. Je le répète, cet article est caricatural. Il y a beaucoup de choses très bien dans Java et beaucoup d'applications très bien conçues en Java comme il y a des choses discutables dans Python (par exemple) et des choses écrites en Python qui sont moches et inefficaces (mais moins qu'en Java, simple rapport statistique)

    Et tout merci pour ce commentaire d'un vrai programmeur qui défend son beef-steack contre un bloggeur de pure mauvaise foi.

    RépondreSupprimer
  4. Haha en tout cas je ne veux pas paraitre de le défendre façon hardcore, si tu savais le nombre de doléances que j'ai à son propos...

    Mais qui ne concernent pas directement le langage (qui, sémantiquement, est des langages que j'ai pratiqués le meilleur rapport compréhension à la lecture/taille, car pour encore trop de monde le code est sa propre doc... grrrr) davantage des histoires de packaging, de classpath, et toutes ces horreurs qui font qu'on peut passer 2 heures à résoudre un conflit de classes de mes deux.

    Tiens, ça pourrait faire un bon article ça... le rapport temps de dev/temps de paramétrages en tout genre d'une appli avec les langages dits modernes. Bah là, c'est pas la joie...

    RépondreSupprimer