Chapitre 227 minIntermédiaire
Reprendre un code existant : lire, comprendre, rétro-concevoir
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- explorer un projet inconnu dans le bon ordre, sans tout lire ;
- suivre le parcours d'une fonctionnalité de la route à la base ;
- interroger l'historique Git pour comprendre pourquoi le code est ainsi ;
- reconstituer un diagramme de classes et un schéma de base à partir du code.
4 exercices corrigés1 schéma
Reprendre un code existant, c'est comprendre assez une application que tu n'as pas écrite pour la modifier sans la casser : à quoi elle sert, comment elle se lance, comment ses fichiers s'enchaînent et pourquoi elle est écrite ainsi. La rétro-conception (reverse engineering) va un cran plus loin : elle reconstitue, à partir du code et de la base, les modèles qui manquent, comme un diagramme de classes ou un schéma de base de données. Ce chapitre donne une méthode d'exploration, les commandes Git qui racontent l'histoire du code et les techniques de rétro-conception.
Avant de lire : trois réflexes
Tu arrives sur Sorbier Véhicules. L'alternant qui l'a écrite est parti, le README tient en trois lignes, et ton premier ticket t'attend. Avant d'ouvrir un fichier, adopte trois réflexes.
Ne pas réécrire. Devant un code inconnu, la tentation est de tout refaire « proprement ». C'est presque toujours une erreur : le code existant contient des années de corrections de cas particuliers que personne n'a documentés. Tu les perdrais, et tu réintroduirais leurs bugs. On améliore un code existant par petites touches, protégées par des tests (chapitre 8).
Supposer qu'il y a une raison. Une ligne qui te paraît inutile ou absurde a souvent une raison que tu ne connais pas encore : un client particulier, un bug de navigateur, une contrainte de la base. Avant de la supprimer, cherche pourquoi elle est là. L'historique Git te le dira souvent, on y vient plus bas.
Prendre des notes. Ouvre un fichier NOTES.md (ou une page de wiki) et écris au fil de l'eau ce que tu découvres : comment lancer l'application, où est quoi, ce qui t'a surpris, les questions à poser. Ces notes deviendront la documentation qui manquait (chapitre 10).
L'ordre d'exploration
Ne plonge pas au hasard dans cinquante fichiers. Explore dans cet ordre : chaque étape prépare la suivante.
| Étape | Ce que tu regardes | Ce que tu apprends |
|---|---|---|
| 1 | Le README.md | À quoi sert l'application, comment l'installer et la lancer |
| 2 | package.json ou composer.json | Le langage et ses versions, les bibliothèques, les commandes du projet |
| 3 | L'arborescence | L'organisation : MVC, couches, dossiers de tests |
| 4 | La configuration (.env.example, docker-compose.yml, config/) | Les services nécessaires : base, ports, variables d'environnement |
| 5 | Le point d'entrée (server.js, public/index.php) | Comment tout se branche au démarrage |
| 6 | Un seul parcours, de la route jusqu'à la base | Comment une fonctionnalité traverse les couches |
| 7 | Les tests | Le comportement attendu, écrit noir sur blanc |
| 8 | L'historique Git | Pourquoi le code est comme il est |
Lire les fichiers de dépendances
Le fichier de dépendances est la carte d'identité technique du projet. Voici celui de l'API de Sorbier Frais :
{
"name": "sorbier-frais-api",
"version": "1.4.2",
"private": true,
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js",
"test": "jest"
},
"dependencies": {
"express": "^5.1.0",
"jsonwebtoken": "^8.5.1",
"mysql2": "^3.11.0"
},
"devDependencies": {
"jest": "^30.0.0",
"supertest": "^7.0.0"
},
"engines": { "node": ">=22" }
}
En vingt secondes, tu sais que l'application est en version 1.4.2, qu'elle demande Node.js 22 ou plus, qu'elle utilise Express 5, des jetons JWT et MySQL, et que ses tests tournent avec Jest et Supertest. Surtout, les scripts te donnent les vraies commandes : npm run dev lance le serveur en rechargement automatique, npm test lance les tests. Quand le README est faux ou absent, ce sont eux qui disent la vérité.
Côté PHP, Sorbier Véhicules a son composer.json :
{
"name": "sorbier/vehicules",
"type": "project",
"require": {
"php": ">=8.4",
"twig/twig": "^3.0",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^13.0"
},
"autoload": {
"psr-4": { "Sorbier\\Vehicules\\": "src/" }
},
"scripts": {
"test": "phpunit tests"
}
}
PHP 8.4 au minimum, Twig pour les gabarits HTML, Monolog pour les journaux, PHPUnit pour les tests. La section autoload t'apprend que la classe Sorbier\Vehicules\Service\Disponibilite se trouve dans src/Service/Disponibilite.php : c'est la convention PSR-4, qui fait correspondre l'espace de noms au chemin du fichier.
Lancer l'application et ses tests en premier
Avant de chercher un bug ou d'ajouter quoi que ce soit, fais tourner le projet tel qu'il est. C'est ton point de référence.
# API Node.js
cp .env.example .env # puis renseigne les variables
docker compose up -d # la base MySQL du projet, si elle est fournie
npm ci # installe exactement les versions du package-lock.json
npm test
# Application PHP
composer install # installe exactement les versions du composer.lock
composer test
npm ci et composer install installent les versions figées dans le fichier de verrouillage (package-lock.json, composer.lock), celles avec lesquelles le projet a été testé. npm install ou composer update pourraient installer des versions plus récentes et changer le comportement avant même que tu aies touché au code.
Si des tests échouent avant ton intervention, note-le : ils ne sont pas de ton fait, et tu dois le savoir pour ne pas les confondre avec une régression que tu aurais introduite. Si l'application ne démarre pas parce que le README est périmé, reconstitue les étapes à partir des scripts et du docker-compose.yml, puis corrige le README : c'est déjà de la maintenance.
Suivre le parcours d'une fonctionnalité
Prenons une fonctionnalité de Sorbier Frais : afficher le résumé d'une note (mois, nombre de lignes, total). La requête GET /api/notes/42/resume traverse quatre couches.
La route associe une URL et une méthode HTTP à une fonction. Elle dit aussi quels middlewares s'exécutent avant.
// routes/notes.routes.js
const express = require('express');
const { requireAuth } = require('../middlewares/auth');
const notesController = require('../controllers/notes.controller');
const router = express.Router();
router.get('/:id/resume', requireAuth, notesController.getResume);
module.exports = router;
Le contrôleur traduit le HTTP en appel métier, et le résultat en réponse. Dans une application bien découpée, il est mince : quelques lignes.
// controllers/notes.controller.js
const notesRepo = require('../repositories/notes.repository');
const { resumeNote } = require('../services/notes');
async function getResume(req, res) {
const note = await notesRepo.trouverParId(req.params.id);
if (note === null) return res.status(404).json({ erreur: 'Note introuvable' });
res.json(resumeNote(note));
}
module.exports = { getResume };
Le service porte la logique : calculs, règles de gestion. C'est là que vivent la plupart des bugs de calcul.
// services/notes.js
function totalNote(lignes) {
return lignes.reduce((total, ligne) => total + ligne.montant, 0);
}
function resumeNote(note) {
return {
mois: note.mois,
nbLignes: note.lignes.length,
total: totalNote(note.lignes),
};
}
module.exports = { totalNote, resumeNote };
Le dépôt (repository) parle à la base, avec des requêtes préparées.
// repositories/notes.repository.js
const pool = require('../db');
async function trouverParId(id) {
const [notes] = await pool.execute(
'SELECT id_note, mois, etat FROM note_frais WHERE id_note = ?',
[id],
);
if (notes.length === 0) return null;
const [lignes] = await pool.execute(
'SELECT categorie, libelle, montant FROM ligne_frais WHERE id_note = ?',
[id],
);
return { ...notes[0], lignes };
}
module.exports = { trouverParId };
En quatre fichiers, tu connais l'architecture de toute l'API : les autres fonctionnalités suivent le même chemin. Tu as aussi repéré, sans le chercher, un endroit fragile : totalNote additionne ligne.montant sans vérifier son type. Garde-le dans tes notes ; on y revient au chapitre suivant.
Côté PHP, Sorbier Véhicules suit le même principe avec un contrôleur frontal : toutes les requêtes passent par public/index.php, qui choisit le contrôleur selon l'URL.
<?php
// public/index.php (extrait)
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Sorbier\Vehicules\Controleur\ReservationControleur;
$action = $_GET['action'] ?? 'planning';
$controleur = new ReservationControleur(/* dépendances : PDO, Twig, journal */);
match ($action) {
'planning' => $controleur->planning(),
'reserver' => $controleur->reserver($_POST),
'annuler' => $controleur->annuler((int) ($_POST['id'] ?? 0)),
default => http_response_code(404),
};
Le match est la table des routes. Pour comprendre « réserver », tu suis ReservationControleur::reserver(), puis le service Disponibilite, puis le dépôt et sa requête SQL. Le cours Architecture MVC détaille cette organisation en couches.
Naviguer dans le code avec VS Code
Tu n'as pas à deviner où une fonction est définie ni qui l'appelle : l'éditeur le sait. Sur Windows et Linux, Ctrl ; sur macOS, Cmd (sauf mention contraire).
| Raccourci | Effet | À quoi ça sert en maintenance |
|---|---|---|
F12 | Aller à la définition | Descendre d'une couche : du contrôleur au service |
Maj+F12 | Afficher les références | Savoir qui appelle une fonction avant de la modifier |
Ctrl+P | Ouvrir un fichier par son nom | Aller vite sans fouiller l'arborescence |
Ctrl+Maj+F | Chercher dans tous les fichiers | « Où est calculé le total ? », « qui lit montant ? » |
Ctrl+Maj+O | Lister les symboles du fichier | Voir les fonctions d'un long fichier |
Alt+← (Ctrl+- sur macOS) | Revenir à l'endroit précédent | Remonter après une série de F12 |
Maj+F12 répond à la question la plus importante avant toute modification : si je touche à cette fonction, qui risque de casser ? Sur Sorbier Frais, totalNote est appelée par le résumé, par l'export CSV et par le tableau de bord du comptable. Une correction devra être testée sur les trois.
Interroger l'historique Git
Le code dit comment l'application fonctionne. L'historique dit pourquoi elle est ainsi. Quatre commandes suffisent pour l'interroger.
git log --oneline -- services/notes.js # les commits qui ont touché ce fichier
git blame -L 1,15 services/notes.js # le dernier commit de chaque ligne
git log -S "ligne.montant" --oneline # les commits qui ont ajouté ou retiré ce texte
git show 3f9c2ab # le détail d'un commit : message et modifications
git blame affiche, pour chaque ligne, le commit, l'auteur et la date de sa dernière modification :
3f9c2ab1 (Inès Martin 2026-10-30 10:14:02 +0100 2) function totalNote(lignes) {
3f9c2ab1 (Inès Martin 2026-10-30 10:14:02 +0100 3) return lignes.reduce((total, ligne) => total + ligne.montant, 0);
3f9c2ab1 (Inès Martin 2026-10-30 10:14:02 +0100 4) }
Un git show 3f9c2ab affiche ensuite le message du commit, par exemple refactor(notes): calcule le total côté API (#229). Tu sais maintenant que le total était calculé ailleurs avant le 30 octobre, que le changement répondait au ticket 229 et à qui poser la question. Le nom « blame » (« blâmer ») est trompeur : la commande sert à trouver le contexte d'une ligne, pas un coupable.
git log -S (la « pioche ») retrouve le commit qui a fait apparaître ou disparaître un morceau de code précis, même s'il a changé de fichier depuis. C'est l'outil pour répondre à « depuis quand ce code est-il là ? ».
La rétro-conception : reconstituer les modèles
La rétro-conception consiste à remonter du code et des données vers les modèles qui les décrivent : schéma de base, diagramme de classes, diagramme de séquence. Elle sert quand ces modèles n'existent pas, sont perdus ou ne correspondent plus au code. En maintenance, c'est fréquent.
De la base au schéma relationnel
La base est souvent la partie la plus stable et la plus parlante d'une application. Commence par elle.
SHOW TABLES;
SHOW CREATE TABLE reservation;
SHOW CREATE TABLE donne le script complet d'une table, avec ses clés et ses contraintes. Pour lister d'un coup toutes les clés étrangères, interroge le dictionnaire de données de MySQL :
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'sorbier_vehicules'
AND REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY TABLE_NAME;
| TABLE_NAME | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
|---|---|---|---|
| reservation | id_utilisateur | utilisateur | id_utilisateur |
| reservation | id_vehicule | vehicule | id_vehicule |
| utilisateur | id_service | service | id_service |
Tu en déduis le schéma relationnel, au format des sujets (clé primaire en gras, clés étrangères précédées de #) :
service (id_service, libelle)
utilisateur (id_utilisateur, nom, prenom, email, #id_service)
vehicule (id_vehicule, immatriculation, modele, places, km_compteur)
reservation (id_reservation, debut, fin, motif, km_depart, km_retour, statut, #id_vehicule, #id_utilisateur)
Et de là, le MCD : une réservation concerne un véhicule et est faite par un utilisateur, qui appartient à un service. Les outils comme DBeaver ou MySQL Workbench dessinent aussi le diagramme de la base automatiquement (fonction de rétro-ingénierie ou onglet « Diagramme ER »). Le dessin est pratique, mais lis les contraintes toi-même : un outil ne te dira pas qu'une colonne statut en VARCHAR n'accepte en pratique que trois valeurs, ni qu'une clé étrangère manque. Le cours Merise rappelle le passage du schéma relationnel au MCD.
Du code au diagramme de classes
Pour une application objet, ouvre les classes du modèle et note, pour chacune, ses attributs, ses méthodes publiques et ses liens avec les autres.
<?php
// src/Modele/Reservation.php
declare(strict_types=1);
namespace Sorbier\Vehicules\Modele;
use DateTimeImmutable;
final class Reservation
{
public function __construct(
private ?int $id,
private Vehicule $vehicule,
private Utilisateur $demandeur,
private DateTimeImmutable $debut,
private DateTimeImmutable $fin,
private string $statut = 'confirmee',
) {}
public function annuler(): void
{
$this->statut = 'annulee';
}
public function dureeEnHeures(): float
{
return ($this->fin->getTimestamp() - $this->debut->getTimestamp()) / 3600;
}
}
Le constructeur te donne les attributs, et leurs types te donnent les associations : une Reservation connaît un Vehicule et un Utilisateur. Dans le diagramme de classes, cela donne deux associations orientées, de multiplicité 1 côté véhicule et côté utilisateur. Les méthodes publiques annuler() et dureeEnHeures() deviennent les opérations de la classe. Les outils de génération (extensions de VS Code, PhpStorm, Visual Studio pour le C#) produisent ce diagramme, mais le faire à la main sur trois ou quatre classes centrales t'oblige à les comprendre. Le chapitre Du diagramme de classes au code C#, et rétro-conception du cours UML donne les notations et la démarche.
Du parcours au diagramme de séquence
Le parcours que tu as suivi plus haut (route, contrôleur, service, dépôt, base) se dessine directement en diagramme de séquence : une ligne de vie par objet, une flèche par appel, dans l'ordre. C'est le modèle le plus utile pour expliquer à quelqu'un, ou à une commission, où se place ta modification.
Quand le code résiste
Certains morceaux restent obscurs même après lecture : une fonction de 200 lignes, des noms d'une lettre, une logique imbriquée sur cinq niveaux. Quelques techniques aident.
- L'exécuter plutôt que la lire. Pose un point d'arrêt à l'entrée et avance pas à pas en regardant les variables (chapitre 4). Voir les valeurs réelles vaut mieux que de les imaginer.
- Écrire des tests de caractérisation. Appelle la fonction avec des entrées variées et fige ses résultats actuels dans des tests, justes ou faux. Tu documentes ainsi son comportement, et tu disposes d'un filet pour la modifier (chapitre 8).
- Dessiner. Un organigramme ou une table de décision sur papier débrouille souvent une cascade de
if. - Lire les tests existants. Un test bien nommé (
refuse une réservation qui chevauche une autre) explique une règle mieux qu'un commentaire. - Demander. À l'auteur s'il est joignable, à un collègue, à l'utilisateur qui connaît le métier. Dix minutes de question économisent souvent une journée de lecture.
Un assistant d'intelligence artificielle peut aussi résumer ce que fait une fonction ou proposer des hypothèses. Traite ses réponses comme celles d'un collègue qui n'a pas lu le reste du projet : utiles pour démarrer, à vérifier en exécutant le code. Et ne lui colle jamais un code qui contient des secrets ou des données personnelles.
Exercices
Exercice 1 sur 4 Lire un package.json Réussi À revoir
Voici le package.json d'une application que tu reprends :
{
"name": "gestion-stages",
"version": "2.3.0",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "jest --coverage",
"lint": "eslint src"
},
"dependencies": { "express": "^5.1.0", "mysql2": "^3.11.0", "bcrypt": "^6.0.0" },
"devDependencies": { "jest": "^30.0.0", "eslint": "^10.0.0" }
}
- Quel est le point d'entrée de l'application ?
- Quelle commande lance les tests, et que produit-elle en plus des résultats ?
- Quel indice te dit que l'application stocke des mots de passe ?
- Quelle commande utiliser pour installer les dépendances sans risquer de changer leurs versions ?
- La version
2.3.0est-elle une information sur Express, sur Node.js ou sur l'application ?
Voir la réponseMasquer la réponse de l'exercice 1
src/index.js, lancé parnpm start(ounpm run deven rechargement automatique).npm test, qui exécutejest --coverage: en plus des résultats, Jest produit un rapport de couverture de code (la part des lignes exécutées par les tests).- La dépendance
bcrypt, une bibliothèque de hachage de mots de passe. npm ci, qui installe exactement les versions dupackage-lock.json. Encore faut-il que ce fichier soit dans le dépôt.- Sur l'application elle-même : c'est son numéro de version, au format SemVer.
Tu avais trouvé ?
Exercice 2 sur 4 Interpréter un git blame Réussi À revoir
Dans Sorbier Véhicules, la fonction qui vérifie la disponibilité d'un véhicule contient une ligne surprenante :
a71e09d3 (Hugo Bernard 2025-11-04 16:22:41 +0100 31) if ($vehicule->immatriculation() === 'GA-482-LK') { return false; }
- Que fait cette ligne ?
- Que fais-tu avant de la supprimer ?
- Quelle commande affiche le message et le contenu du commit qui l'a introduite ?
Voir la réponseMasquer la réponse de l'exercice 2
- Elle déclare un véhicule précis toujours indisponible, quelle que soit la date.
- Tu cherches pourquoi :
git show a71e09d3pour lire le message et le ticket associé, puis tu demandes au responsable du parc. Le véhicule est peut-être immobilisé pour une longue réparation, ou réservé à la direction. Si la règle est toujours valable, elle ne doit pas rester codée en dur : elle doit devenir une donnée (une colonnedisponibleou une table d'immobilisations), ce qui est une évolution à proposer. Si elle ne l'est plus, tu la supprimes dans un commit à part, avec un message qui explique pourquoi. git show a71e09d3.
Tu avais trouvé ?
Exercice 3 sur 4 Rétro-concevoir un schéma Réussi À revoir
Voici deux tables extraites d'une base sans documentation :
CREATE TABLE stage (
id INT AUTO_INCREMENT PRIMARY KEY,
etudiant_id INT NOT NULL,
entreprise_id INT NOT NULL,
tuteur_id INT NULL,
debut DATE NOT NULL,
fin DATE NOT NULL,
FOREIGN KEY (etudiant_id) REFERENCES etudiant (id),
FOREIGN KEY (entreprise_id) REFERENCES entreprise (id)
);
CREATE TABLE etudiant (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
classe VARCHAR(10) NOT NULL
);
- Écris le schéma relationnel de
stageau format des sujets. - Que signifie
tuteur_id INT NULL? - Quelle anomalie remarques-tu sur
tuteur_id, et comment vérifier vers quelle table il pointe ?
Voir la réponseMasquer la réponse de l'exercice 3
- stage (id, debut, fin, #etudiant_id, #entreprise_id, tuteur_id), avec
etudiant_iden référence àiddeetudiantetentreprise_iden référence àiddeentreprise. - Un stage peut ne pas avoir de tuteur (encore) : la colonne accepte
NULL, la relation est facultative. tuteur_idporte le nom d'une clé étrangère mais n'en est pas une : aucune contrainteFOREIGN KEYne la déclare, donc le SGBD accepte n'importe quel nombre. Pour savoir vers quelle table elle pointe, cherche les requêtes du code qui l'utilisent dans une jointure (git grep -n "tuteur_id"), puis vérifie les données (SELECT DISTINCT tuteur_id …comparé aux identifiants de la table candidate). Une fois la relation confirmée, ajouter la contrainte est une bonne maintenance préventive, après avoir corrigé les valeurs orphelines.
Tu avais trouvé ?
Exercice 4 sur 4 Dans quel ordre ? Réussi À revoir
Tu reprends une application PHP inconnue et tu as une matinée pour corriger un premier bug. Remets ces actions dans l'ordre : lancer les tests ; lire le README ; corriger le bug ; suivre le parcours de la page concernée ; lire composer.json ; reproduire le bug ; installer les dépendances avec composer install.
Voir la réponseMasquer la réponse de l'exercice 4
Lire le README, lire composer.json, installer les dépendances avec composer install, lancer les tests (pour connaître l'état de départ), suivre le parcours de la page concernée, reproduire le bug, corriger le bug.
On peut reproduire le bug avant de suivre le parcours si l'on sait déjà le déclencher depuis l'interface : les deux ordres se défendent. Ce qui ne se défend pas, c'est de corriger avant d'avoir reproduit.
Tu avais trouvé ?