Accueil > TDD c’est Clean Code ?
Walid Ammar

TDD c’est Clean Code ?

TDD c'est Clean Code ?

Mais de quoi parles-tu ?!

Le titre de cet article peut choquer certains d’entre vous, oui je le sais. Toutefois je vous invite à lire l’article jusqu’à la fin pour en comprendre la signification. Le TDD (Test Driven Development), un grand mot qui peut s’interpréter de différentes manières.

A- Pour certains développeurs cela signifie tout simplement l’écriture des tests unitaires avant les incréments de code à produire pour être sûr d’avoir un bon filet de sécurité quant aux régressions.

B- Pour d’autres, l’enjeu est plus grand et dépasse l’ordre d’écriture, il constitue une approche permettant aux développeurs de mieux comprendre le besoin métier et de produire un code pertinent. Pour cette population les tests constituent la spécification des incréments de code à produire.

C- Une troisième population considère l’approche TDD comme une méthode de travail permettant de produire un code de qualité en accentuant le travail de refactoring.

D-  « la réponse D » (^_^)

Le Clean Code ! Encore plus grand et beaucoup plus mitigé comme mot ! Les interprétations sont alors très variées et dépendent des orientations et des affinités de chacun…
Réfléchissez un peu, vous avez surement croisé quelqu’un qui vous a défini le Clean Code autrement que vous ne le conceviez ? N’est-ce pas vrai ? En tout cas moi si.

 

Je vous cite 5 exemples de populations que j’ai souvent croisé dans notre petit monde de geeks :

A- Certaines personnes qualifient de Clean Code tout code lisible et facilement compréhensible. Dans ces codes-là nous trouverons généralement beaucoup de commentaires et des structures algorithmiques linéaires et séquentielles.

B- Une deuxième population qualifie de Clean Code tout code efficient, c’est à dire un code qui restitue un maximum de résultat avec un minimum d’instructions. Dans ces codes-là, nous trouverons généralement des structures algorithmiques optimisées et plus complexes (délégation, réflexion…)

C- Une troisième population qualifie de Clean Code tout code performant, c’est à dire du code stable dont l’exécution consomme peu de ressources et s’achève très rapidement. Ces codes-là sont pour beaucoup des codes « dénormalisés » (non structurés) et adhérents aux couches d’infrastructure.

D- Une quatrième population qualifie de Clean Code tout ce qui sort de la dette technique (Legacy Code). La dette technique ayant elle-même plusieurs définitions et varie en fonction des personnes (nous nous attarderons dessus une autre fois)…

E- La cinquième et dernière population combine certains, voire l’ensemble des critères que je viens de vous citer.

 

Ok, c’est quoi le rapport ?!

En bien lors d’un échange récent avec l’un de mes amis sur les bonnes pratiques de développements, j’ai été heurté par la phrase « TDD c’est Clean Code », une phrase choque qui à mon avis, placée dans un mauvais contexte, pourrait induire en erreur.
Donc, en bon chicaneur que je suis, j’ai essayé de revoir la construction grammaticale de celle-ci pour éviter toute ambiguïté, malheureusement je n’ai pas eu le temps de réagir avant de me prendre 2 Scuds en pleine tête.

Il s’agit des phrases suivantes :

  • Si tu fais du TDD, tu auras obligatoirement du Clean Code.
  • Si tu ne fais pas de TDD, tu n’auras jamais de Clean Code.

Et là je me suis dit que quelque chose clochait. Remettons les choses en bon ordre… Et commençons par le commencement.

 

TDD c’est quoi ?

Et bien simplement, c’est l’écriture des tests avant le code…

Bien sûr que non, soyons sérieux. TDD est une démarche et pas juste un ordre de programmation. Cette démarche vise à produire du code émergent en partant des tests et donc des résultats attendus: les assertions.

Cette démarche implique l’application de certaines règles et l’extrapolation de notre regard sur le code source (on l’observera désormais depuis le code de test) >> ceci nous permet d’obtenir un feedback très rapide du fonctionnement du code que l’on produit, donc bcp moins de debug… Cela permet aussi d’éviter le syndrome d’overengineering, de limiter les duplications de code et de constituer un filet de sécurité anti-régressions augmentant notre niveau de confiance, qui est à mon sens un des objectifs majeurs.

Imaginez qu’on vous demande de construire une maison en vous plaçant au centre du chantier, en vous enlevant tout instrument de mesure et en vous disant que la seule manière de vérifier votre avancement est de passer une nuit dedans pour voir ce qui manque (dans le froid et sous la pluie). Que va-t-il se passer ?

– Vous allez chopper une bonne grippe à force de passer des nuits entières en sac de couchage dans le chantier poussiéreux et tout ça juste pour effectuer des petites mesures. C’est équivalent au stress de debugger un milliard de fois un logiciel entier pour vérifier qu’un petit bout de code marche bien.

– Vous risquez de tailler d’énormes murs de 60cm d’épaisseur car vous avez peur qu’une voiture vous fonce dessus un jour car vous supposez que l’état peut faire construire une autoroute en face de votre maison (comme vous restez à l’intérieur du chantier et que vous n’en sortez pas, vous ne pouvez pas deviner :p). C’est équivalent à l’overengineering obsolète d’une application. Exemple : Architecture DDD pour un petite appli de CRUD où il y’aurait que quelques tables à alimenter.

– Vous risquez d’installer un condensateur délivrant 120A alors que réellement vos installations ne consommeront pas plus de 20A…

– Vous risquez de placer une chambre dans la cave alors que vous vous trouvez dans une zone innondable.

Bref, toute sorte de considérations inutiles dues au fait que vous regardiez votre chantier uniquement de l’intérieur.

Maintenant, imaginez qu’on vous demande de construire la même maison en vous donnant tous les instruments de mesure requis et en vous autorisant à effectuer des entrées-sorties du chantier. Vous allez pouvoir mieux regarder vos réalisations de l’extérieur et de l’intérieur pour les exécuter au mieux. En plus de ça, vous allez mesurer chaque réalisation instantanément et donc ajuster très rapidement s’il le faut, et non pas attendre la tombée de la nuit pour contrôler ! Vous obtenez un feedback très très rapide car il y a très peu, voire pas de debug du logiciel entier !

« TDD is an awareness of the gap between decision and feedback during programming, and control over that gap. »(In TDD By Example de Kent Beck)

Très Bien, maintenant comment mettons-nous ça en œuvre ?

Il y a principalement 2 écoles : Top Down/Outside In et Bottom Up/Inside Out mais nous n’aurons pas le temps de les détailler dans ce post. Nous allons donc plutôt nous concentrer sur les bases : les cycles d’implémentation.

Selon Robert C. Martin, Il devrait y avoir 2 cycles imbriqués l’un dans l’autre (Nano dans Micro) :

 

Le Cycle Nanoscopique (dit « Second by Second Cycle» ou aussi « Line by line cycle »)

ll consiste à alterner l’écriture entre le code métier et le code de test ligne par ligne : une ligne de test, une ligne de code, une ligne de test, une ligne de code, etc.
3 règles régissent ce cycle (comme les lois de la robotique d’Asimov) :

  1. You may not write production code until you have written a failing unit test.
  2. You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
  3. You may not write more production code than is sufficient to pass the currently failing test.

(In Clean Code de Robert C. Martin)

 

Le Cycle Microscopique (dit « Minute by Minute cycle »)

C’est un cycle plus grand et s’achève généralement à la fin d’écriture d’un test unitaire, il est aussi très connu par l’appellation « Red, Green, Refactor »

Ce cycle se décrit par les séquences suivantes :

  1. Create a unit tests that fails
  2. Write production code that makes that test pass.
  3. Clean up the mess you just made.

Uncle Bob stipule que notre cerveau n’est pas capable d’optimiser simultanément le comportement du code et sa structure, donc on se concentre d’abord sur le comportement et on optimise la structure par la suite.

The philosophy is based on the idea that our limited minds are not capable of pursuing the two simultaneous goals of all software systems: 1. Correct behavior. 2. Correct structure. So the RGR cycle tells us to first focus on making the software work correctly; and then, and only then, to focus on giving that working software a long-term survivable structure.

(Source : Robert C. Martin)

Un de mes amis et collègues de travail, Aurelien Galtier, s’amuse souvent à rajouter une étape « 0 » à ce cycle pour amener de la pertinence dans l’ordonnancement des tests à implémenter, qui peut grandement impacter l’architecture et les orientations techniques du logiciel. Cette étape est l’analyse, et je trouve le procédé malin. Voir l’illustration dans le chapitre TDD du 4ème Cell’Insight : Agile Testing

Enfin voilà pour le TDD, j’espère que c’est clair. Maintenant, qu’en est-il du Clean Code ??

 

Le fameux CLEAN Code !

Et bien il n’y a pas de définition universelle pour un « Clean Code ». C’est une notion complètement subjective et perçue de différentes manière en fonction des expériences de chacun.

C’est vrai que depuis quelques années, il y a de plus de plus d’efforts pour rationaliser les normes dans des référentiels et/ou des outils d’analyse statiques de code comme par exemple le seuil de complexité cyclomatique d’une méthode, le seuil de couplage fort entre les assemblies ou encore le nombre maximum de paramètres dans une fonction. Tout ceci reste subjectif et ne peut constituer un référentiel universel pour tous les développeurs. CELa nous donne juste la vision du Clean Code selon certain organismes ou organisation +/- connues et reconnues.

Comment faire alors ? Et bien c’est simple, voyons ce que pensent quelques-uns des grands de ce monde (vous pouvez d’ailleurs trouver toutes ces citation dans Clean Code de Robert C. Martin).

Bjarne Stroustrup, créateur de C++, pense qu’un Clean Code est tout simplement un code élégant et efficient.

Pour Stroustrup, un code élégant est un code qui fait plaisir à ses lecteurs et qui est écrit d’une manière qui empêcherait les bugs de s’y cacher…

« I like my code to be elegant and efficient, The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an architectural strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean Code does one thing well. »

Grady Booch, co-créateur du langage UML et concepteur de la méthode Booch, pense qu’un Clean Code est un code simple, direct et n’obstrue en aucun cas le design de l’application :

« Clean Code is simple and direct, Clean code reads like well-written prose. Clean Code never obscures the designer’s intent but rather is fully of crisp abstractions and straightforward lines of control »

Ward Cunningham, co-créateur de XP et signataire du Manifeste Agile, pense qu’un Clean Code est un code qui ravit son lecteur, mais peut également se transformer en « Beautiful Code » quand il est écrit d’une manière faisant croire à son lecteur que le langage de programmation employé aurait été destiné à résoudre le besoin en question :

« You know you are working on clean code when each routine you read turns out to be pretty much what you expected. You can call it beautiful wode when the code also makes it look like the language was made for the problem »

Michael C. Feathers, l’un des principaux prédicateurs de l’approche TDD et des bonnes pratiques orientées objet, pense qu’un Clean Code est un code écrit par un développeur attentionné et n’a nullement besoin d’optimisation ;

« I could list all of the qualities that I notice in clean code, but there is one overarching quality that leads to all of them. Clean code always looks like it was written by someone who cares. There is nothing obvious that you can do to make it better »

Dave A. Thomas, fondateur de Eclipse et de VisualAge, pense qu’un Clean Code est un code pouvant changer de mains sans la moindre difficulté, un code testable et testé, explicite et exprime les choses d’une seule manière :

« Clean code can be read, and enhanced by a developer other than its original author. It has unit and acceptance tests. It has meaningful names. It provides one way rather than many ways for doing one thing. It has minimal dependencies, which are explicitly defined, and provides a clear and minimal API.»

Robert C. Martin, aussi appelé Uncle Bob, un des principaux prédicateurs de l’approche TDD et signataire du Manifeste Agile, pense qu’un Clean Code est un code dont a pris soin ;

« Clean code is code that has been taken care of. Someone has taken the time to keep it simple and orderly. They have paid appropriate attention to details. They have cared.»

Que peut-on donc conclure ? Et bien pour ma part, j’aime à penser qu’un clean code est tout cela en même temps… Un code élégant, clair, fait avec attention par des gens passionnés, testable, découplé et surtout expressif.

Maintenant si l’on revient à ma discussion d’hier, voici ce que j’en pense. Appliquer une démarche TDD à elle seule n’implique pas forcément la production d’un code de qualité.

TDD n’est qu’une approche mettant en œuvre un cadre favorisant le Clean Code mais ne constitue nullement une assurance tout risque ! TDD est une excellente démarche et j’en suis moi-même un adepte mais cela ne devrait pas masquer un élément très (voire trop) important de l’équation : la qualité du refactoring !

Oui, TDD veut que l’on fasse du refactoring à l’issue de chaque test. Mais il n’invoque nullement les moyens pour y arriver et c’est là où se trouve le plus gros du travail.
Comment refactorer du code ? Partant d’un contexte précis, quelle sera la meilleure façon de faire ? Quelles sont les règles de qualité à appliquer ? Les erreurs qu’il faudra éviter ? Par quoi devons-nous commencer ? Comment rendre notre code parfaitement compréhensible par d’autres personnes ? Toutes ces questions constituent les véritables leviers de qualité d’un code.

Comme je le disais au tout début, il n’y a pas de normes universelles. En revanche, il existe bien des règles, des patterns et des pratiques plus ou moins généralisés en fonction des communautés auxquelles on adhère. En voici quelques exemples

 

Patterns and principles :

  • SOLID : Single Responsibility, Open/Closed Principle,Liskov Substitution, Interface Segregation, Dependency Injection.
  • GRASP : General Responsibility Assignment Software Patterns/Principles
  • GOF Patterns : Gang of Four Design patterns

 

Pratiques :

  • DRY : Don’t Repeat Yourself
  • LIM : Less Is More
  • YAGNI : You Aren’t Gonna Need It
  • KISS : Keep It Simple, Stupid (ou Keep It Stupid, Simple 🙂 )
  • SoC : Separation Of Concerns
  • Demeter : Don’t Talk to Strangers

 

Métriques :

  • CC : Cyclomatic Complexity
  • CCR : Code Coverage Rate
  • LOC : Lines Of Code
  • SI : Stability Index
  • MI : Maintainability Index(…)

Je vous renvoie à l’article de Nicholas Suter, posté il y a quelques années, qui reprend quelques-unes des métriques les plus courantes du marché.

Sinon, pour en finir avec la philosophie, je vous propose un cas concret pour illustrer tout ceci ;

 

Stop talk ! Act !

Partant sur un besoin simple : mettons-nous dans la peau d’un développeur junior qui devrait appliquer TDD à la lettre

Supposant que l’on nous demande de créer deux fonctions nous permettant de convertir des chiffres romains en chiffres numériques, les valeurs possibles vont de 1 à 10.

Bien entendu, je ne prendrai pas en considération les nanocycles « line to line » pour vous simplifier les illustrations, on commencera alors par créer le premier test. Je n’utiliserai aucune librairie d’assertion particulière car ce n’est pas l’objet de l’exercice)

​
    [TestClass]
    public class TestConversionChiffresRomains
    {
        [TestMethod]
        public void DoitRetournerChiffreUnQuandJeConvertisLeCaractereI()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("I");
            Assert.AreEqual(result, 1);
        }
    }    

On essaie d’exécuter le test mais le code ne compile pas. C’est normal car la classe « ConvertisseurChiffresRomains » n’est pas encore implémentée. A ce stade, on aura accompli la première étape (Red) du cycle RGR. (No compilation is fail)

A présent, on passe à l’implémentation du code avec le minimum d’efforts possible pour faire passer mon test au vert :

​
    public class ConvertisseurChiffresRomains
    {
        public int ConvertirEnChiffreNumerique(string chiffreRomain)
        {
            if (chiffreRomain == "I")
                return 1;
            throw new ArgumentException("Impossible de convertir le numéro");
        }
    } 

On relance l’exécution du test et là ça marche ! On vient d’accomplir l’étape (Green) du cycle.

On voit s’il faut réfactorer et à ce stade. Dans ce cas-ci, rien à signaler donc on considère que l’étape 3 (Refactor) est accomplie, on poursuit avec le reste…

On va répéter l’opération 10 fois jusqu’à l’implémentation des cas d’usages possibles (en supposant qu’à ce stade nous n’aurions pas encore eu besoin de refactorer), on obtient alors ce magnifique code :

​
    [TestClass]
    public class TestConversionChiffresRomains
    {
        [TestMethod]
        public void DoitRetournerChiffreUnQuandJeConvertisLeCaractereI()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("I");
            Assert.AreEqual(result, 1);
        }
        [TestMethod]
        public void DoitRetournerChiffreDeuxQuandJeConvertisLeCaractereII()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("II");
            Assert.AreEqual(result, 2);
        }
        [TestMethod]
        public void DoitRetournerChiffreTroisQuandJeConvertisLeCaractereIII()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("III");
            Assert.AreEqual(result, 3);
        }
        [TestMethod]
        public void DoitRetournerChiffreQuatreQuandJeConvertisLeCaractereIV()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("IV");
            Assert.AreEqual(result, 4);
        }
        [TestMethod]
        public void DoitRetournerChiffreCinqQuandJeConvertisLeCaractereV()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("V");
            Assert.AreEqual(result, 5);
        }
        [TestMethod]
        public void DoitRetournerChiffreSixQuandJeConvertisLeCaractereVI()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("VI");
            Assert.AreEqual(result, 6);
        }
        [TestMethod]
        public void DoitRetournerChiffreSeptQuandJeConvertisLeCaractereVII()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("VII");
            Assert.AreEqual(result, 7);
        }
        [TestMethod]
        public void DoitRetournerChiffreHuitQuandJeConvertisLeCaractereVIII()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("VIII");
            Assert.AreEqual(result, 8);
        }
        [TestMethod]
        public void DoitRetournerChiffreNeufQuandJeConvertisLeCaractereIX()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("IX");
            Assert.AreEqual(result, 9);
        }
        [TestMethod]
        public void DoitRetournerChiffreDixQuandJeConvertisLeCaractereX()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            var result = convertisseur.ConvertirEnChiffreNumerique("X");
            Assert.AreEqual(result, 10);
        }

        [ExpectedException(typeof(ArgumentException))]
        [TestMethod]
        public void DoitLeverUneExceptionQuandJeConvertisUnCaractereInconnu()
        {
            var convertisseur = new ConvertisseurChiffresRomains();
            convertisseur.ConvertirEnChiffreNumerique("XX");
        }
    }

    public class ConvertisseurChiffresRomains
    {
        public int ConvertirEnChiffreNumerique(string chiffreRomain)
        {
            if (chiffreRomain == "I")
                return 1;
            if (chiffreRomain == "II")
                return 2;
            if (chiffreRomain == "III")
                return 3;
            if (chiffreRomain == "IV")
                return 4;
            if (chiffreRomain == "V")
                return 5;
            if (chiffreRomain == "VI")
                return 6;
            if (chiffreRomain == "VII")
                return 7;
            if (chiffreRomain == "VIII")
                return 8;
            if (chiffreRomain == "IX")
                return 9;
            if (chiffreRomain == "X")
                return 10;
            throw new ArgumentException("Impossible de convertir le numéro");
        }
    }

Toutefois, le développeur que je suis n’aime pas trop les branchements IF-ELSE, je décide alors d’apporter une révolution : Je vais refactorer ceci en un magnifique SWITCH-CASE dont voici le résultat :

​
    public class ConvertisseurChiffresRomains
    {
        public int ConvertirEnChiffreNumerique(string chiffreRomain)
        {
            switch (chiffreRomain)
            {
                case "I":   return 1;
                case "II":  return 2;
                case "III": return 3;
                case "IV":  return 4;
                case "V":   return 5;
                case "VI":  return 6;
                case "VII": return 7;
                case "VIII": return 8;
                case "IX":  return 9;
                case "X":   return 10;
                default:    throw new ArgumentException("Impossible de convertir le numéro");
            }
        }
    }

Là c’est bon, je considère que j’ai finis le boulot et que j’ai produit du Clean Code couvert à 100% en employant l’approche TDD. C’est vrai quoi, j’ai commencé par implémenter l’Assert, j’ai implémenté mon code et j’ai essayé de refactorer.

Nous sommes bien d’accord, j’ai respecté à la lettre les lois micro-cycliques de l’approche TDD.

blog-wammar-art1-1

Question : Vous ne remarquez rien? Croyez-vous qu’on puisse mieux faire?

Eh bien moi oui. Je vous explique.
Le grand nombre de tests unitaires qu’on a obtenu pour un si petit périmètre est symptomatique d’une grande complexité cyclomatique. Ce n’est pas normal donc je veux en avoir le cœur net !

Je procède à une analyse de complexité et là BIIIMMMM ! La méthode ConvertirEnChiffreNumérique affiche une complexité cyclomatique totale de 24 ce qui est « un peu » élevé pour notre besoin. Ceci est dû à l’utilisation du switch qui affiche au mieux une CC égale à O(n).

blog-wammar-art1-2

Et bien si l’on pouvait s’affranchir de cette complexité, qu’en dites-vous ? Allons-y faisons ça ;

Je décide désormais de changer notre « Switch-Case » par un Getter sur une variable privée qui sera alimentée par injection.

On transforme ainsi le code de la classe « ConvertisseurChiffresRomains » Ainsi ;

    public class ConvertisseurChiffresRomains
    {
        private readonly Dictionary<string, int> _nombresRomains;

        public ConvertisseurChiffresRomains(Dictionary<string, int> nombresRomains)
        {
            _nombresRomains = nombresRomains;
        }

        public int ConvertirEnChiffreNumérique(string chiffreRomain)
        {
            if (_nombresRomains.All(x => x.Key != chiffreRomain))
                throw new ArgumentException("Impossible de convertir le numéro");
            return _nombresRomains[chiffreRomain];
        }
    } 

On dégage donc les 11 tests qu’on aura fait jusque-là qu’on remplacera par ces 2 là ;

    [TestClass]
    public class TestConversionChiffresRomains
    {
        [TestMethod]
        public void DoitRetournerChiffreNumeriqueQuandJeConvertieLeCaractèreRomain()
        {
            var chiffreRomain = "CHIFFREROMAIN";
            var chiffreNumérique = 100;
            var convertisseur = new ConvertisseurChiffresRomains(new Dictionary<string, int>() { [chiffreRomain] = chiffreNumérique });
            var result = convertisseur.ConvertirEnChiffreNumérique(chiffreRomain);
            Assert.AreEqual(result, chiffreNumérique);
        }

        [ExpectedException(typeof(ArgumentException))]
        [TestMethod]
        public void DoitLeverUneExceptionQuandJeConvertieUnCaractèreInconnu()
        {
            var chiffreRomainIconnu = "CHIFFREROMAINICONNU";
            var chiffreRomain = "CHIFFREROMAIN";
            var chiffreNumérique = 100;
            var convertisseur = new ConvertisseurChiffresRomains(new Dictionary<string, int>() { [chiffreRomain] = chiffreNumérique });
            var result = convertisseur.ConvertirEnChiffreNumérique(chiffreRomainIconnu);
            Assert.AreEqual(result, chiffreNumérique);
        }
    }

On exécute les nouveaux tests et tout reste au vert, cool ! Qu’en est-il de la complexité de la couverture ?!

BIIIMMMMM la CC est passée de 24 à 3 et la couverture des tests est restée à 100 %, c’est génial non !

blog-wammar-art1-3

blog-wammar-art1-4

  • Dans le premier exemple, on avait un objet encapsulant entièrement les correspondances ChiffreRomain-ChiffreNumérique ce qui nous obligeait à changer le code à chaque fois qu’un nouveau chiffre rentre en application (en plus, on avait une palanquée de tests).
  • Dans le deuxième exemple, on aura rendu possible l’institution d’une structure de Référentiel pour nous éviter la duplication de code dans les tests et nous débarrasser de cet immonde SWITCH-CASE, ainsi on pourra sortir notre dictionnaire (si besoin bien entendu) sur un conteneur transverse de type « Variable de contexte », fichier de configuration ou même base de données afin que notre algorithme soit paramétrable et non modifiable (si un nouveau chiffre rentre en application, il suffira d’alimenter le référentiel sans modifier le code) et puis faut pas oublier qu’on est passé de 11 tests à 2. De cette façon, nous respectons le O de SOLID.

Maintenant, si certains d’entre vous ressentent le besoin de me sauter à la gorge et me rappelant YAGNI, je vous demanderais gentiment de faire vous-même l’exercice de développer un convertisseur quel qu’il soit en encapsulant son dictionnaire de correspondances et revenir me voir après. Je crois qu’on se comprendra à ce moment-là (^_^)

Bref, qu’en pensez-vous maintenant ? Pouvons-nous qualifier de Clean Code les premiers exemples (IF-ELSE / SWITCH-CASE) ? Qu’en est-il du deuxième ?

Et bien comme je l’avais cité plus haut, le Clean Code est une notion complètement subjective et dépend de beaucoup de paramètres. A mes yeux, le premier code n’en est pas, le deuxième quant à lui l’est beaucoup plus même si l’on peut encore l’optimiser…

Donc si je résumé notre exercice, on aura fait du TDD jusqu’au bout mais on n’aura obtenu un résultat satisfaisant que lorsqu’on aura d’avantage investi sur l’étape de Refacto.

C’est ça la clé !

  • Si tu fais du TDD sans vraiment accentuer la phase de refactoring, tu n’auras pas forcément de Clean Code !
  • L’approche TDD n’est pas une assurance tout risque et ne décrit nullement la manière de refactorer du code, ç’est à vous de le faire !

Voilà, c’est tout dit ! Maintenant si je dois conclure cet article, je dirai qu’il est bien voire essentiel de faire du TDD (moi-même j’en fais systématiquement). Cela dit faites attention car ce n’est pas une fin en soi, c’est plutôt un moyen / une approche / une démarche / un instrument (chacun l’appellera comme il voudra) nous permettant de mieux comprendre le besoin métier, d’avoir un feedback rapide du code, de produire des incréments pertinents et nous lancer sur la voie du Clean Code. Le reste comme on dit, c’est entre le clavier et la chaise !

Nos autres articles
Commentaires

TDD ne permet effectivement pas de faire du clean code mais apporte bien d’autres avantages.
Bravo pour l’article qui met tout ça bien en évidence.

Sujet épineux… Doit-on refactorer ses tests et leur appliquer les principes du clean-code ? Car les tests sont du code…

Yo renaldo!
Et bien j’aurai tendance à dire qu’il faudra bien les écrire dès le début :p
Non sans blague, à mon avis oui il faudra les refactorer si l’on n’a pas d’autres urgences à gérer.
Un test doit avant tout être « FIRST » et très explicite, celui qui le consultera devra très rapidement en comprendre la signification sinon ça constitue de la dette!
Allez, une ptite citation pour la route, je te laisse deviner qui a dit ça :p
« If we want to remove duplication from our model code, do we want to remove it from our test code also? Maybe! »

Laisser un commentaire

Restez au courant des dernières actualités !
Le meilleur de l’actualité sur le Cloud, le DevOps, l’IT directement dans votre boîte mail.