Langage des exemples de code

Ce qui rend un changement coûteux, et pourquoi les agents IA aggravent la note

Du code facile à changer, épisode 1


Dans l’épisode précédent, j’ai proposé de remplacer « propre » par une question : à quel point ce code est-il facile et sûr à changer ?

La question est importante, mais elle reste abstraite tant qu’on ne sait pas d’où vient le coût d’un changement. Je vais vous proposer une réponse dans cet épisode.

Il n’y a, en pratique, que trois raisons pour lesquelles une modification prend une journée au lieu d’une heure. Elles serviront de grille de lecture à toute la suite de la série.

Une précision de vocabulaire avant de commencer, parce qu’elle évite un contresens dans toute la série. Ces trois raisons décrivent un coût : ce qu’on paie le jour du changement. Mais le coût n’est que le dernier maillon d’une chaîne qui en compte trois.

  • À la racine, un problème de conception. Une décision du métier éparpillée, une responsabilité au mauvais endroit, une règle qui n’est écrite nulle part.
  • En surface, un code smell. C’est la forme visible de ce problème, ce qu’on remarque en lisant le code : une fonction de trois cents lignes, trois switch sur la même valeur.
  • Au moment du changement, le coût. Une journée au lieu d’une heure, pour l’une des raisons que je vais détailler ici.

Un smell n’est pas une cause. C’est un symptôme : il révèle le problème de conception, et il annonce le coût. La nuance a une conséquence très pratique. Si on prend le symptôme pour la cause, on se contente de le faire disparaître, mais le problème reste parfois en place quand même.

Et puisqu’on ne peut plus parler de code en 2026 sans parler des agents qui l’écrivent, je termine par une conviction que je n’avais pas encore écrite ici : avec des agents IA, la maintenabilité ne compte pas moins qu’avant.

Elle compte davantage.

Cause 1 : il faut comprendre avant de toucher !

La plus grande partie d’un changement n’est pas l’écriture du code. C’est sa lecture. Avant de modifier une ligne, il faut savoir ce que fait le code autour, pourquoi il le fait, et les impacts qu’un changement peut produire.

Ce temps de compréhension explose dans quelques situations bien connues.

  • Le code est long et mélange les niveaux d’abstraction. Une même fonction valide une saisie, calcule une remise et formate un courriel. Pour changer la remise, il faut lire les trois.
  • Les noms mentent. Une méthode getXXX() qui crée l’objet s’il n’existe pas. Une variable total qui contient un sous-total hors taxes. Chaque nom trompeur oblige à vérifier tout ce qui l’entoure.
  • La connaissance est implicite. Une règle métier n’est écrite nulle part, elle est encodée dans l’ordre de trois if. Personne ne sait plus pourquoi le deuxième vient avant le premier, et personne n’ose les inverser.
  • Le code produit des effets de bord invisibles. Une modification dans une fonction peut impacter d’autres parties du code sans que cela soit évident. La création d’un nouvel objet qui envoie un mail, un setter qui provoque une modification en BDD, etc. Tous ces effets de bord invisibles rendent les modifications périlleuses.
Pour changer la remise, il faut tout lire
export function checkout(cart: Cart, customer: Customer): Receipt {
  if (cart.items.length === 0) throw new Error('Panier vide');
  if (!customer.email.includes('@')) throw new Error('Email invalide');

  let total = 0;
  for (const item of cart.items) total += item.price * item.quantity;

  if (customer.orders > 10) total *= 0.9;
  if (cart.coupon === 'RENTREE') total -= 5;
  if (total < 0) total = 0;

  const vat = total * 0.2;
  sendEmail(customer.email, `Merci ! Total : ${(total + vat).toFixed(2)} €`);

  return { total, vat };
}
public Receipt Checkout(Cart cart, Customer customer)
{
    if (cart.Items.Count == 0) throw new InvalidOperationException("Panier vide");
    if (!customer.Email.Contains('@')) throw new ArgumentException("Email invalide");

    decimal total = 0;
    foreach (var item in cart.Items) total += item.Price * item.Quantity;

    if (customer.Orders > 10) total *= 0.9m;
    if (cart.Coupon == "RENTREE") total -= 5;
    if (total < 0) total = 0;

    var vat = total * 0.2m;
    _mailer.Send(customer.Email, $"Merci ! Total : {total + vat:F2} €");

    return new Receipt(total, vat);
}

Le métier demande : “la remise fidélité ne doit plus se cumuler avec un coupon”.

La modification tient en deux lignes. Mais pour les écrire sans crainte, il faut d’abord comprendre que total est hors taxes, que l’ordre remise puis coupon compte, que le plancher à zéro protège contre un coupon supérieur au panier, et que le courriel affiche un montant toutes taxes comprises. Rien de tout ça n’est dit. Tout se déduit.

C’est le coût que signalent les Bloaters (Long Method, Large Class, Long Parameter List) et une partie des Dispensables. Le problème de conception, en dessous, est presque toujours le même : trop de choses rassemblées au même endroit, si bien que chaque lecture se paie.

Cause 2 : il faut toucher à plusieurs endroits

Deuxième situation : on sait exactement quoi changer, mais le changement est éparpillé. Une seule décision du métier a été recopiée, sous des formes différentes, à plusieurs endroits du code.

Reprenons les transporteurs du manifeste, et ajoutons-en un.

Ajouter DHL : combien d'endroits ?
type Carrier = 'colissimo' | 'chronopost' | 'relay' | 'dhl';

// shipping.ts
export function shippingFee(carrier: Carrier, weightKg: number): number {
  switch (carrier) { /* un cas par transporteur */ }
}
export function deliveryDelayDays(carrier: Carrier): number {
  switch (carrier) { /* un cas par transporteur */ }
}
export function carrierLabel(carrier: Carrier): string {
  switch (carrier) { /* un cas par transporteur */ }
}

// export/orders-csv.ts, écrit six mois plus tard par quelqu'un d'autre
const isExpress = (carrier: Carrier): boolean => carrier === 'chronopost';
public enum Carrier { Colissimo, Chronopost, Relay, Dhl }

// Shipping.cs
public static decimal Fee(Carrier carrier, decimal weightKg) => carrier switch { /* ... */ };
public static int DelayDays(Carrier carrier) => carrier switch { /* ... */ };
public static string Label(Carrier carrier) => carrier switch { /* ... */ };

// Export/OrdersCsv.cs, écrit six mois plus tard par quelqu'un d'autre
private static bool IsExpress(Carrier carrier) => carrier == Carrier.Chronopost;

Trois fonctions dans un fichier, plus une comparaison isolée dans un export. DHL est un transporteur express. Qui pensera à l’export ?

Le langage aide un peu, et c’est instructif de voir où il s’arrête. En TypeScript, un switch sur une union sans branche par défaut devient une erreur de compilation dès qu’un cas manque. Les trois fonctions seront donc signalées. Mais la comparaison carrier === 'chronopost' compile très bien avec DHL : elle répond simplement false. En C#, la branche par défaut des trois switch lève une exception, donc l’oubli ne se voit qu’à l’exécution, et l’export ne se voit pas du tout.

C’est ce qu’annoncent les Change Preventers (Shotgun Surgery, Divergent Change), les Object-Orientation Abusers comme les switch répétés, et une bonne partie des Couplers. Le problème de conception, ici, est que la notion de transporteur est éparpillée dans le code. Et le coût n’est pas seulement le temps de modifier quatre fichiers. C’est aussi le risque d’en oublier un.

Cause 3 : on ne sait pas ce qu’on va casser

Troisième situation : le changement est compris, il est localisé, il est même simple. Mais rien ne permet de savoir s’il casse quelque chose ailleurs.

Alors on fait ce que font toutes les équipes dans ce cas. On teste à la main, longtemps. On ajoute un flag pour pouvoir revenir en arrière. On repousse la modification au sprint suivant. Ou, le plus souvent, on contourne : plutôt que de modifier la fonction existante, on en écrit une deuxième à côté, presque identique. Ce qui nourrit la cause 2 pour le prochain changement.

Cette cause-là a un remède principal, et ce sont les tests. Pas n’importe lesquels : des tests qui vérifient le comportement plutôt que l’implémentation, qui tournent vite, et qui disent clairement ce qui a cassé. Un code couvert par de tels tests peut être laid, long et mal nommé, il reste sûr à changer. L’inverse n’est pas vrai.

C’est aussi ce que le couplage et l’état partagé aggravent, et que les Couplers aident à repérer : plus un module dépend des détails internes d’un autre, plus un changement local a de chances de produire un effet lointain.

La grille de la série

Chaque famille de code smells que la série va parcourir révèle surtout un type de problème de conception, et annonce surtout l’une de ces trois causes de coût.

Le smell, en surface Le problème de conception, à la racine Le coût potentiel
Bloaters Trop de responsabilités rassemblées au même endroit Compréhension difficile
Change Preventers Une décision du métier répartie dans le code Multiples changements
Object-Orientation Abusers Une abstraction absente ou mal ajustée Multiples changements
Dispensables Du code qui n’a pas, ou plus, de raison d’être Compréhension difficile
Couplers Des modules qui dépendent des détails internes des autres Multiples changements, et risque de régressions

Retiens surtout ceci : un smell n’est pas grave en soi. Il est grave dans la mesure où le code qu’il touche va changer. Une Long Method que personne ne modifie ne coûte rien. Par contre, dans le module que le métier fait évoluer chaque semaine, on doit payer à chaque fois.

Et quand c’est un agent qui écrit le code ?

C’est la première fois que j’en parle sur ce blog, alors je pose ma position clairement.

On entend souvent l’argument suivant : “Si un agent IA écrit et modifie le code, peu importe qu’il soit facile à comprendre ou à changer. L’agent lit tout, modifie tout, et réécrira si besoin.” La maintenabilité serait une préoccupation d’humains, appelée à disparaître avec eux.

Je pense exactement l’inverse. Je l’ai défendu dans ma conférence Les Deux Tours : l’écriture du code n’a jamais été le goulet d’étranglement. Le goulet, c’est de comprendre le code, le maintenir et le faire évoluer. Un agent accélère l’écriture. Il ne supprime aucune des trois causes. Il les amplifie.

Certes, quand on passe d’un développement 100% humain à un développement agentique on a l’impression de gagner en vélocité. Mais une fois cette impression d’accélérateur passée, on se retrouve avec les mêmes problèmes qu’avant, avec en plus de la dette technique amplifiée, un risque de dette de conception et de dette de compréhension décuplé.

Un agent paie aussi pour comprendre

Un agent ne « lit pas tout ». Il travaille avec une fenêtre de contexte limitée, et chaque fichier qu’il ouvre pour comprendre l’existant la remplit. Une fonction de trois cents lignes qui mélange quatre responsabilités lui coûte du contexte, du temps et de l’argent, exactement comme elle coûte de l’attention à un humain.

Et quand il comprend mal, il ne le dit pas. Un nom qui ment trompe un agent aussi sûrement qu’un développeur. La différence, c’est que l’agent livrera quand même une modification complète et assurée, bâtie sur sa mauvaise lecture.

Un agent oublie l’export

Reprenons l’exemple de DHL. Un agent à qui l’on demande d’ajouter le transporteur va modifier le type. Le compilateur va lui signaler les trois switch, il va les compléter, la compilation va passer, et il va annoncer que c’est terminé.

La comparaison dans l’export, rien ne la lui signale. Elle compile. Elle répond false. Un humain qui connaît le projet a une chance de s’en souvenir. L’agent n’en a aucune, sauf si le code rend ce lien visible.

Un agent est remarquablement efficace pour un changement local. Il est mauvais pour retrouver tous les endroits où une même décision a été recopiée sous des formes différentes. Autrement dit, il est mauvais exactement là où la cause 2 fait mal.

Un agent reproduit ce qu’il voit

Un agent écrit du code qui ressemble au code déjà présent dans son contexte. C’est même ce qu’on attend de lui : respecter les conventions du projet.

Mais il respecte aussi les smells. S’il trouve trois switch sur le transporteur, il en écrira un quatrième pour la prochaine fonctionnalité. Si la logique est dupliquée, il dupliquera. Un humain qui duplique produit un smell par jour. Un agent qui duplique en produit un par minute.

Sans feedback sur la structure, on produit du couplage plus vite.

Un agent a besoin de tests pour savoir qu’il a fini

La cause 3 est sans doute la plus importante. Un agent n’a pas d’intuition sur ce qu’il a pu casser. Il a ce que le projet lui donne : un compilateur, des tests, un linter. Éventuellement il peut piloter un navigateur si on lui demande gentiment.

C’est sa seule boucle de feedback.

Dans un code couvert par des tests de comportement rapides, un agent peut itérer : il modifie, les tests passent ou cassent, il corrige. Dans un code sans tests, il modifie, rien ne proteste, et il déclare la tâche terminée avec la même assurance que si tout était vérifié. Et s’il écrit lui-même les tests après coup, il valide ce qu’il vient de produire, avec le biais de confirmation poussé à son maximum.

Et la relecture devient le goulet

Il reste une dernière conséquence. Quand un agent écrit le code, un humain doit encore le relire et en répondre. Le temps gagné à l’écriture se retrouve à la relecture. Et relire un diff qui touche des dizaines de fichiers, dans un code qu’on comprend mal, sans tests pour confirmer, c’est précisément l’exercice le plus coûteux qui soit.

Si vous voulez générer de l’épuisement professionnel dans vos équipes, c’est un excellent moyen !

Un code facile à comprendre, où chaque décision vit à un seul endroit, et couvert par des tests, produit des diffs courts, localisés et vérifiés. Ce sont les seuls qu’on peut relire vite sans relire mal.

Ce que j’ai envie de dire en résumé

Les trois causes de coût ne sont pas des faiblesses humaines que les agents viennent corriger. Ce sont des propriétés du code. Elles s’appliquent à quiconque le modifie, humain ou machine, et la machine les subit en accéléré.

Le code facile à changer n’est pas devenu un luxe. C’est ce qui sépare une équipe qui va plus vite avec des agents d’une équipe qui produit de la dette plus vite.

La suite

Dans le prochain épisode, je vais m’attaquer à une famille de code smells que je vois bien trop souvent, aussi bien auprès de mes étudiants que dans les entreprises.

Tu as envie de lire la suite ? Abonne-toi à mon flux RSS pour être tenu au courant en direct de mes publications !


Tu veux creuser ces sujets avec ton équipe ? J’interviens en Brown Bag Lunch dans vos locaux, ou par mail à edouard@lacrafterie.tech.

Version markdown de cet article