Factorisation
Règle de 3, DRY raisonné, quand abstraire et quand pas
DRY n'est pas un dogme. Une mauvaise abstraction coûte plus cher qu'une duplication. La règle qui marche : factorise après le 3ᵉ usage, jamais avant.
La règle de 3
Première fois : tu écris le code. Deuxième fois : tu dupliques avec un petit pincement, tu notes mentalement qu'il y a une similarité. Troisième fois : tu factorises. Maintenant tu vois ce qui est vraiment commun et ce qui varie d'un cas à l'autre.
Pourquoi attendre ? Parce qu'avec 2 cas, tu n'as pas assez d'infos pour distinguer le motif réel de la coïncidence. Les abstractions bâties sur 2 exemples sont presque toujours mauvaises : elles capturent un détail au lieu du principe.
Bonne factorisation vs mauvaise factorisation
Bonne factorisation
- Réduit la complexité : le caller a moins à penser
- Capture une intention claire :
calculateCommission(request)se comprend tout seul - Réutilisée par 3+ endroits différents
- Stable dans le temps : si tu reviens dans 3 mois, l'abstraction tient toujours
Mauvaise factorisation
- Augmente la complexité : le caller doit lire la définition pour comprendre
- Couvre 2 cas avec un paramètre
flagqui change tout le comportement - Doit être contournée la fois suivante avec un
ifou un override - Passe des objets de config énormes pour gérer toutes les variations
Signaux d'une mauvaise abstraction
Si tu as l'un de ces symptômes, annule l'abstraction et duplique :
- Le paramètre
optionsgrossit à chaque nouveau cas - Des
if/switchse multiplient dans le helper pour gérer les variantes - Des callers passent des valeurs "fictives" parce qu'ils n'utilisent pas certains paramètres
- Des callers contournent l'abstraction parce qu'elle ne couvre pas leur cas
- Tu hésites à modifier l'abstraction de peur de casser des callers que tu ne connais pas tous
Tous ces signaux veulent dire la même chose : tu as factorisé trop tôt ou trop large. Reviens en arrière, duplique, et attends.
Bon DRY vs mauvais DRY
// 🔴 Mauvais DRY, abstrait trop tôt
function processItem(item, mode, withLogging, callback, options) {
if (mode === 'create') { /* … */ }
if (mode === 'update') { /* … */ }
if (mode === 'delete') { /* … */ }
if (withLogging) { /* … */ }
if (options?.notify) callback?.(item);
// 60 lignes de cas particuliers
}
// 🟢 Bon, 3 fonctions claires
function createMember(input) { /* … */ }
function updateMember(id, patch) { /* … */ }
function deleteMember(id) { /* … */ }// 🔴 Mauvais DRY, factorisation par accident
function formatUser(user) {
return `${user.firstName} ${user.lastName}`;
}
function formatMember(member) {
return formatUser(member); // marche par hasard
}
// 🟢 Bon, chaque domaine a son propre format
function formatMember(member) {
return `${member.code}, ${member.name}`;
}
function formatUser(user) {
return `${user.firstName} ${user.lastName}`;
}Quand factoriser sans hésiter
Certains cas justifient une factorisation dès le 1ᵉʳ usage :
1. Logique métier centrale
Le calcul du taux de commission, la résolution du status d'une demande, l'algorithme de matching. Ce sont des règles métier : on ne veut jamais les écrire deux fois, parce qu'elles vont diverger.
// ✅ Toujours dans un seul endroit, jamais dupliqué
export function resolveCommissionRate(req: Request, member: Member): number {
const requestOverride = findRequestOverride(req.id);
if (requestOverride) return requestOverride.rate;
const memberOverride = findMemberOverride(member.id);
if (memberOverride) return memberOverride.rate;
return DEFAULT_RATE;
}2. Validation des inputs externes
Si une validation Zod/TypeBox est utilisée par 1 route, elle peut rester locale. Si plusieurs routes l'utilisent, on l'extrait.
3. Interface avec un service externe
Le client Anthropic, le client Resend, le client Meta WhatsApp. Une fonction wrapper par service, partagée par tous les modules qui en ont besoin.
4. Format de données canonique
La représentation canonique d'un membre dans une notification (#102, Restaurant du Lac), le format d'un email, le format d'un domain event. Une seule définition, pour éviter que des notifications affichent #102 et d'autres Restaurant du Lac selon l'humeur du dev.
Quand ne PAS factoriser
Code qui ressemble visuellement mais qui a une intention différente
// Ces deux fonctions ressemblent, il ne faut PAS les fusionner
function logHttpError(err: Error) {
console.error('[HTTP]', err.message, err.stack);
await sentry.capture(err);
}
function logBusinessError(err: BusinessError) {
console.error('[BIZ]', err.message, err.stack);
await sentry.capture(err);
}Elles ressemblent. Mais elles servent deux flux différents qui vont diverger : logHttpError va finir par envoyer des métriques HTTP, logBusinessError va finir par alerter Discord. Les fusionner crée un point de blocage à cette future évolution.
"Helpers" génériques de transformation
// 🔴 Mauvais, masque la lisibilité
const result = chain(items)
.map(toEnriched)
.filter(isValid)
.uniqBy('id')
.pipe(applyDefaults);// 🟢 Bon, explicite
const enriched = items.map(toEnriched);
const valid = enriched.filter(isValid);
const unique = uniqBy(valid, 'id');
const final = unique.map(applyDefaults);Le second est plus long, mais on lit ligne par ligne ce qui se passe. Le premier demande de connaître chain, pipe, et leur comportement.
Composants React "configurables à outrance"
// 🔴 Mauvais
<Button
variant="primary"
size="md"
iconLeft={iconA}
iconRight={iconB}
loading
fullWidth
rounded="lg"
shadow="sm"
hoverEffect="lift"
// … 15 props
>
Click me
</Button>Si tu en es là, c'est que Button essaye de faire 15 choses. Splitte en sous-composants spécialisés (PrimaryButton, IconButton, LoadingButton).
Rule of thumb
"Duplication is far cheaper than the wrong abstraction." , Sandi Metz
Cette phrase résume tout. Une duplication coûte 5 minutes à supprimer le jour où le motif devient clair. Une mauvaise abstraction coûte des heures de refacto, des bugs subtils, et un découragement collectif.
Process en review
En PR, quand tu vois une factorisation :
- L'auteur a-t-il 3 callers réels ? Si non, refuser et garder en duplication.
- L'abstraction capture-t-elle un nom métier clair ? Si elle s'appelle
processStuffouhandleData, c'est non. - Y a-t-il un
flagqui change tout le comportement ? Refuser, splitter en deux fonctions. - Les callers passent-ils des objets de config énormes ? Refuser, l'abstraction est trop large.
Et inversement, si tu vois une duplication :
- Combien de callers ? Moins de 3 → tolérable.
- L'intention est-elle vraiment la même ? Pas juste "ça se ressemble"
- La factorisation simplifierait-elle le caller ? Si c'est juste pour compter moins de lignes, non.
Voir aussi
- Seuils de fichiers, la duplication ne doit pas faire exploser les fichiers
- Architecture modulaire, où placer les helpers partagés