Les grands développeurs savent quand et comment refactoriser le code Rails
Publié: 2022-03-11Refactoriser à grande échelle : Pourquoi feriez-vous quelque chose comme ça ?
Si ce n'est pas cassé, ne le répare pas.
C'est une phrase bien connue, mais comme nous le savons, la plupart des progrès technologiques humains ont été réalisés par des personnes qui ont décidé de réparer ce qui n'est pas cassé. Surtout dans l'industrie du logiciel, on pourrait dire que la plupart de ce que nous faisons consiste à réparer ce qui n'est pas cassé.
Correction des fonctionnalités, amélioration de l'interface utilisateur, amélioration de la vitesse et de l'efficacité de la mémoire, ajout de fonctionnalités : ce sont toutes des activités pour lesquelles il est facile de voir si elles en valent la peine, et nous, en tant que développeurs Rails expérimentés, plaidons pour ou contre le fait d'y consacrer notre temps. Cependant, il existe une activité qui tombe pour la plupart dans une zone grise - la refactorisation standard, et en particulier la refactorisation de code à grande échelle.
Le terme refactoring à grande échelle mérite d'être expliqué. Exactement ce qui peut être considéré comme "à grande échelle" variera d'un cas à l'autre car le terme est un peu vague, mais je considère que tout ce qui affecte de manière significative plus que quelques classes, ou plus d'un sous-système, et son interface comme étant "grande". .” À l'autre extrémité, toute refactorisation Rails qui reste cachée derrière l'interface d'une seule classe serait certainement "petite". Bien sûr, il y a beaucoup de zones grises entre les deux. Enfin, faites confiance à votre instinct, si vous redoutez de le faire, alors c'est probablement "gros".
Le refactoring, par définition, ne produit aucune fonctionnalité visible, rien que vous puissiez montrer au client, aucun livrable. Au mieux, ils pourraient produire de petites améliorations de la vitesse et de l'utilisation de la mémoire, mais ce n'est pas l'objectif principal. On pourrait dire que l'objectif principal est le code dont vous êtes satisfait. Mais parce que vous réorganisez le code de telle manière qu'il a des conséquences considérables sur toute la base de code, il y a une chance que tout se déchaîne et qu'il y ait des problèmes. C'est bien sûr de là que vient la peur dont nous parlions. Avez-vous déjà présenté quelqu'un de nouveau à votre base de code et après avoir posé des questions sur un morceau de code particulièrement organisé, vous avez répondu avec quelque chose du genre :
Yeeeaahh, c'est du code hérité qui avait du sens à l'époque mais les spécifications ont changé et c'est maintenant trop cher pour le réparer ?
Peut-être même leur avez-vous jeté un regard très sérieux et leur avez-vous dit de laisser tomber et de ne pas y toucher.
La question, "Pourquoi voudrions-nous même le faire?" est naturel et peut être aussi important que le processus de refactoring car bien souvent il y a d'autres personnes que vous devez convaincre de vous permettre de passer votre temps coûteux sur le refactoring. Considérons donc les cas où vous voudriez le faire et les avantages à en tirer :
Amélioration des performances
Vous êtes satisfait de l'organisation actuelle de votre code du point de vue de la maintenabilité, mais cela pose toujours des problèmes de performances. Il est tout simplement trop difficile d'optimiser la configuration actuelle et les modifications seraient très fragiles.
Il n'y a qu'une seule chose à faire ici et c'est de la profiler en détail. Exécutez des benchmarks et faites des estimations sur le montant que vous gagnerez, puis essayez d'estimer comment cela se traduira par des gains concrets. Parfois, vous pourriez même réaliser que la refactorisation de code proposée n'en vaut pas la peine. D'autres fois, vous aurez des données dures et froides pour étayer votre cas.
Améliorations architecturales
Peut-être que l'architecture est correcte mais quelque peu obsolète, ou peut-être est-elle si mauvaise que vous grincer des dents chaque fois que vous touchez cette partie de la base de code. Cela fonctionne bien et rapidement, mais c'est pénible d'ajouter de nouvelles fonctionnalités. C'est dans cette douleur que réside la valeur commerciale de la refactorisation. La «douleur» signifie également que le processus de refactorisation prendra plus de temps pour ajouter de nouvelles fonctionnalités, peut-être beaucoup plus longtemps.
Et il y a un avantage à en tirer. Faites des estimations du rapport coût/bénéfice pour quelques exemples de fonctionnalités avec et sans la grande refactorisation que vous proposez. Expliquez que des différences similaires s'appliqueront à la plupart des fonctionnalités à venir touchant cette partie du système maintenant et pour toujours dans le futur pendant le développement du système. Vos estimations peuvent être erronées, car elles le sont souvent dans le développement de logiciels, mais leurs ratios seront probablement proches.
Le mettre à jour
Parfois, le code est initialement bien écrit. Vous en êtes extrêmement content. Il est rapide, économe en mémoire, maintenable et bien aligné sur les spécifications. Initialement. Mais ensuite, les spécifications changent, les objectifs commerciaux changent ou vous apprenez quelque chose de nouveau sur vos utilisateurs finaux qui invalide vos hypothèses initiales. Le code fonctionne toujours bien, et vous en êtes toujours assez satisfait, mais quelque chose est juste gênant quand vous le regardez dans le contexte du produit final. Les choses sont placées sur le sous-système légèrement erroné ou les propriétés se trouvent dans la mauvaise classe, ou peut-être que certains noms n'ont plus de sens. Ils remplissent désormais un rôle dont le nom en termes commerciaux est complètement différent. Cependant, il est encore très difficile de justifier tout type de refactorisation Rails à grande échelle puisque le travail impliqué sera à l'échelle de n'importe lequel des autres exemples, mais les avantages sont beaucoup moins tangibles. Quand on y pense, ce n'est même pas si difficile de l'entretenir. Vous devez juste vous rappeler que certaines choses sont en fait autre chose. Vous devez juste vous rappeler que A signifie en fait B et que la propriété Y sur A se rapporte en fait à C.
Et c'est là que réside le véritable avantage. Dans le domaine de la neuropsychologie, de nombreuses expériences suggèrent que notre mémoire à court terme ou de travail est capable de contenir seulement 7+/-2 éléments, l'une d'elles étant l'expérience de Sternberg. Lorsque nous étudions un sujet, nous commençons par des éléments de base et, au départ, lorsque nous pensons à des concepts de niveau supérieur, nous devons réfléchir à leurs définitions. Par exemple, considérez un terme simple "mot de passe SHA256 salé". Au départ, nous devons conserver dans notre mémoire de travail les définitions de "salé" et "SHA256" et peut-être même une définition de "fonction de hachage". Mais une fois que nous comprenons parfaitement le terme, il n'occupe qu'un seul emplacement de mémoire car nous le comprenons intuitivement. C'est l'une des raisons pour lesquelles nous devons comprendre pleinement les concepts de niveau inférieur pour pouvoir raisonner sur ceux de niveau supérieur. Il en est de même pour les termes et définitions propres à notre projet. Mais si nous devons nous souvenir de la traduction du vrai sens à chaque fois que nous discutons de notre code, cette traduction occupe un autre de ces précieux emplacements de mémoire de travail. Cela produit une charge cognitive et rend plus difficile le raisonnement à travers la logique de notre code. À son tour, s'il est plus difficile de raisonner, cela signifie qu'il y a plus de chances que nous oubliions un point important et introduisions un bogue.
Et n'oublions pas les effets secondaires les plus évidents. Il y a de fortes chances de confusion lors de la discussion des modifications avec notre client ou toute personne connaissant uniquement les termes commerciaux corrects. Les nouvelles personnes qui rejoignent l'équipe doivent se familiariser à la fois avec la terminologie métier et avec ses équivalents dans le code.
Je pense que ces raisons sont très convaincantes et justifient le coût de la refactorisation dans de nombreux cas. Néanmoins, soyez prudent, il peut y avoir de nombreux cas extrêmes où vous devez utiliser votre meilleur jugement pour déterminer quand et comment refactoriser.
En fin de compte, la refactorisation à grande échelle est bonne pour les mêmes raisons que beaucoup d'entre nous aiment démarrer un nouveau projet. Vous regardez ce fichier source vierge et un nouveau monde courageux commence à tourbillonner dans votre esprit. Cette fois, vous le ferez correctement, le code sera élégant, il sera à la fois magnifiquement présenté, rapide et robuste et facilement extensible, et surtout ce sera un plaisir de travailler avec chaque jour. La refactorisation, à petite et à grande échelle, vous permet de retrouver ce sentiment, de donner un nouveau souffle à une ancienne base de code et de rembourser cette dette technique.
Enfin, il est préférable que la refactorisation soit guidée par des plans visant à faciliter la mise en œuvre d'une certaine nouvelle fonctionnalité. Dans ce cas, la refactorisation sera plus ciblée et une grande partie du temps consacré à la refactorisation sera également récupérée immédiatement grâce à une implémentation plus rapide de la fonctionnalité elle-même.
Préparation
Assurez-vous que votre couverture de test est très bonne dans tous les domaines de la base de code que vous êtes susceptible de toucher. Si vous voyez certaines parties qui ne sont pas bien couvertes, passez d'abord un peu de temps à augmenter la couverture du test. Si vous n'avez pas du tout de tests, vous devriez d'abord passer du temps à créer ces tests. Si vous ne pouvez pas créer une suite de tests appropriée, concentrez-vous sur les tests d'acceptation et écrivez-en autant que vous le pouvez, et assurez-vous d'écrire des tests unitaires pendant que vous refactorisez. Théoriquement, vous pouvez effectuer la refactorisation du code sans une bonne couverture de test, mais cela vous demandera de faire beaucoup de tests manuels et de le faire souvent. Cela prendra beaucoup plus de temps et sera plus sujet aux erreurs. En fin de compte, si votre couverture de test n'est pas assez bonne, le coût d'une refactorisation Rails à grande échelle peut être si élevé que vous devriez malheureusement envisager de ne pas le faire du tout. À mon avis, c'est un avantage des tests automatisés qui n'est pas assez souvent souligné. Les tests automatisés vous permettent de refactoriser souvent et, plus important encore, d'être plus audacieux.
Une fois que vous vous êtes assuré que votre couverture de test est bonne, il est temps de commencer à cartographier vos modifications. Au début, vous ne devriez pas faire de codage. Vous devez cartographier approximativement tous les changements impliqués et tracer toutes les conséquences à travers la base de code, ainsi que charger des connaissances sur tout cela dans votre esprit. Votre objectif est de comprendre exactement pourquoi vous modifiez quelque chose et le rôle qu'il joue dans la base de code. Si vous trébuchez dessus en changeant des choses simplement parce qu'elles semblent devoir être changées ou parce que quelque chose s'est cassé et que cela semble le réparer, vous vous retrouverez probablement dans une impasse. Le nouveau code semble fonctionner, mais de manière incorrecte, et maintenant vous ne pouvez même plus vous souvenir de toutes les modifications que vous avez apportées. À ce stade, vous devrez peut-être abandonner le travail que vous avez effectué sur la refactorisation du code à grande échelle, et essentiellement vous avez perdu votre temps. Prenez donc votre temps et explorez le code pour comprendre les ramifications de chaque modification que vous êtes sur le point d'apporter. Il paiera généreusement à la fin.

Vous aurez besoin d'une aide pour le processus de refactoring. Vous préférerez peut-être autre chose, mais j'aime un simple morceau de papier vierge et un stylo. Je commence par écrire le changement initial que je veux faire en haut à gauche du papier. Ensuite, je commence à chercher tous les endroits concernés par le changement et je les note sous le changement initial. Il est important ici d'utiliser votre jugement. En fin de compte, les notes et les diagrammes sur le papier sont là pour vous, alors choisissez un style qui correspond le mieux à votre mémoire. J'écris des extraits de code courts avec des puces en dessous et de nombreuses flèches menant à d'autres notes de ce type indiquant des choses qui en dépendent directement (flèche pleine) ou indirectement (flèches en pointillés). J'annote également les flèches avec des marques abrégées pour rappeler quelque chose de spécifique que j'ai remarqué dans la base de code. N'oubliez pas que vous ne reviendrez à ces notes qu'au cours des prochains jours pendant que vous effectuerez les modifications prévues et qu'il est parfaitement acceptable d'utiliser des rappels très courts et cryptés afin qu'ils utilisent moins d'espace et soient plus faciles à mettre en page sur le papier . Quelques fois, je nettoyais mon bureau des mois après une refactorisation de Rails et j'ai trouvé un de ces papiers. C'était du charabia complet, je n'avais absolument aucune idée de ce que signifiait quoi que ce soit sur ce papier, sauf qu'il aurait pu être écrit par quelqu'un devenu fou. Mais je sais que ce morceau de papier était indispensable pendant que je travaillais sur le problème. Aussi, ne pensez pas que vous devez écrire chaque changement. Vous pouvez les regrouper et suivre les détails d'une manière différente. Par exemple, sur votre article principal, vous pouvez noter que vous devez "renommer toutes les occurrences de Ab en Cd", puis vous pouvez suivre les détails de plusieurs manières différentes. Vous pouvez tous les écrire sur une feuille de papier séparée, vous pouvez planifier d'effectuer à nouveau une recherche globale pour toutes les occurrences de celui-ci, ou vous pouvez simplement laisser tous les fichiers source où la modification doit être effectuée ouverts dans l'éditeur de votre choix. et notez mentalement de les parcourir une fois que vous avez terminé de cartographier les modifications.
Lorsque vous cartographiez les conséquences de votre changement initial, de par sa nature à grande échelle, vous identifierez très probablement des changements supplémentaires qui ont eux-mêmes d'autres conséquences. Répétez l'analyse pour eux également, en notant tous les changements dépendants. Selon la taille des modifications, vous pouvez les écrire sur le même morceau de papier ou en choisir un nouveau vierge. Une chose très importante à essayer de faire lors de la cartographie des changements est d'essayer d'identifier les limites où vous pouvez réellement arrêter les changements de ramification. Vous souhaitez limiter la refactorisation au plus petit ensemble de modifications sensibles et arrondies. Si vous voyez un point auquel vous pouvez simplement vous arrêter et laisser le reste tel quel, faites-le même si vous voyez qu'il doit être refactorisé, même s'il est lié dans son concept à vos autres modifications. Terminez cette ronde de refactorisation de code, testez-la en profondeur, déployez-la et revenez pour en savoir plus. Vous devez rechercher activement ces points pour que la taille des modifications reste gérable. Bien sûr, comme toujours, faites un jugement. Très souvent, je suis arrivé à un point où je pouvais couper le processus de refactoring en ajoutant des classes proxy pour faire un peu de traduction d'interface. J'ai même commencé à les implémenter quand j'ai réalisé qu'ils demanderaient autant de travail que de pousser le refactoring un peu plus loin jusqu'au point où il y aurait un point "d'arrêt naturel" (c'est-à-dire presque aucun code proxy nécessaire). J'ai ensuite fait marche arrière, annulant mes dernières modifications et refactorisé. Si tout cela ressemble un peu à la cartographie d'un territoire inexploré, c'est parce que j'ai l'impression que c'est le cas, sauf que les cartes de territoire ne sont qu'en deux dimensions.
Exécution
Une fois que vous avez terminé votre préparation de refactorisation, il est temps d'exécuter le plan. Assurez-vous que votre concentration est élevée et assurez-vous un environnement sans distraction. Je vais parfois jusqu'à désactiver complètement la connexion Internet à ce stade. Le truc, c'est que si vous vous êtes bien préparé, ayez une bonne série de notes sur le papier à côté de vous, et votre concentration est en place ! Vous pouvez souvent passer très rapidement à travers les changements de cette façon. En théorie, l'essentiel du travail a été fait en amont, lors de la préparation.
Une fois que vous êtes en train de refactoriser le code, faites attention aux morceaux de code étranges qui font quelque chose de très spécifique et peuvent sembler être du mauvais code. Peut-être qu'il s'agit d'un mauvais code, mais très souvent, ils traitent en fait un cas étrange qui a été découvert lors d'une enquête sur un bogue en production. Au fil du temps, la plupart des codes Rails poussent des « poils » ou des « verrues » qui gèrent des bogues de cas étranges, par exemple, un code de réponse étrange ici qui est peut-être nécessaire pour IE6 ou une condition qui gère un bogue de synchronisation étrange. Ils ne sont pas importants pour la vue d'ensemble, mais sont toujours des détails importants. Idéalement, ils sont explicitement couverts par des tests unitaires, sinon essayez d'abord de les couvrir. Une fois, j'ai été chargé de porter une application de taille moyenne de Rails 2 à Rails 3. Je connaissais très bien le code mais c'était un peu brouillon et il y avait beaucoup de changements à prendre en compte, j'ai donc opté pour la réimplémentation. En fait, ce n'était pas une véritable réimplémentation, car ce n'est presque jamais une décision intelligente, mais j'ai commencé avec une application Rails 3 vierge et j'ai refactorisé des tranches verticales de l'ancienne application dans la nouvelle, en utilisant à peu près le processus décrit. Chaque fois que je terminais une tranche verticale, je parcourais l'ancien code Rails, examinant chaque ligne et vérifiant qu'elle avait sa contrepartie dans le nouveau code. J'étais essentiellement en train de choisir tous les anciens "cheveux" du code et de les répliquer dans la nouvelle base de code. En fin de compte, la nouvelle base de code a traité tous les cas particuliers.
Assurez-vous d'effectuer des tests manuels assez souvent. Cela vous obligera à la fois à rechercher des «casses» naturelles dans le processus de refactorisation qui vous permettront de tester une partie du système et vous donneront l'assurance que vous n'avez rien cassé que vous ne vous attendiez pas à casser dans le processus .
Envelopper
Une fois que vous avez terminé de refactoriser votre code Rails, assurez-vous de revoir toutes vos modifications une dernière fois. Regardez l'ensemble du diff et passez-le en revue. Très souvent, vous remarquerez des choses subtiles que vous avez manquées au début de la refactorisation parce que vous n'aviez pas les connaissances que vous avez maintenant. C'est un bel avantage de la refactorisation à grande échelle : vous obtenez une image mentale plus claire de l'organisation du code, surtout si vous ne l'avez pas écrit à l'origine.
Si possible, demandez à un collègue de l'examiner également. Il n'a même pas besoin d'être particulièrement familier avec cette partie exacte de la base de code, mais il devrait avoir une connaissance générale du projet et de son code. Avoir un regard neuf sur les changements peut aider beaucoup. Si vous ne pouvez absolument pas demander à un autre développeur de les regarder, vous devrez faire semblant d'en être un vous-même. Passez une bonne nuit de sommeil et revoyez-la avec un esprit frais.
Si vous manquez de QA, vous devrez également porter ce chapeau. Encore une fois, faites une pause et éloignez-vous du code, puis revenez pour effectuer des tests manuels. Vous venez de subir l'équivalent d'entrer dans une armoire de câblage électrique encombrée avec un tas d'outils et de tout trier, peut-être de couper et de recâbler des choses, donc un peu plus de soin doit être pris que d'habitude.
Enfin, savourez les fruits de votre travail compte tenu de tous les changements prévus qui seront désormais beaucoup plus propres et plus faciles à mettre en œuvre.
Quand ne le feriez-vous pas ?
Bien qu'il y ait de nombreux avantages à effectuer régulièrement une refactorisation à grande échelle pour garder le code du projet frais et de haute qualité, cela reste une opération très coûteuse. Il y a aussi des cas où il ne serait pas conseillé :
Votre couverture de test est médiocre
Comme cela a été mentionné : une couverture de test très faible pourrait être un gros problème. Utilisez votre propre jugement, mais il serait peut-être préférable à court terme de vous concentrer sur l'augmentation de la couverture tout en travaillant sur de nouvelles fonctionnalités et en effectuant autant de refactorisations localisées à petite échelle que possible. Cela vous aidera beaucoup une fois que vous aurez décidé de franchir le pas et de trier de plus grandes parties de la base de code.
La refactorisation n'est pas pilotée par une nouvelle fonctionnalité et la base de code n'a pas changé depuis longtemps
J'ai utilisé le passé au lieu de dire "la base de code ne changera pas" exprès. À en juger par l'expérience (et par expérience, je veux dire se tromper plusieurs fois), vous ne pouvez presque jamais vous fier à vos prédictions quant au moment où une certaine partie de la base de code devra être modifiée. Alors, faites la meilleure chose suivante : regardez vers le passé et supposez que le passé se répétera. Si quelque chose n'a pas été changé depuis longtemps, vous n'avez probablement pas besoin de le changer maintenant. Attendez que ce changement se produise et travaillez sur autre chose.
Vous êtes pressé par le temps
La maintenance est la partie la plus coûteuse du cycle de vie du projet et la refactorisation la rend moins coûteuse. Il est absolument nécessaire pour toute entreprise d'utiliser le refactoring pour réduire la dette technique afin de rendre la maintenance future moins chère. Sinon, il risque d'entrer dans un cercle vicieux dans lequel il devient de plus en plus coûteux d'ajouter de nouvelles fonctionnalités. J'espère que c'est évident pourquoi c'est mauvais.
Cela dit, la refactorisation à grande échelle est très, très imprévisible en ce qui concerne le temps qu'elle prendra, et vous ne devriez pas le faire à moitié. Si, pour des raisons internes ou externes, vous êtes pressé par le temps et que vous n'êtes pas sûr de pouvoir terminer dans ce délai, vous devrez peut-être abandonner la refactorisation. La pression et le stress, en particulier ceux induits par le temps, conduisent à un niveau de concentration inférieur, ce qui est absolument nécessaire pour une refactorisation à grande échelle. Travaillez à obtenir plus d'"adhésion" de votre équipe pour réserver du temps et consultez votre calendrier pour une période où vous aurez le temps. Il n'est pas nécessaire que ce soit une période de temps continue. Bien sûr, vous aurez d'autres problèmes à résoudre, mais ces pauses ne devraient pas durer plus d'un jour ou deux. Si c'est le cas, vous devrez vous rappeler votre propre plan car vous commencerez à oublier ce que vous avez appris sur la base de code et exactement où vous vous êtes arrêté.
Conclusion
J'espère vous avoir donné quelques indications utiles et vous avoir convaincu des avantages, et oserais-je dire de la nécessité, d'effectuer une refactorisation à grande échelle à certaines occasions. Le sujet est très vague, et bien sûr, rien de ce qui est dit ici n'est une vérité définitive et les détails varieront d'un projet à l'autre. J'ai essayé de donner des conseils, qui sont à mon avis, généralement applicables, mais comme toujours, considérez votre cas particulier et utilisez votre propre expérience pour vous adapter à ses défis spécifiques. Bonne chance pour le refactoring !