« J’ai passé des nuits blanches à essayer d’ajouter des fonctionnalités dans le code que nous avons acquis d’une autre société. J’ai affaire à la forme la plus pure de Legacy Code »

« J’ai vraiment du mal à gérer un code enchevêtré et non structuré avec lequel je dois travailler mais que je ne comprends pas du tout. Legacy Code ! »

Legacy code est un terme qui a probablement beaucoup de définitions différentes comme -code acquis de quelqu’un d’autre, code écrit par quelqu’un d’autre, code difficile à comprendre ou code écrit dans des technologies dépassées. Quelle que soit la définition, la plupart d’entre nous pensent que le code hérité est effrayant.

Question> Comment définiriez-vous le code hérité ?

Définir le code hérité

Michael Feathers dans son livre « Working Effectively with Legacy Code » définit le code hérité comme, le code sans tests.

Le code sans tests est un mauvais code. Peu importe à quel point il est bien écrit ; à quel point il est bien structuré ; à quel point il est bien encapsulé.Sans tests, il n’y a aucun moyen de savoir si notre code s’améliore ou empire.

Bien, une version légèrement modifiée de cette définition est « le code sans tests unitaires est appelé code hérité ». Il est toujours préférable d’avoir des tests aussi proches du code que possible (tests unitaires > tests d’intégration > tests d’interface utilisateur). Ainsi, il ne serait pas injuste d’appeler un code sans tests unitaires un code hérité.

Travailler avec un code hérité

Question> Quelle approche adopterez-vous si vous deviez faire un changement dans un code hérité ?

La plupart d’entre nous pourraient dire : « Je vais faire le changement et l’appeler un jour, pourquoi se soucier d’améliorer le code ». Le raisonnement derrière ce processus de pensée pourrait être –

  • Je n’ai pas assez de temps pour refactoriser le code, Je préférerais faire un changement et terminer mon histoire
  • Pourquoi risquer de changer la structure du code qui fonctionne en production depuis longtemps
  • Quel est l’avantage global de refactorer le code hérité

Michael Feathers appelle ce style de faire un changement comme Edit and Pray. Vous planifiez et faites vos changements et quand vous avez terminé, vous priez et priez encore plus pour que vos changements soient corrects.

Avec ce style, on ne peut que contribuer à augmenter le code Legacy.

Il existe un style différent de faire des changements qui est Cover and Modify. Construire un filet de sécurité, faire des changements dans le système, laisser le filet de sécurité fournir un retour et travailler sur ces retours.

On peut supposer sans risque que Cover and Modify est une façon d’aller pour traiter le code hérité.

Question> Mais, devriez-vous même passer du temps à écrire des tests dans le code hérité ou même penser à refactorer un code hérité ?

Devriez-vous même passer du temps à penser à refactorer un code hérité ?

La règle du scout

L’idée derrière la règle du scout, telle qu’énoncée par l’oncle Bob, est assez simple : Laissez le code plus propre que vous ne l’avez trouvé ! Chaque fois que vous touchez un ancien code, vous devez le nettoyer correctement. N’appliquez pas simplement une solution de raccourci qui rendra le code plus difficile à comprendre, mais traitez-le plutôt avec soin. Il ne suffit pas de bien écrire le code, il faut le garder propre au fil du temps.

Nous obtenons un message très fort lorsque la règle des scouts est appliquée au code hérité « laissez une trace de compréhension derrière vous pour que d’autres puissent suivre », ce qui signifie que nous allons refactorer le code pour le rendre plus compréhensible. Et afin de refactoriser, nous construirons Safety Net autour de lui.

Maintenant que nous comprenons que nous ne pouvons pas prendre de raccourcis, la seule option qui nous reste est d’écrire quelques tests, de refactoriser le code et de poursuivre le développement. Questions>

  • Quels tests devons-nous écrire ?
  • De combien devons-nous refactoriser ?

Quels tests écrire

Dans presque tous les systèmes hérités, ce que le système fait est plus important que ce qu’il est censé faire.

Tests de caractérisation, les tests dont nous avons besoin lorsque nous voulons préserver le comportement sont appelés tests de caractérisation. Un test de caractérisation est un test qui caractérise le comportement réel d’un morceau de code. Il n’y a pas de « Eh bien, il devrait faire ceci » ou « Je pense qu’il fait cela ». Les tests documentent le comportement actuel réel du système.

Écrire un test de caractérisation

Un test de caractérisation par définition documente le comportement actuel réel du système exactement de la même manière qu’il fonctionne sur l’environnement de production.

Ecrivons un test de caractérisation pour un objet Client qui génère une déclaration de texte pour certains films loués par un client.

import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){ Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented); john.addRental(rental); String statement = john.generateTextStatement();
assertEquals("", statement);
}

Ce test tente de comprendre (ou de caractériser) la génération de la « Déclaration de texte » pour un client étant donné un film pour enfants loué pour 3 jours. Parce que nous ne comprenons pas le système (du moins pour l’instant), nous nous attendons à ce que le relevé soit vide ou contenant n’importe quelle valeur fictive.

Exécutons le test et laissons-le échouer. Quand il échoue, nous avons découvert ce que le code fait réellement dans cette condition.

java.lang.AssertionError: 
Expected :""
Actual :Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points.

Maintenant, que nous connaissons le comportement du code, nous pouvons aller de l’avant et modifier le test.

import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){
String expectedStatement = "Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points"; Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented);
john.addRental(rental); Sting statement = john.generateTextStatement();
assertEquals(expectedStatement, statement);
}

Attendez, avons-nous juste copié la sortie générée par le code et placée dans notre test. Oui, c’est exactement ce que nous avons fait.

Nous n’essayons pas de trouver des bugs en ce moment. Nous essayons de mettre en place un mécanisme pour trouver des bugs plus tard, des bugs qui se manifestent comme des différences par rapport au comportement actuel du système. Lorsque nous adoptons cette perspective, notre vision des tests est différente : ils n’ont pas d’autorité morale ; ils sont juste assis là à documenter ce que le système fait réellement. À ce stade, il est très important d’avoir cette connaissance de ce que le système fait réellement quelque part.

Question> Quel est le nombre total de tests que nous écrivons pour caractériser un système ?

Réponse> C’est infini. Nous pourrions consacrer une bonne partie de notre vie à écrire cas après cas pour n’importe quelle classe d’un code hérité.

Question> Quand nous arrêtons-nous alors ? Y a-t-il un moyen de savoir quels cas sont plus importants que les autres ?

Réponse> Regardez le code que nous caractérisons. Le code lui-même peut nous donner des idées sur ce qu’il fait, et si nous avons des questions, les tests sont un moyen idéal de les poser. À ce moment-là, écrivez un ou des tests qui couvrent une assez bonne partie du code.

Question> Est-ce que cela couvre tout dans le code ?

Réponse> Il se peut que non. Mais alors nous faisons l’étape suivante. Nous pensons aux changements que nous voulons faire dans le code et essayons de déterminer si les tests que nous avons détecteront les problèmes que nous pouvons causer. S’ils ne le font pas, nous ajoutons plus de tests jusqu’à ce que nous soyons confiants qu’ils le feront.

Combien remanier ?

Il y a tellement de choses à remanier dans le code hérité et nous ne pouvons pas tout remanier. Pour répondre à cette question, nous devons revenir à la compréhension de notre objectif de refactoring du code hérité.

Nous voulons refactoriser le code hérité pour le laisser plus propre que ce qu’il était quand il nous est parvenu et pour le rendre compréhensible pour les autres.

Avec cela dit, nous voulons améliorer le système en gardant le focus sur la tâche. Nous ne voulons pas devenir fous avec le refactoring en essayant de réécrire l’ensemble du système en quelques jours. Ce que nous voulons faire, c’est « remanier le code qui nous empêche de mettre en œuvre tout nouveau changement ». Nous allons essayer de mieux comprendre cela avec un exemple dans le prochain article.

Conclusion

.

Articles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.