Chapitre 1 · 3 h
Pourquoi l'objet
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.
Quiz · 1 question
Trois tableaux parallèles titres, auteurs et disponibles décrivent des livres. Quel est le défaut principal de cette organisation ?
- Elle consomme trois fois plus de mémoire qu'un tableau unique — consommation mémoire
- La correspondance entre les trois tableaux n'est garantie par RIEN : c'est une convention tenue à la main dans chaque fonction, et une suppression mal faite désynchronise les données sans provoquer la moindre erreur — cohérence non garantie
- Les tableaux ne peuvent pas contenir de types différents, ce qui interdit d'ajouter une date — types hétérogènes
Réponse : La mémoire n'est pas en cause — trois tableaux de n éléments occupent autant qu'un tableau de n objets à trois champs, à peu de chose près. Le défaut est que l'indice i est une CONVENTION : rien dans le langage n'exprime que titres[i] et auteurs[i] parlent du même livre, donc rien ne le vérifie. Une suppression qui décale deux tableaux sur trois produit un programme qui continue de fonctionner, sans erreur, en attribuant Solaris à Asimov — et le symptôme apparaît loin de la faute. Regrouper les trois champs dans un objet fait disparaître le problème PAR CONSTRUCTION : il n'y a plus de correspondance à maintenir, il y a une chose qui porte ses données.
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.
Quiz · 1 question
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é ?
- C'est une bonne conception : tout ce qui concerne un livre est au même endroit, ce qui est précisément le but de l'objet — bonne conception
- La classe se décrit avec des « et » : elle porte trois responsabilités indépendantes, qui changeront pour des raisons différentes — il y a là trois classes — trois responsabilités
- Peu importe, tant que les attributs sont privés : c'est l'encapsulation qui compte, pas le découpage — seule l'encapsulation compte
Réponse : Regrouper données et traitements ne signifie pas tout regrouper : le critère est la RESPONSABILITÉ, pas le sujet. Un livre est responsable de son état d'emprunt — cela, personne d'autre ne peut le savoir à sa place. L'affichage relève de l'interface utilisateur, l'enregistrement du stockage, et ces trois choses changent pour des raisons indépendantes : passer d'une console à une interface graphique ne devrait pas toucher aux règles d'emprunt. Le critère opérationnel est la description en UNE PHRASE SANS « ET » : dès qu'il en faut un, on tient probablement deux classes. Et l'encapsulation du chapitre 3 ne sauve rien ici — on peut parfaitement encapsuler une classe qui fait trois métiers, elle restera difficile à faire évoluer.
À 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.
Exercice de code
Faites désynchroniser des tableaux parallèles, puis réécrivez la même bibliothèque en objets.
Point de départ
// ── 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");
Solution
let titres = ["Dune", "Fondation", "Solaris", "Ubik"];
let auteurs = ["Herbert", "Asimov", "Lem", "Dick"];
let disponibles = [true, false, true, true];
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(26) + "tailles [" + tailles.join(",") + "]" +
(fautes.length ? " INCOHÉRENT : " + fautes.join(" ; ") : " cohérent"));
}
function supprimerFautif(i) { titres.splice(i, 1); auteurs.splice(i, 1); }
function supprimerCorrige(i) {
titres.splice(i, 1); auteurs.splice(i, 1); disponibles.splice(i, 1);
}
console.log("— 1. version procédurale —");
verifierCoherence("au départ");
supprimerFautif(1);
verifierCoherence("après suppression fautive");
console.log(" Les titres et les auteurs restent d'accord — la faute porte sur le");
console.log(" troisième tableau, donc « Solaris » hérite de la disponibilité de");
console.log(" « Fondation ». Aucune erreur, aucun avertissement, et un livre");
console.log(" emprunté qui se déclare disponible.");
titres = ["Dune", "Fondation", "Solaris", "Ubik"];
auteurs = ["Herbert", "Asimov", "Lem", "Dick"];
disponibles = [true, false, true, true];
supprimerCorrige(1);
verifierCoherence("après suppression correcte");
console.log(" Réparé — jusqu'au jour où l'on ajoutera les ISBN : il faudra");
console.log(" retrouver TOUTES les fonctions qui insèrent ou suppriment.");
console.log("");
console.log("— 2. version objet —");
function creerLivre(titre, auteur, disponible) {
return {
titre, auteur, disponible,
// La règle « on n'emprunte pas ce qui est déjà emprunté » est écrite UNE
// fois. Tout le code qui emprunte passe forcément par ici.
emprunter() {
if (!this.disponible) return false;
this.disponible = false;
return true;
},
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),
];
console.log(" avant : " + bibliotheque.map((l) => l.toString()).join(" | "));
bibliotheque.splice(1, 1); // une seule opération, rien à synchroniser
console.log(" après : " + bibliotheque.map((l) => l.toString()).join(" | "));
console.log(" Il n'y a plus de correspondance à maintenir : le livre a emporté");
console.log(" ses trois champs avec lui. Ajouter l'ISBN ne toucherait AUCUNE");
console.log(" fonction de suppression.");
console.log("");
console.log("— 3. la règle vit dans la méthode —");
const dune = bibliotheque[0];
console.log(" premier emprunt : " + dune.emprunter() + " -> " + dune.toString());
console.log(" second emprunt : " + dune.emprunter() + " -> refusé, et personne");
console.log(" n'a eu à y penser à l'extérieur");
console.log("");
console.log("— 4. le même service, deux écritures —");
console.log(" procédural : emprunter(titres, auteurs, disponibles, i) 4 paramètres");
console.log(" objet : livre.emprunter() 0 paramètre");
console.log(" Et pour changer la règle : un seul endroit contre autant d'appelants.");
En travaux pratiques
Travaux pratiques 1 · 2 h
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.
Avant de commencer
- Un JDK et de quoi compiler en ligne de commande
- Une expérience de la programmation impérative
Énoncé
- 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.
- Le premier accroc — Ajoutez un champ : le nombre d'exemplaires. Comptez les endroits du code que vous devez modifier.
- 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. Indice : Si vous ne déplacez pas TOUS les tableaux exactement de la même façon, les données se désolidarisent.
- 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.
- 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.
- Protéger — Rendez la disponibilité modifiable uniquement par des méthodes emprunter et rendre. Recomptez les endroits qui peuvent violer la règle.
- 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.
C'est réussi quand
- 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é
Correction
String[] titres = new String[100]; String[] auteurs = new String[100]; int[] annees = new int[100]; boolean[] dispo = new boolean[100]; /* le tri qui casse tout : on trie UN tableau */ Arrays.sort(annees); /* titres, auteurs et dispo n'ont pas bougé : « 1984 » est maintenant associé à l'auteur de « Dune » */
Rien dans le langage ne dit que ces quatre tableaux sont liés : la relation n'existe que dans la tête du programmeur, donc elle se perd. C'est le premier des trois problèmes que l'objet résout — non pas par élégance, mais parce que la cohérence devient impossible à rompre par accident.
/* la règle : un document indisponible ne s'emprunte pas */ /* mais N'IMPORTE OÙ dans le programme, on peut écrire : */ dispo[i] = false; /* dans emprunter() */ dispo[i] = false; /* dans inventaire() — par erreur */ dispo[i] = true; /* dans importer() — sans vérifier */ 11 endroits touchent dispo, 11 endroits peuvent casser la règle
Une règle métier écrite dans un commentaire n'est pas une règle : c'est un espoir. Tant que le champ est accessible partout, la vérifier partout est le seul moyen — et il suffit d'un oubli. C'est le second problème : rien ne PROTÈGE l'invariant.
public class Document {
private String titre;
private String auteur;
private int annee;
private boolean disponible = true;
public Document(String titre, String auteur, int annee) {
this.titre = titre;
this.auteur = auteur;
this.annee = annee;
}
public boolean emprunter() {
if (!disponible) return false; /* la règle, à UN seul endroit */
disponible = false;
return true;
}
public void rendre() { disponible = true; }
public boolean estDisponible() { return disponible; }
}Les données et les opérations qui les gouvernent sont au même endroit, et l'extérieur ne peut plus toucher disponible directement. La règle métier n'est plus un commentaire : elle est le seul chemin possible. Trier un tableau de Document déplace chaque objet entier — le désalignement de l'étape 3 devient inexprimable.
procédural objet lignes 180 210 fichiers à modifier pour un champ 4 1 endroits pouvant casser l'invariant 11 1 désalignement possible au tri oui non
L'objet coûte trente lignes de plus. Il rend en échange trois choses qui ne se mesurent qu'à la maintenance : un seul endroit à modifier, un seul endroit à surveiller, et une classe d'erreurs qui n'existe plus. Sur un programme de deux cents lignes le gain est discutable ; sur dix mille il n'est plus discuté.
Il ne rend pas le programme plus rapide — il ajoute souvent une indirection. Il ne rend pas le code plus court. Et il ne dispense pas de savoir programmer : une classe avec quinze champs publics et aucune méthode est un tableau parallèle déguisé. L'objet n'apporte que ce que vous en faites : encapsuler un invariant, nommer un concept du domaine, et permettre de remplacer une implémentation sans toucher aux appelants.
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
Flashcards · 5 cartes
- Quels sont les quatre symptômes d'un programme procédural qui grossit ?
- 1) La COHÉRENCE n'est garantie par rien : les tableaux parallèles se désynchronisent sans la moindre erreur, et le symptôme apparaît loin de la faute. 2) Les FONCTIONS ENFLENT : cinq puis huit paramètres, dont l'ordre devient une source d'erreurs. 3) TOUT LE MONDE PEUT TOUT MODIFIER : aucune règle ne peut être imposée en un seul endroit. 4) UN CHANGEMENT DE STRUCTURE se propage à tout le code. Cause commune : données et traitements sont séparés.
- Quelle est l'idée centrale de l'objet, et ses deux conséquences immédiates ?
- Mettre ENSEMBLE les données et les opérations qui les concernent. Conséquences : les fonctions maigrissent — emprunter(livre) devient livre.emprunter(), le premier paramètre étant l'objet lui-même ; et 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 la méthode, et tout le code passe par là.
- Quel est le critère opérationnel de la responsabilité d'une classe ?
- 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 ET sauvegarde le fichier » signale trois classes : trois responsabilités indépendantes, qui changeront pour des raisons différentes. La réutilisation vient ensuite, comme effet secondaire d'une bonne délimitation.
- Que faut-il retenir du panorama des langages objet ?
- Simula 67 invente les classes pour la simulation, d'où le vocabulaire. Smalltalk pousse l'idée à son terme : tout est objet. C++ greffe l'objet sur le C, avec héritage multiple et complexité. Java simplifie délibérément et rend les notions EXPLICITES (private, abstract, interface sont des mots-clés), d'où son choix ici. Python et JavaScript relèvent d'autres traditions — typage dynamique, objets à prototypes. Deux lignes de partage : le typage statique ou dynamique, et l'héritage multiple, interdit en Java pour les classes.
- Que l'objet ne résout-il pas ?
- Il n'est pas toujours le bon outil : calcul numérique, filtre de trente lignes, pilote de périphérique n'y gagnent rien. Il ne rend pas un mauvais découpage bon — une hiérarchie mal pensée est plus difficile à corriger qu'un mauvais découpage en fonctions, les dépendances y étant plus profondes. Et il COÛTE : plus de fichiers, plus de vocabulaire, une indirection. Le seuil de rentabilité est là où plusieurs personnes travaillent sur un code qui va vivre des années.