Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

This article (in french) shows a TDD approach to convert arabic number into roman numbers.

NotificationsYou must be signed in to change notification settings

STudio26/TDD_Chiffres_romains

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 

Repository files navigation

Qu'est-ce que le Test Driven Developpement ?

Le Test Driven Development (TDD) est une manière de développer, où ce sont les spécifications écrites sous forme de tests (et leurs résultats attendus) qui guident votre développement avant l'écriture du code. Chaque test ajouté fait avancer vers la résolution du problème, chaque modification du code déjà écrit est encouragée, tant que les tests précédents ne sont pas "cassés". Si vous cassez un test que vous aviez validé, vous devez revenir en arrière sur votre code ajouté et appliquer d'autres modifications qui vous feront progresser (et jamais régresser).

Vous êtes lassés des exemples classiques de TDD tel queFizzBuzz et vous souhaitez aller un tout petit peu plus loin ? Dans cet article je vous présente un nouvel exercice à pratiquer si vous êtes en manque d'inspiration ainsi qu'une ébauche de solution. Attention, je n'ai pas dit que l'exemple FizzBuzz n'était pas un bon exemple, au contraire, c'est un exemple que je vous invite à pratiquer au moins une fois ! Mais si vous cherchez à aller plus loin et que vous ne savez pas dans quelle direction aller, vous trouverez, je l'espère, une piste dans cet article.La numérotation romaine

Sans vouloir rappeler toutes les règles d'écriture des nombres romains (Wikipedia fait cela très bien), voici la liste des premiers nombres de 1 à 10 :I,II,III,IV,V,VI,VII,VIII,IX etX.

Dans la démarche TDD je vois ces nombres comme une spécification. Si on donne 1 à notre convertisseur, il répondraI, si on lui donne 2 il répondraII, si on lui donne 3 il répondraIII et ainsi de suite (non, si on lui donne 4 il ne répondra pasIIII maisIV). Quoi de plus rassurant que d'ajouter une minuscule étape, un baby step (un nouveau test qui nous fait progresser) et d'être sûr de ne rien casser de ce qu'on a déjà codé ?

Lorsqu'on pratique le TDD il faut ajouter le minimum de code pour faire passer le test. On part de rien (vraiment rien), on écrit un test, une spécification, et on fait en sorte que cela fonctionne. On a d'abord une erreur de compilation (je prendrai Java comme langage d'exemple) que l'on va corriger (rendez-vous compte on part de zéro, la classe que l'on référence dans le code de test n'existe même pas !), probablement une deuxième (la méthode que vous appelez n'existe pas non plus !), que l'on va corriger également puis ensuite il faudra écrire du code, et le plus souvent on retourne la valeur en dur pour commencer. Cela n'est pas intuitif, mais on va à la fois répondre à la spécification et surtout écrire le moins de code possible.

1, 2, 3 nous irons au bois

On commence ? La première spécification dit qu'on attendI pour le nombre 1. Ce qui donne, en terme de tests :

packagecom.mycompany;importstaticorg.junit.Assert.assertEquals;importorg.junit.Test;publicclassRomanNumbersTest {@TestpublicvoidOneShouldReturnI() {RomanNumberroman =newRomanNumber();assertEquals("I",roman.convert(1));    }}

Évidemment cela ne se compile même pas, la classeRomanNumber n'existe pas. Pour pouvoir faire passer le test, le minimum est de créer cette classe ensuite… on verra.

Ce qui nous donne :

packagecom.mycompanypublicclassRomanNumber {publicRomanNumber() {    }}

Mais on a toujours une erreur de compilation dans la classe de test car la méthodeconvert() n'existe pas. Il faut donc créer cette méthode. Elle retourne une chaine, en attendant d'en savoir plus on va retourner une chaine vide.

packagecom.mycompany;publicclassRomanNumber {RomanNumber() {    }Stringconvert(intvalue) {return"";    }}

On n'a plus d'erreur de compilation ! Évidemment le test ne passe pas, car à aucun moment on ne retourneI. Le minimum de code pour que le test passe est de retournerI… en dur. Si si, vous avez bien lu,en dur.

packagecom.mycompanypublicclassRomanNumber {RomanNumber() {    }Stringconvert(intvalue) {return"I";    }}

Bravo, le premier test passe. 1 en nombre romain s'écritI.

Passons à la suite. Si on veut convertir 2 en nombre romain on doit avoir II en résultat. Écrivons le test, qui échouera inévitablement (car on retourne toujoursI), puis adaptons le code. Cela nous donne le code suivant pour le test :

@TestpublicvoidTwoShouldReturnII() {RomanNumberroman =newRomanNumber();assertEquals("II",roman.convert(2));    }

Et voici le code le plus simple possible issu de la méthode précédente et qui ferra passer les deux tests.

Stringconvert(intvalue) {if (value ==2)return"II";return"I";    }

On ne cherche pas la "meilleure" solution. On aurait pu mettre unelse, mais à quoi bon ? En le mettant, bien évidemment les deux tests vont réussir. Il n'y a pas d'obligation. Vous verrez qu'en avançant on structurera assez vite le code. De même, je n'ai pas mis d'accolades après leif. Si un jour vous ajoutez du code et oubliez d'ajouter les accolades nécessaires, vous pourrez vraiment compter sur les tests pour vous dire que quelque chose ne va pas 😀.

Allez, on passe à trois ? On ajoute le test, on fait échouer le code (il va retournerI alors qu'on attendIII) et on adapte le code, au plus simple. Ce qui nous donne (par exemple) :

Stringconvert(intvalue) {if (value ==3)return"III";if (value ==2)return"II";return"I";    }

Les tests passent, cool ! Là on se pose, et on se souvient d'avoir entendu que d'abord on écrit un test qui échoue, qu'ensuite on écrit le code pour que le test passe et ensuite on fait du réusinage de code (durefactoring). Si on a 1 on retourneI, si on a 2 on retourneII et si on a 3 on retourneIII, on ne pourrait pas écrire cela plus simplement ? Il y aplein de manières différentes de faire cela d'ailleurs. On essaye, et si c'est faux, les tests vont échouer, on ne risque donc rien 😉. On peut toujours revenir en arrière…

Stringconvert(intvalue) {StringBuilderanswer =newStringBuilder();for (inti =1;i <=value;i++)answer.append("I");returnanswer.toString();    }

Ça passe, les trois tests sont toujours verts.

4, 5, 6 cueillir des cerises

On va tenter de passer à quatre. Tout d'abord le test :

@TestpublicvoidFourShouldReturnIV() {RomanNumberroman =newRomanNumber();assertEquals("IV",roman.convert(4));    }

Et le minimum de code pour que notre test passe :

Stringconvert(intvalue) {if (value ==4)return"IV";StringBuilderanswer =newStringBuilder();for (inti =1;i <=value;i++)answer.append("I");returnanswer.toString();    }

Mouais, ce n'est pas très joli. Posons nous encore une fois. D'un côté on a le cas de la valeur 4, d'un autre on a le cas des valeur inférieures à 4 (1 à 3). On peut extraire une méthode pour apporter un peu de lisibilité, non ?

Cela donne, par exemple :

Stringconvert(intvalue) {if (value ==4)return"IV";returnconvertOneToThree(value);    }privateStringconvertOneToThree(intvalue) {StringBuilderanswer =newStringBuilder();for (inti =1;i <=value;i++)answer.append("I");returnanswer.toString();    }

Tous les tests sont au vert, passons au nombre 5. Je vous fais grâce du test (qu'il faut écrire et qui doit échouer avant toute chose, là il donneraIIIII au lieu deV), je vous propose le code suivant :

Stringconvert(intvalue) {if (value ==5)return"V";if (value ==4)return"IV";returnconvertOneToThree(value);    }

Bon, c'est moche, mais tout passe. Je ne vois pas trop quel refactoring on pourrait opérer… Passons au nombre 6 (si vous écrivez et exécutez le code il retourneraIIIIII mais on attendVI). Le code proposé est assez simple, et on ne voit pas forcément comment l'améliorer à ce stade.

Stringconvert(intvalue) {if (value ==6)return"VI";if (value ==5)return"V";if (value ==4)return"IV";returnconvertOneToThree(value);    }

7, 8, 9 dans mon panier neuf

J'ajoute les tests pour les nombres 7 et 8 et le code correspondant, toujours en faisant un test sur la valeur et en retournant la valeur attendue… en dur. Le code finit par ressembler à cela :

Stringconvert(intvalue) {if (value ==8)return"VIII";if (value ==7)return"VII";if (value ==6)return"VI";if (value ==5)return"V";if (value ==4)return"IV";returnconvertOneToThree(value);    }

Et là, on se dit qu'on peut améliorer tout ça, non ? 6, 7 et 8 c'est 5 (V) plus 1, 2 et 3 respectivement, non ? Et on a déjà une méthode qui retourneI,II ouIII ! On peut tenter une modification du code. Si on casse quelque chose, les tests nous le diront tout de suite.

Stringconvert(intvalue) {if (value >5)return"V" +convertOneToThree(value -5);if (value ==5)return"V";if (value ==4)return"IV";returnconvertOneToThree(value);    }

C'est plus concis, tout aussi lisible… et cela continue de fonctionner. Mais, attendez… on dirait queconvertOneToThree() retourne une chaine vide si on lui passe zéro. Du coup, on peut englober la valeur 5 (V plusrien), vous ne croyez pas ? On essaye. Si on casse quelque chose, les tests nous le diront. C'est l'occasion de renommer la méthodeconvertOneToThree() enconvertNothingToThree().

Stringconvert(intvalue) {if (value >=5)return"V" +convertNothingToThree(value -5);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

On s'est pour l'instant arrêté à 8. Si on code le test pour 9, on va avoir la valeurVIIII. On va ajouter du code pour que le test passe. Allons au plus simple et voyons voir ce que cela donne :

Stringconvert(intvalue) {if (value ==9)return"IX";if (value >=5)return"V" +convertNothingToThree(value -5);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

10, 11, 12 elles seront toutes rouges

On continue avec le nombre 10 ? En écrivant le test vous aurez la valeurVIIIII alors que c'estX qui est attendu.

Le code le plus simple pour que cela passe pour le nombre 10 pourrait être :

Stringconvert(intvalue) {if (value ==10)return"X";if (value ==9)return"IX";if (value >=5)return"V" +convertNothingToThree(value -5);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

Après la ligne où on teste si la valeur est supérieur ou égale à 5, on traite les cas 5, 6, 7 et 8. Comme le code n'est plus très clair, on peut extraire une méthode qui explique ce que l'on fait. Par exemple :

Stringconvert(intvalue) {if (value ==10)return"X";if (value ==9)return"IX";if (value >=5)returnconvertFiveToEight(value);if (value ==4)return"IV";returnconvertNothingToThree(value);    }privateStringconvertFiveToEight(intvalue) {return"V" +convertNothingToThree(value -5);    }

On pourrait regrouper les deux premiers cas, mais cela n'apporterait pas grand chose. 9 c'est dix mois 1, tout comme 4 c'est 5 moins 1. Il y aurait peut-être des choses à améliorer de ce côté-là, mais pour l'instant rien de précis ne se dégage. Enfin… si. On aIV,IX qui sont des exceptions, tout comme, si on va plus loin dans la numérotation,XL pour 40 etXC pour 90 ouCD pour 400 etCM pour 900.

Continuons avec onze. Si on écrit le code de test il va retournerVIIIIII. On peut facilement le corriger, sans trop réfléchir de cette manière :

Stringconvert(intvalue) {if (value ==11)return"XI";if (value ==10)return"X";if (value ==9)return"IX";if (value >=5)returnconvertFiveToEight(value);if (value ==4)return"IV";returnconvertOneToThree(value);    }

Et douze ? Et treize ? J'accélère légèrement, mais je m'obligetoujours à écrire le test et à le faire échouer avant d'ajouter le code pour faire passer le test. Ne prenez pas de raccourci, c'est dangereux !

Stringconvert(intvalue) {if (value ==13)return"XIII";if (value ==12)return"XII";if (value ==11)return"XI";if (value ==10)return"X";if (value ==9)return"IX";if (value >=5)returnconvertFiveToEight(value);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

Et là on peut prendre le temps de récrire les 4 première lignes comme ont l'a déjà fait.X,XI,XII etXIII c'est la concaténation deX etrien,I,II ouIII.

Stringconvert(intvalue) {if (value >=10)return"X" +convertNothingToThree(value -10);if (value ==9)return"IX";if (value >=5)returnconvertFiveToEight(value);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

Ne pourrait-on pas encore améliorer le code ? Convertir 10, 11, 12 ou 13 c'est comme ajouterrien,I,II ouIII àX. Maisrien,I,II ouIII est retourné aussi bien parconvertNothingToThree() que parconvert(), non ? Du coup ou peut facilement introduire une récursivité ici et coder ainsi :

Stringconvert(intvalue) {if (value >=10)return"X" +convert(value -10);if (value ==9)return"IX";if (value >=5)returnconvertFiveToEight(value);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

Ce faisant on se rend compte que leconvertFiveToEight() peut aussi se résumer àV concaténé àrien,I,II ouIII. Il est finalement plus simple de se séparer de cette méthodeconvertFiveToEight() introduite pour plus de lisibilité. On peut ainsi écrire :

Stringconvert(intvalue) {if (value >=10)return"X" +convert(value -10);if (value ==9)return"IX";if (value >=5)return"V" +convert(value -5);if (value ==4)return"IV";returnconvertNothingToThree(value);    }

Et là, je ne sais pas si c'est la magie du TDD, mais il émerge un design auquel on n'avait pas du tout pensé. On part d'une valeur (par exempleXII), on soustrait ce qu'on arrive à convertir (10, ouX) et on continue avec le reste (2, qui s'écritII). Est-ce que ce n'est pas cela que l'on procède nous, humains, quand on doit faire la conversion ? Évidemment cela suppose que nous connaissions les jalonsI,V,X,L,C,D etM, les règles d'additions et de répétitions (3 fois maximum) et les exceptions utilisant la soustraction commeIV,IX,XL,XC,CD etCM.Et après ?

Dans unprochain article on ira au delà deXIII, toujours avec la même méthode. Si vous voulez essayer par vous même, allez-y et faites moi part de votre retour d'expérience.

Je tiens à préciser que le code présenté ici a été écrit après avoir déjà codé ce programme de transformation une première fois. On peut considérer cela comme unkata. J'ai volontairement laissé l'introduction de la méthodeconvertFiveToEigth() dans ma démarche, car je voulais que le code soit plus clair. Il n'y a aucun problème à revenir dessus et à l'enlever, ni d'avoir honte de l'avoir ajouté à un instantt. Sincèrement, la solution qui émerge est différente de celle qui a déjà émergée. Et il n'y a aucun problème avec cela, car il n'y a pas qu'une seule manière de faire, et les deux passent l'ensemble des tests, et c'est bien ce qui compte, non ?

Épilogue

Je trouvais la méthodeconvertNothingToThree() un peu lourde :

privateStringconvertNothingToThree(intvalue) {StringBuilderanswer =newStringBuilder();for (inti =1;i <=value;i++)answer.append("I");returnanswer.toString();    }

On crée unStringBuilder (qui reste vide parfois), ensuite on entre dans un boucle pour compter le nombre de répétitions deI à générer. L'idée de la récursivité pourX,XI,XII etXIII appliquée au reste quand on a enlevéX ne s'appliquerait-elle pas aussi àrien,I,II etIII ? Mais si, bien sûr ! On peut simplement écrire :

privateStringconvertNothingToThree(intvalue) {return (value ==0) ?"" :"I" +convertNothingToThree(value -1);    }

Et comme je trouve qu'ici extraire une méthode pour une ligne n'apporte plus grand chose, on peut décider de tout regrouper ainsi dansconvert() :

Stringconvert(intvalue) {if (value >=10)return"X" +convert(value -10);if (value ==9)return"IX";if (value >=5)return"V" +convert(value -5);if (value ==4)return"IV";return (value ==0) ?"" :"I" +convert(value -1);    }

Évidemment les tests sont toujours verts.

Avec un peu de recul, ce qui est rassurant dans cette version, c'est qu'on ne mentionne que les jalons (I,V,X) et les exceptions (IV etIX). Et qu'une seule fois chacun ! C'est rassurant dans la mesure où on se rend compte qu'on a que du code utile, rien de superflu. Et c'est effectivementce qu'apporte le TDD, une conception (utiliser les jalons, les exceptions données par les spécifications, réusiner le code) qui a étépilotée par lestests.

Dans laseconde partie de cet article on ira plus loin et traiteratous les cas, et vous serez peut-être surpris, comme je l'ai été, de la tournure que prendra le code !

Un énorme merci àBenoit Gantaume qui m'a conforté dans l'idée qu'il fallait sortir du contenu et ne pas attendre que le contenu soit parfait avant de le publier (sous peine de ne jamais le publier) !

Annexe

Voici l'ensemble des tests qui ont été écrits jusqu'à présent :

packagecom.mycompany;importstaticorg.junit.Assert.assertEquals;importorg.junit.Test;publicclassRomanNumbersTest {@TestpublicvoidOneShouldReturnI() {RomanNumberroman =newRomanNumber();assertEquals("I",roman.convert(1));    }@TestpublicvoidTwoShouldReturnII() {RomanNumberroman =newRomanNumber();assertEquals("II",roman.convert(2));    }@TestpublicvoidThreeShouldReturnIII() {RomanNumberroman =newRomanNumber();assertEquals("III",roman.convert(3));    }@TestpublicvoidFourShouldReturnIV() {RomanNumberroman =newRomanNumber();assertEquals("IV",roman.convert(4));    }@TestpublicvoidFiveShouldReturnV() {RomanNumberroman =newRomanNumber();assertEquals("V",roman.convert(5));    }@TestpublicvoidSixShouldReturnVI() {RomanNumberroman =newRomanNumber();assertEquals("VI",roman.convert(6));    }@TestpublicvoidSevenShouldReturnVII() {RomanNumberroman =newRomanNumber();assertEquals("VII",roman.convert(7));    }@TestpublicvoidEightShouldReturnVIII() {RomanNumberroman =newRomanNumber();assertEquals("VIII",roman.convert(8));    }@TestpublicvoidNineShouldReturnIX() {RomanNumberroman =newRomanNumber();assertEquals("IX",roman.convert(9));    }@TestpublicvoidTenShouldReturnX() {RomanNumberroman =newRomanNumber();assertEquals("X",roman.convert(10));    }@TestpublicvoidElevenShouldReturnXI() {RomanNumberroman =newRomanNumber();assertEquals("XI",roman.convert(11));    }@TestpublicvoidTwelveShouldReturnXII() {RomanNumberroman =newRomanNumber();assertEquals("XII",roman.convert(12));    }@TestpublicvoidThirteenShouldReturnXIII() {RomanNumberroman =newRomanNumber();assertEquals("XIII",roman.convert(13));    }}

About

This article (in french) shows a TDD approach to convert arabic number into roman numbers.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp