DEV Community

Rodolphe D.
Rodolphe D.

Posted on

Pourquoi ton code prépare un futur qui n'arrivera jamais (YAGNI)

TL;DR

J'ai passé un week-end à rendre mon appli de factures multi-devises (avec taux de change en temps réel), multi-langues (4 langues) et exportable vers 3 logiciels comptables. Nombre de factures émises dans une autre devise que l'euro : 0. Nombre de fois où j'ai changé de langue : 0. Nombre d'exports : 0. YAGNI (You Aren't Gonna Need It), c'est : "ne construis pas aujourd'hui ce dont tu pourrais avoir besoin demain." On supprime ~400 lignes et on les remplace par un helper de 3 lignes.


Le design Frontend Mentor de l'Invoice App affiche tous les montants en livres sterling. £ 1,800.90. Moi, je suis freelance à Paris. Je facture en euros.

Un développeur raisonnable aurait remplacé £ par € et serait passé à autre chose.

Moi, je me suis dit : "Et si un jour j'ai un client à Londres ? Ou aux États-Unis ? Ou au Japon ?" Et pendant que j'y étais : "Et si mon appli devenait un SaaS ? Il faudra bien la traduire. Et les comptables vont vouloir un export." Le samedi matin, j'avais un café et une idée. Le dimanche soir, j'avais une table currencies, un service qui appelle une API de taux de change, 4 fichiers de traduction et une interface InvoiceExporter avec 3 implémentations, dont une qui faisait juste throw new Error('TODO').

Six semaines plus tard, aucune de ces fonctionnalités n'a servi une seule fois. Par contre, l'API de taux de change a changé ses conditions d'utilisation et ma page de factures a planté un mardi matin.

Bienvenue dans le syndrome de la boule de cristal : ce moment où tu écris du code pour un client imaginaire, et où c'est ton vrai utilisateur (toi) qui paie l'addition.


Imagine que tu pars un week-end à la mer, en juillet.

Tu fais ta valise. Maillot, t-shirts, crème solaire. Puis le doute s'installe. "Et s'il neige ?" Doudoune. "Et si on m'invite à un mariage ?" Costume. "Et si je dois faire de la plongée ?" Combinaison néoprène. Tu arrives avec trois valises de 23 kg, tu paies le supplément bagages, tu te bloques le dos dans l'escalier de la location. Et tu passes le week-end en maillot.

Le pire, c'est que si tu avais vraiment eu besoin d'un pull, tu l'aurais acheté sur place. En dix minutes.

YAGNI (You Aren't Gonna Need It), c'est faire la valise pour le week-end que tu vas réellement passer. Pas pour tous ceux que tu pourrais imaginer. Le principe vient de l'Extreme Programming, et il repose sur un constat simple : quand le besoin futur finit par arriver, il ne ressemble presque jamais à ce que tu avais imaginé.

Le code "avant" (le péché)

La table des devises, pour gérer les 3 décimales du dinar koweïtien (on ne sait jamais) :

// database/migrations/1729000000000_create_currencies_table.ts
export default class extends BaseSchema {
  async up() {
    this.schema.createTable('currencies', (table) => {
      table.string('code', 3).primary()
      table.string('symbol').notNullable()
      table.integer('decimals').defaultTo(2)
      table.string('locale').notNullable()
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Le service de taux de change, avec son cache et sa clé d'API dans le .env :

// app/services/exchange_rate_service.ts
export default class ExchangeRateService {
  private cache = new Map<string, { rates: Record<string, number>; fetchedAt: number }>()

  async convert(amount: number, from: string, to: string) {
    if (from === to) return amount
    const rates = await this.getRates(from)
    return Math.round(amount * rates[to])
  }

  private async getRates(base: string) {
    const cached = this.cache.get(base)
    if (cached && Date.now() - cached.fetchedAt < 3_600_000) return cached.rates

    const res = await fetch(
      `https://api.example-rates.com/latest?base=${base}&key=${env.get('EXCHANGE_API_KEY')}`
    )
    const { rates } = await res.json()
    this.cache.set(base, { rates, fetchedAt: Date.now() })
    return rates
  }
}
Enter fullscreen mode Exit fullscreen mode

Les traductions, en 4 langues, pour un utilisateur qui parle français :

resources/lang/
├── fr/invoices.json
├── en/invoices.json
├── es/invoices.json
└── de/invoices.json   ← traduit avec un outil en ligne, jamais relu
Enter fullscreen mode Exit fullscreen mode
<!-- inertia/pages/invoices/show.vue -->
<span>{{ t('invoices.status.' + invoice.status) }}</span>
<button>{{ t('invoices.actions.mark_as_paid') }}</button>
Enter fullscreen mode Exit fullscreen mode

Et le clou du spectacle : les exports comptables, avec une belle interface pour respecter l'Open/Closed Principle :

// app/exporters/invoice_exporter.ts
export interface InvoiceExporter {
  readonly format: 'csv' | 'fec' | 'sage'
  export(invoices: Invoice[]): Promise<Buffer>
}

// app/exporters/sage_exporter.ts
export class SageExporter implements InvoiceExporter {
  readonly format = 'sage' as const

  async export(_invoices: Invoice[]): Promise<Buffer> {
    throw new Error('TODO: comprendre le format Sage')
  }
}
Enter fullscreen mode Exit fullscreen mode

Faisons les comptes : une migration, un service, un value object Money (que je t'épargne), 4 fichiers de langue, 3 exporters, une variable d'environnement, une dépendance à une API externe. Environ 400 lignes. Et surtout, une taxe sur chaque nouvelle fonctionnalité : chaque texte ajouté doit être traduit 4 fois, chaque montant doit passer par le convertisseur, chaque test doit mocker l'API de taux.

Utilisation réelle de tout ça : zéro.

Le refactor "après" (la rédemption)

Étape 1 — on supprime. La migration currencies, le service, les exporters, les dossiers es/ et de/, la clé d'API. git rm n'a jamais été aussi agréable.

Étape 2 — un seul helper pour formater l'argent :

// inertia/lib/money.ts
const formatter = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' })

export const formatMoney = (cents: number) => formatter.format(cents / 100)
Enter fullscreen mode Exit fullscreen mode

Intl.NumberFormat est natif, ne coûte rien, et gère déjà les espaces insécables et la virgule française. Les montants restent stockés en centimes (des entiers), pas en float : ça, c'est une décision qui coûte cher à changer plus tard, donc elle mérite d'être prise maintenant.

Étape 3 — les textes sont écrits en français, directement dans les templates :

<!-- inertia/pages/invoices/show.vue -->
<script setup lang="ts">
import { formatMoney } from '~/lib/money'

defineProps<{ invoice: Invoice }>()
</script>

<template>
  <span>{{ invoice.status }}</span>
  <p>Total : {{ formatMoney(invoice.totalCents) }}</p>
  <button v-if="invoice.status === 'pending'">Marquer comme payée</button>
</template>
Enter fullscreen mode Exit fullscreen mode

Étape 4 — l'export comptable : rien. Le jour où mon comptable me demande un fichier, j'écris un export CSV. Ça prend un quart d'heure, et il sera au bon format, parce que ce sera son format, pas celui que j'avais imaginé.

Bilan : ~400 lignes en moins, une clé d'API en moins, une API externe en moins qui peut tomber un mardi matin. Et la page de factures charge plus vite, parce qu'elle n'attend plus un taux de change EUR → EUR.

Le piège à éviter (petite mise en garde)

YAGNI ne veut pas dire "ignore l'avenir" ni "code n'importe comment, on verra bien." Deux nuances importantes.

1. Les décisions difficiles à défaire méritent qu'on y pense maintenant. Stocker les montants en centimes plutôt qu'en float, choisir son schéma de base de données, sécuriser l'authentification : ce ne sont pas des fonctionnalités "au cas où", c'est la fondation. YAGNI s'applique aux fonctionnalités, pas à la qualité.

2. YAGNI marche parce que DRY marche. Si un jour j'ai vraiment un client à Londres, ajouter une devise me prendra dix minutes : le formatage n'existe qu'à un seul endroit, dans formatMoney. C'est la Partie 1 qui rend la Partie 3 possible. Du code simple et sans duplication est facile à faire évoluer ; tu n'as pas besoin de prédire l'avenir si tu peux t'y adapter rapidement.

Et petite ironie de l'histoire : le seul vrai besoin "futur" qui finira par arriver pour une appli de factures en France, c'est la facturation électronique imposée par la réforme. Aucun de mes 3 exporters ne l'aurait gérée.


La semaine prochaine, Partie 4 : le match. DRY dit "ne te répète pas", KISS dit "reste simple"... et parfois, les deux ne sont pas d'accord. On verra pourquoi un peu de duplication vaut parfois mieux qu'une mauvaise abstraction.

Top comments (0)