Aller au contenu principal
Lance-toi : 40% de réduction sur toutes les formations jusqu'au 30 juin En savoir plus

Rule of Three : pourquoi factoriser au premier doublon est souvent une mauvaise idée

Tu as appris à ne pas te répéter. Tu factorises dès que tu vois deux lignes similaires. Et six mois plus tard, ton abstraction te coûte plus cher que la duplication qu'elle remplaçait. La Rule of Three est ce qui sépare une bonne factorisation d'une fausse économie.

Architecture logicielle ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

La plupart des développeurs juniors apprennent DRY avant tout autre principe. Don't Repeat Yourself. Trois mots qui sonnent comme un commandement. Le résultat : dès qu'un bout de code ressemble à un autre, on extrait une fonction, on crée une classe parente, on invente un paramètre pour gérer les deux cas. On a l'impression de bien faire.

Toi, tu as peut-être déjà vécu la suite. Six mois plus tard, le code "factorisé" doit gérer un troisième cas qui ne rentre pas dans le moule. Tu ajoutes un flag. Puis un autre. La fonction devient illisible. Le test qui devait être simple se met à échouer pour des raisons que personne ne comprend.

Le blocage n'est pas DRY. Le blocage, c'est de l'avoir appliqué trop tôt, sur des cas qui se ressemblaient en surface mais qui répondaient à des besoins différents. La Rule of Three est le garde-fou qui empêche cette erreur. Elle dit : attends d'avoir vu la même chose trois fois avant de l'abstraire.

Ce n'est pas un permis de copier-coller à l'infini. C'est une discipline qui te force à reconnaître la différence entre une coïncidence et un vrai pattern.

Le problème concret que tu rencontres

Imagine. Tu écris un module qui envoie un email de bienvenue à un nouvel inscrit. Quelques jours plus tard, on te demande d'envoyer aussi un email de réinitialisation de mot de passe. Le code se ressemble. Tu factorises dans une fonction sendEmail().

Trois semaines après, on ajoute un troisième email : une facture. Mais cette fois, il faut un PDF en pièce jointe. Tu ajoutes un paramètre attachment optionnel.

Puis arrive un quatrième cas : un email de notification interne qui doit logger l'événement dans un système d'audit. Tu ajoutes un paramètre auditLog. Puis un cinquième cas où le destinataire n'est pas un email mais un webhook Slack. Tu commences à comprendre que ta fonction sendEmail() n'envoie plus seulement des emails.

Ton abstraction est devenue un patchwork. Le bug récent dans l'envoi de factures vient d'une condition mal placée dans la fonction "générique" qui ne devrait même pas exister. Si tu avais attendu, tu aurais vu qu'il s'agissait de cinq cas distincts qui partagent trois lignes de code, pas d'un seul concept.

Le principe : trois passages avant d'abstraire

La Rule of Three vient de Martin Fowler, dans son livre Refactoring. La formulation tient en une phrase. La première fois que tu écris quelque chose, tu l'écris. La deuxième fois que tu écris quelque chose de similaire, tu acceptes la duplication. La troisième fois, tu refactorises.

L'idée derrière est statistique. Avec deux occurrences, tu n'as pas assez d'information pour savoir ce qui est vraiment commun entre elles. Tu vois deux blocs qui se ressemblent visuellement. Mais la ressemblance visuelle n'est pas une preuve de ressemblance conceptuelle.

Avec trois occurrences, tu peux comparer. Tu vois ce qui varie d'un cas à l'autre. Tu vois ce qui reste stable. Tu vois quels paramètres ton abstraction va devoir accepter. Tu vois aussi, parfois, que les trois cas ne devraient pas être factorisés du tout.

Le principe va plus loin que la simple duplication de code. Il s'applique à toute généralisation. Une classe abstraite, une configuration paramétrable, un système de plugins. Tant que tu n'as pas trois cas concrets, tu construis sur des suppositions. C'est exactement ce que YAGNI reproche aux ingénieurs qui anticipent des besoins qui n'arriveront jamais.

Pourquoi la duplication est parfois moins chère que l'abstraction

Sandi Metz, une figure reconnue du monde Ruby, le formule autrement : duplication is far cheaper than the wrong abstraction. Une mauvaise abstraction coûte plus cher qu'une duplication assumée.

La raison est mécanique. Quand deux blocs de code dupliqués divergent dans le futur, tu les modifies indépendamment, sans conséquence sur l'autre. Quand deux usages partagent une abstraction qui ne devrait pas exister, chaque modification de l'un casse l'autre. Tu ajoutes des conditions, des flags, des paramètres optionnels. La fonction grossit. Elle devient un point de friction permanent.

Pire, supprimer une mauvaise abstraction est rarement fait. Une fois que dix endroits dépendent d'une fonction mutualisée, démêler ce qui est vraiment partagé de ce qui est devenu spécifique demande un travail que personne n'a le temps de faire. La fausse économie initiale se transforme en dette technique permanente.

Le bon réflexe au début d'un projet est donc plutôt l'inverse de ce qu'on apprend. Duplique, observe, attends. La factorisation arrive quand le pattern est devenu évident, pas quand tu le devines.

Un exemple en JavaScript

Tu travailles sur une application qui doit valider des formulaires. Premier formulaire : inscription. Deuxième formulaire : édition du profil. Les deux vérifient que l'email est valide et que le mot de passe fait au moins 8 caractères.

Premier réflexe DRY : créer une fonction validateUserForm(). Mauvais réflexe. Tu n'as que deux cas. Garde la duplication.

// Inscription
function validateSignup(data) {
  if (!data.email.includes('@')) return 'Email invalide'
  if (data.password.length < 8) return 'Mot de passe trop court'
  if (!data.terms) return 'Conditions non acceptees'
  return null
}

// Edition de profil
function validateProfile(data) {
  if (!data.email.includes('@')) return 'Email invalide'
  if (data.password.length < 8) return 'Mot de passe trop court'
  return null
}

Trois mois plus tard, un troisième formulaire apparaît : la connexion. Lui aussi a besoin de valider email et mot de passe. Maintenant tu as trois cas. La règle s'applique : tu peux factoriser.

function validateCredentials(data) {
  if (!data.email.includes('@')) return 'Email invalide'
  if (data.password.length < 8) return 'Mot de passe trop court'
  return null
}

function validateSignup(data) {
  const error = validateCredentials(data)
  if (error) return error
  if (!data.terms) return 'Conditions non acceptees'
  return null
}

La factorisation est claire parce que tu as vu trois cas réels. Tu sais exactement ce qui est commun (email + mot de passe), ce qui ne l'est pas (les conditions générales, propres à l'inscription). Si tu avais factorisé au deuxième cas, tu aurais probablement inclus la vérification des conditions générales dans la fonction commune, ou ajouté un paramètre checkTerms qui aurait pollué les autres usages.

Un exemple en Python

Tu écris un script qui exporte des données vers un fichier CSV. La semaine suivante, un autre besoin arrive : exporter ces mêmes données vers du JSON. La tentation est de créer immédiatement une classe Exporter abstraite avec deux implémentations.

def export_to_csv(data, path):
    with open(path, 'w') as f:
        for row in data:
            f.write(','.join(str(v) for v in row.values()) + '\n')

def export_to_json(data, path):
    import json
    with open(path, 'w') as f:
        json.dump(data, f)

Garde ça. Ne crée pas de classe abstraite. Tu n'as pas assez d'information sur ce qui va vraiment varier entre les formats. Le CSV fait du streaming ligne par ligne, le JSON sérialise en bloc. Les modes d'écriture sont différents, les gestions d'erreur seront différentes.

Quand un troisième format apparaît, par exemple XML ou Excel, tu pourras regarder les trois fonctions ensemble et décider si une abstraction a du sens, et laquelle. Souvent tu découvriras qu'il vaut mieux extraire seulement la partie "ouvrir un fichier et écrire dedans", pas tout le processus d'export. Ce genre de découverte n'est possible qu'en présence de trois cas concrets.

Les pièges à éviter

Confondre similarité visuelle et similarité conceptuelle

Deux blocs de code qui se ressemblent peuvent répondre à des règles métier complètement différentes. Le calcul de TVA pour un produit et le calcul d'une remise client peuvent avoir la même structure (un pourcentage appliqué à un montant), mais ils évoluent indépendamment. Les factoriser revient à coupler deux concepts qui n'auraient jamais dû l'être.

Compter le nombre de lignes au lieu du nombre de cas

Si tu as cinquante lignes dupliquées entre deux fichiers, ça reste deux cas. La Rule of Three compte les contextes d'usage, pas les caractères copiés. Une grosse duplication entre deux endroits peut être plus saine qu'une mini-abstraction prématurée entre trois endroits qui n'ont rien à voir.

Empiler les paramètres pour faire rentrer un nouveau cas

Si tu as une fonction qui prend déjà six paramètres et qu'un septième cas arrive, ne lui ajoute pas un huitième flag. C'est le signal que ton abstraction initiale était mauvaise. Soit tu la divises, soit tu reviens à de la duplication. Un drapeau booléen optionnel dans une fonction est presque toujours le symptôme d'une factorisation prématurée.

Utiliser la règle comme excuse pour ne jamais refactoriser

La Rule of Three n'est pas un permis de laisser pourrir le code. Une fois que tu as trois cas, tu refactorises. Ne pas le faire revient à transformer ton code en collection de variations qui finiront par diverger silencieusement, avec les bugs qui vont avec. Le principe Boy Scout Rule reste valable : quand tu passes dans un endroit dupliqué pour la troisième fois, c'est le moment d'agir.

Ignorer le contexte du projet

Dans certains contextes, la duplication coûte cher dès la deuxième occurrence. Une règle de sécurité, un calcul fiscal, une logique de paiement. Si une duplication peut diverger silencieusement avec des conséquences graves, mieux vaut factoriser tôt. La Rule of Three est un guide par défaut, pas une loi absolue.

Comment intégrer la règle dans ton quotidien

La règle s'applique mécaniquement. Quand tu écris du code qui ressemble à un code déjà écrit, marque le mentalement. Tu peux laisser un commentaire // duplication possible avec X pour ne pas oublier.

Quand tu écris la même chose une troisième fois, fais une pause. Regarde les trois implémentations côte à côte. Pose-toi trois questions. Qu'est-ce qui est strictement identique ? Qu'est-ce qui varie ? Est-ce que les trois cas évoluent vraiment ensemble dans le futur ?

Si les réponses sont claires, factorise. Sinon, garde la duplication un cycle de plus. Mieux vaut une abstraction qui arrive en retard qu'une abstraction qui arrive trop tôt. Cette mécanique se travaille en équipe et fait partie des automatismes qu'on développe dans nos parcours, notamment dans la formation PHP orienté objet et dans le cursus développement backend, où la question "quand factoriser" revient à chaque exercice de conception.

La Rule of Three face aux autres principes

La Rule of Three n'annule pas DRY. Elle en règle le tempo. DRY te dit que la duplication est un risque. La Rule of Three te dit à partir de quel moment ce risque devient supérieur à celui d'une mauvaise abstraction.

Elle complète aussi KISS. Garder une duplication temporaire est souvent plus simple que créer une abstraction qui rajoute un niveau d'indirection. Tant qu'il n'est pas justifié, ce niveau d'indirection rend le code plus difficile à lire pour le prochain développeur qui débarque.

Et elle s'aligne avec Composition over Inheritance sur un point fondamental. Beaucoup de hiérarchies de classes héritées de la POO classique ont été conçues sur la base de deux exemples seulement. Le troisième cas a ensuite forcé à empiler des sous-classes, des overrides, des hooks. Une simple composition aurait suffi, mais l'héritage avait déjà été choisi.

À retenir

La duplication n'est pas un péché. Une mauvaise abstraction l'est davantage. La Rule of Three te donne un seuil simple : trois cas avant d'abstraire. Avant ça, tu n'as pas assez d'information pour faire un choix solide.

Cette discipline t'évite l'un des pièges les plus coûteux du développement : créer une généralisation sur deux exemples, puis passer les années suivantes à la maintenir en vie à coup de paramètres et de conditions. Quand tu factorises au bon moment, ton code se simplifie. Quand tu factorises trop tôt, il se rigidifie.

Questions fréquentes

La Rule of Three contredit-elle le principe DRY ?

Non. DRY énonce qu'il ne faut pas dupliquer la connaissance dans le code. La Rule of Three précise quand appliquer DRY pour éviter une factorisation prématurée. Les deux principes se complètent : DRY est l'objectif final, la Rule of Three est la méthode pour y arriver sans créer une mauvaise abstraction.

Est-ce que la règle s'applique aussi à du code de production critique ?

Pas toujours. Pour de la logique métier sensible comme un calcul de TVA, un règlement fiscal ou une règle de sécurité, mieux vaut factoriser dès la première duplication. Le coût d'une divergence silencieuse entre deux endroits qui devraient appliquer la même règle est trop élevé. La Rule of Three reste un guide pour les cas où l'erreur de factorisation est plus coûteuse que la duplication elle-même.

Comment savoir si une abstraction existante est mauvaise ?

Plusieurs signaux. La fonction ou la classe accumule des paramètres optionnels et des flags booléens. Chaque nouveau cas force à ajouter une condition à l'intérieur. Les développeurs commencent à dupliquer du code pour éviter de toucher à l'abstraction. Les tests deviennent difficiles à écrire parce qu'il faut couvrir trop de branches. Quand ces symptômes apparaissent, il est temps de défaire la factorisation et de revenir à du code spécifique.

Que faire si je trouve une duplication ancienne dans un code que je n'ai pas écrit ?

Compte le nombre d'occurrences réelles. Si tu en trouves trois ou plus, regarde si elles partagent vraiment la même intention métier. Si oui, factorise en gardant les paramètres au strict minimum nécessaire. Si elles ressemblent en surface mais répondent à des règles différentes, laisse la duplication en place. Ne pas factoriser est parfois la bonne décision, même quand DRY semble l'imposer.

La Rule of Three s'applique-t-elle aux tests automatisés ?

Oui, et c'est même un cas où la règle compte particulièrement. Les tests sont souvent dupliqués en surface, mais leur valeur tient à leur lisibilité. Factoriser trop tôt une fixture de test ou une fonction utilitaire de mock revient à rendre les tests difficiles à comprendre quand ils échouent. Mieux vaut accepter une duplication de quelques lignes dans des tests qu'introduire un système de helpers complexe au deuxième cas.

Cette règle vient-elle de Martin Fowler ?

Martin Fowler la popularise dans son livre Refactoring publié en 1999, en attribuant la formulation à Don Roberts. La règle elle-même circule dans la communauté du développement bien avant, sous différentes formes. Sandi Metz a ensuite donné une formulation très connue avec "duplication is far cheaper than the wrong abstraction", qui résume bien l'esprit du principe.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse — publiez un article invité et gagnez en visibilité.

Écrire pour nous