Ce qui casse dans un programme procédural qui grossit ; regrouper données et traitements, responsabilité et réutilisation ; panorama des langages objet.
Écrivons une gestion de bibliothèque avec les outils d'Algorithmique 1. Trois tableaux :
String[] titres = {"Dune", "Fondation", "Solaris"};String[] auteurs = {"Herbert", "Asimov", "Lem"};boolean[] disponibles = {true, false, true};Le livre numéro 1, c'est titres[1], auteurs[1] et disponibles[1]. Rien ne le dit, rien ne
le garantit : c'est une convention, tenue à la main, dans chaque fonction qui touche à ces
tableaux.
Retirons maintenant « Fondation ». Il faut décaler les trois tableaux. Si une seule des trois opérations est oubliée — ou faite dans le mauvais ordre, ou faite deux fois — les titres, les auteurs et les disponibilités cessent de correspondre. Le programme continue, sans la moindre erreur, et affiche que « Solaris » est d'Asimov.
Ce chapitre part de ce genre de panne. L'objet n'est pas une façon plus élégante d'écrire la même chose : c'est une réponse à un problème précis, et il vaut mieux avoir vu le problème.
Ce qui casse quand un programme grossit
Quatre symptômes reviennent, et ils sont liés.
La cohérence n'est garantie par rien. Les tableaux parallèles en sont le cas d'école, mais le problème est général : dès que plusieurs variables doivent rester d'accord entre elles, et que rien dans le langage ne l'impose, la moindre distraction les désynchronise. Et le symptôme apparaît loin de la faute.
Les fonctions enflent. Une fonction qui doit connaître le titre, l'auteur, la disponibilité,
l'emprunteur et la date prend cinq paramètres, puis huit. L'ordre des arguments devient une
source d'erreurs — deux String consécutifs s'intervertissent sans que le compilateur bronche.
Tout le monde peut tout modifier. Rien n'empêche une fonction d'écrire disponibles[i] = true sans vérifier que le livre a bien été rendu. Il n'existe aucun endroit où faire respecter
une règle : la règle est éparpillée dans tous les appelants, ou nulle part.
Un changement de structure se propage partout. Ajouter l'ISBN, c'est un quatrième tableau, et toutes les fonctions qui insèrent ou suppriment sont à relire. Sur un programme de mille lignes, c'est une journée ; sur cinquante mille, c'est un refus.
Le point commun de ces quatre symptômes : les données et les traitements qui les concernent sont séparés. Les données sont dans des tableaux globaux, les traitements dans des fonctions qui les prennent en paramètre, et rien ne les relie sinon la discipline du programmeur.
L'idée : regrouper
La programmation orientée objet propose une réponse tenant en une phrase : mettre ensemble les données et les opérations qui les concernent.
class Livre { titre auteur disponible emprunter() rendre() estDisponible()}Un Livre n'est plus trois cases dans trois tableaux, c'est une chose. La désynchronisation
disparaît par construction : il n'y a plus trois tableaux à maintenir d'accord, il y a une liste
de livres, chacun portant ses propres données. Supprimer un livre, c'est le retirer d'une liste,
et il emporte tout ce qui le concerne.
Deux conséquences suivent immédiatement.
Les fonctions maigrissent. emprunter(livre) devient livre.emprunter() : le premier
paramètre n'est plus un paramètre, c'est l'objet lui-même. Une méthode qui prenait cinq
arguments n'en prend souvent plus aucun.
Il existe enfin un endroit où faire respecter une règle. « On n'emprunte pas un livre déjà
emprunté » s'écrit une fois, dans emprunter(). Tout le code qui emprunte passe par là. Le
chapitre 3 en fera le principe central du semestre.
Responsabilité et réutilisation
Deux notions se dégagent, et elles guideront tout le cours.
La responsabilité est la question à se poser devant chaque classe : de quoi cet objet
est-il responsable, et de quoi ne l'est-il pas ? Un Livre est responsable de son état
d'emprunt. Il n'est pas responsable de l'affichage à l'écran, ni de l'enregistrement dans un
fichier, ni de la politique de retard de l'établissement. Une classe qui fait tout est une
fonction géante déguisée, et l'on n'aura rien gagné.
Le critère opérationnel, qu'on emploiera tout le semestre : une classe doit pouvoir se décrire en une phrase sans « et ». « Un livre connaît son titre, son auteur, et sait s'il est empruntable » va bien. « Un livre connaît son titre et gère l'affichage du catalogue et sauvegarde le fichier » signale trois classes.
La réutilisation vient ensuite, presque gratuitement. Une classe bien délimitée s'emploie dans un autre programme sans rien emporter avec elle. Ce n'est pas la promesse de tout réutiliser — cette promesse des années 1990 n'a jamais été tenue — mais un effet secondaire réel d'une bonne délimitation.
Trois tableaux parallèles titres, auteurs et disponibles décrivent des livres. Quel est le défaut principal de cette organisation ?
Panorama
L'objet n'est pas récent, et connaître ses variantes évite de croire que Java est la programmation objet.
Simula 67 invente les classes, pour écrire des simulations — d'où son nom, et d'où le vocabulaire : on simulait des guichets, des files, des clients, c'est-à-dire des choses.
Smalltalk (années 1970) pousse l'idée jusqu'au bout : tout y est objet, y compris les nombres et les classes elles-mêmes, et le seul mécanisme est l'envoi de message.
C++ (1983) greffe l'objet sur le C, en gardant la compatibilité et la performance — d'où l'héritage multiple, et une complexité considérable.
Java (1995) simplifie délibérément : pas d'héritage multiple de classes, ramasse-miettes,
machine virtuelle. C'est le langage de ce cours, parce que ses notions y sont explicites —
private, abstract, interface sont des mots-clés, là où Python s'en remet à des conventions.
Python et JavaScript relèvent d'autres traditions : typage dynamique pour le premier, objets à prototypes pour le second, où un objet hérite directement d'un autre objet sans qu'aucune classe n'intervienne.
Deux lignes de partage traversent ce paysage, et l'on y reviendra. Le typage — statique comme en Java, où le compilateur vérifie les types, ou dynamique comme en Python, où l'on vérifie à l'exécution ; c'est ce qui fera tout l'intérêt du chapitre 6. Et l'héritage multiple, autorisé en C++ et en Python, interdit en Java pour les classes — le chapitre 6 expliquera ce qui le remplace.
Ce que l'objet ne résout pas
Une mise en garde, pour ne pas transformer ce cours en apologie.
L'objet n'est pas toujours le bon outil. Un programme de calcul numérique, un filtre de texte de trente lignes, un pilote de périphérique ne gagnent rien à être découpés en classes — et le C du cours précédent reste le bon choix pour le dernier.
L'objet ne rend pas un mauvais découpage bon. Une hiérarchie de classes mal pensée est plus difficile à corriger qu'un mauvais découpage en fonctions, parce que les dépendances y sont plus profondes. C'est d'ailleurs le sujet du point qui coince du chapitre 5.
Et l'objet coûte : plus de fichiers, plus de vocabulaire, une indirection à l'exécution. Sur un programme de cent lignes, ce coût n'est pas amorti. Le seuil se situe à peu près là où l'introduction de ce chapitre commence à faire mal — quand plusieurs personnes travaillent sur le même code, et quand celui-ci va vivre plusieurs années.
Une classe Livre gère son état d'emprunt, son affichage à l'écran et son enregistrement dans un fichier. Que dit le critère de responsabilité ?
À vous
L'exercice part du code de l'introduction et le fait échouer, avant de le réparer.
D'abord la version à tableaux parallèles : vous écrivez la suppression d'un livre, et le squelette contient volontairement l'oubli d'un des trois décalages. Un vérificateur de cohérence vous dira ce que le programme, lui, ne dirait jamais.
Ensuite la version objet : une liste de livres, chacun portant ses champs. Vous réécrirez la même suppression et constaterez qu'il n'y a plus rien à synchroniser — non pas parce que vous avez été plus attentif, mais parce que le problème n'existe plus.
La dernière partie compte les paramètres : la même opération écrite en procédural et en objet, côte à côte.
Faites désynchroniser des tableaux parallèles, puis réécrivez la même bibliothèque en objets.
// ── 1. Version procédurale : trois tableaux parallèles ──────────────────── let titres = ["Dune", "Fondation", "Solaris", "Ubik"]; let auteurs = ["Herbert", "Asimov", "Lem", "Dick"]; let disponibles = [true, false, true, true]; // La correspondance entre les trois tableaux n'est écrite NULLE PART : on la // vérifie ici avec une copie de référence, ce qu'aucun programme réel n'a. const REFERENCE = { Dune: "Herbert", Fondation: "Asimov", Solaris: "Lem", Ubik: "Dick" }; function verifierCoherence(etiquette) { const fautes = []; for (let i = 0; i < titres.length; i++) { if (REFERENCE[titres[i]] !== auteurs[i]) { fautes.push(titres[i] + " attribué à " + auteurs[i] + " au lieu de " + REFERENCE[titres[i]]); } } const tailles = [titres.length, auteurs.length, disponibles.length]; console.log(" " + etiquette.padEnd(24) + "tailles [" + tailles.join(",") + "]" + (fautes.length ? " INCOHÉRENT : " + fautes.join(" ; ") : " cohérent")); } function supprimerProcedural(i) { titres.splice(i, 1); auteurs.splice(i, 1); // ← il manque le troisième décalage. Le programme ne dira rien. } // ── 2. Version objet ────────────────────────────────────────────────────── function creerLivre(titre, auteur, disponible) { return { titre, auteur, disponible, // La règle vit ICI, en un seul endroit, et tout le monde y passe. emprunter() { return false; }, // ← à écrire rendre() { this.disponible = true; }, toString() { return this.titre + " (" + this.auteur + ")" + (this.disponible ? "" : " — emprunté"); }, }; } let bibliotheque = [ creerLivre("Dune", "Herbert", true), creerLivre("Fondation", "Asimov", false), creerLivre("Solaris", "Lem", true), creerLivre("Ubik", "Dick", true), ]; function supprimerObjet(i) { bibliotheque.splice(i, 1); // et c'est tout : le livre emporte ses données } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Lancez la suppression procédurale et lisez le rapport de cohérence. // 2. Réparez supprimerProcedural, puis demandez-vous ce qui se passera le // jour où l'on ajoutera un quatrième tableau (les ISBN). // 3. Écrivez emprunter() : il doit REFUSER si le livre est déjà emprunté. // Combien d'endroits faut-il modifier pour changer cette règle ? verifierCoherence("au départ"); supprimerProcedural(1); verifierCoherence("après suppression");
En travaux pratiques
Partir du code procédural, et le faire craquer
Écrire d'abord la version sans objets, la faire évoluer jusqu'à ce qu'elle devienne pénible, puis mesurer précisément ce que l'objet répare.
- Un JDK et de quoi compiler en ligne de commande
- Une expérience de la programmation impérative
- 1. La version procédurale
Écrivez une gestion de médiathèque avec des tableaux parallèles : un pour les titres, un pour les auteurs, un pour les années, un pour la disponibilité. Ajoutez, affichez, empruntez.
- 2. Le premier accroc
Ajoutez un champ : le nombre d'exemplaires. Comptez les endroits du code que vous devez modifier.
- 3. Le second accroc
Écrivez une fonction qui trie les documents par année. Faites-la tourner et vérifiez les auteurs après le tri.
- 4. Le troisième accroc
Ajoutez une règle : un document indisponible ne peut pas être emprunté. Comptez combien d'endroits du programme peuvent violer cette règle.
- 5. Regrouper
Créez une classe Document rassemblant les quatre champs, et remplacez les tableaux parallèles par un tableau de documents. Refaites les étapes 2 et 3.
- 6. Protéger
Rendez la disponibilité modifiable uniquement par des méthodes emprunter et rendre. Recomptez les endroits qui peuvent violer la règle.
- 7. Comparer
Dressez le tableau : nombre de lignes, nombre d'endroits à modifier pour ajouter un champ, nombre d'endroits pouvant casser l'invariant. Avant et après.
- Vous constatez, sur votre propre code, le désalignement des tableaux après tri
- Ajouter un champ ne touche qu'un seul fichier après refonte
- Un seul endroit du programme peut désormais modifier la disponibilité
Ce que la suite en fait
Le chapitre 2 donne la syntaxe de ce que ce chapitre a esquissé : ce qu'est réellement une
classe, ce qu'est un objet, ce que new fait en mémoire — et la référence this, qui est le
premier point qui coince du semestre.
Le chapitre 3 reprendra la troisième panne de l'introduction, celle où n'importe qui peut écrire n'importe quoi dans les données : c'est l'encapsulation, et c'est ce qui transforme le regroupement de ce chapitre en véritable garantie.
À retenir
Vous avez parcouru les 9 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.