J'ai un projet. Dans ce projet, je souhaitais le refactoriser pour ajouter une fonctionnalité et le refactorer pour ajouter la fonctionnalité.
Le problème est que quand j'ai eu fini, il s'est avéré que je devais faire un changement d'interface mineur pour l'adapter. Alors j'ai fait le changement. Et puis, la classe consommatrice ne peut pas être implémentée avec son interface actuelle par rapport à la nouvelle, elle a donc également besoin d'une nouvelle interface. Maintenant, trois mois plus tard, j'ai dû résoudre d'innombrables problèmes pratiquement sans rapport. Je cherche à résoudre les problèmes qui étaient planifiés depuis un an ou simplement répertoriés comme ne seront pas réglés en raison de difficultés avant que la chose ne soit compilée encore.
Comment puis-je éviter ce genre de refactorisation en cascade à l'avenir? Est-ce juste un symptôme de mes classes précédentes dépendantes trop étroitement les unes des autres?
Brève édition: Dans ce cas, le refactor était la fonctionnalité, car le refactor augmentait l'extensibilité d'un morceau de code particulier et diminuait le couplage. Cela signifiait que les développeurs externes pouvaient faire plus, ce qui était la fonctionnalité que je souhaitais proposer. Ainsi, le refactor d'origine lui-même n'aurait pas dû être un changement fonctionnel.
Plus gros montage que j'ai promis il y a cinq jours:
Avant de commencer ce refactor, j'avais un système avec une interface, mais lors de l'implémentation, j'ai simplement passé en revue dynamic_cast
toutes les implémentations possibles fournies. Cela signifiait évidemment que vous ne pouviez pas simplement hériter de l'interface, d'une part, et qu'il serait impossible à quiconque sans accès à la mise en oeuvre d'implémenter cette interface. J'ai donc décidé que je voulais résoudre ce problème et ouvrir l'interface à la consommation publique afin que tout le monde puisse l'implémenter et que l'implémentation de l'interface nécessitait la totalité du contrat - évidemment une amélioration.
Quand j'ai trouvé et tué avec le feu tous les endroits où j'avais fait cela, j'ai trouvé un endroit qui s'est avéré être un problème particulier. Cela dépendait des détails d'implémentation de toutes les classes dérivées et des fonctionnalités dupliquées déjà implémentées, mais améliorées ailleurs. Il aurait pu être implémenté en termes d’interface publique et réutiliser l’implémentation existante de cette fonctionnalité. J'ai découvert qu'il fallait un élément de contexte particulier pour fonctionner correctement. Grosso modo, l'implémentation précédente de l'appelant ressemblait un peu à
for(auto&& a : as) {
f(a);
}
Cependant, pour obtenir ce contexte, je devais le changer en quelque chose de plus semblable à
std::vector<Context> contexts;
for(auto&& a : as)
contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
f(con);
Cela signifie que pour toutes les opérations qui faisaient auparavant partie f
, certaines d’entre elles doivent faire partie de la nouvelle fonction g
qui fonctionne sans contexte, et certaines d’entre elles doivent faire partie d’une partie de la fonction actuellement différée f
. Mais toutes les méthodes f
n’appellent pas nécessairement le besoin ou le désir de ce contexte - certaines d’entre elles ont besoin d’un contexte distinct qu’elles obtiennent par des moyens distincts. Donc, pour tout ce qui f
finit par s’appeler (c’est-à-dire grosso modo tout ), je devais déterminer le contexte, le cas échéant, dont ils avaient besoin, de quelle manière, et comment les scinder de l’ancien f
au nouveau f
. g
.
Et c'est comme ça que j'ai fini où je suis maintenant. La seule raison pour laquelle j'ai persévéré, c'est que j'avais besoin de cette refactorisation pour d'autres raisons.
Réponses:
La dernière fois que j'ai essayé de lancer un refactoring avec des conséquences imprévues, et que je ne pouvais pas stabiliser la construction et / ou les tests après une journée , j'ai abandonné et j'ai retourné la base de code au point précédant le refactoring.
Ensuite, j'ai commencé à analyser ce qui n'allait pas et à élaborer un meilleur plan pour procéder à la refactorisation en plusieurs étapes. Mon conseil pour éviter les refactorisations en cascade est donc simple: sachez quand arrêter , ne laissez pas les choses vous échapper!
Parfois, il faut mordre la balle et jeter une journée complète de travail, ce qui est certainement plus facile que de jeter trois mois de travail. Le jour où vous perdez n’est pas complètement vain, vous avez au moins appris à ne pas aborder le problème. Et à mon expérience, il y a toujours des possibilités pour faire de plus petites étapes dans la refactorisation.
Remarque secondaire : vous semblez être dans une situation où vous devez décider si vous êtes prêt à sacrifier trois mois de travail et à recommencer avec un nouveau plan de refactoring (et, espérons-le, plus réussi). J'imagine que ce n'est pas une décision facile à prendre, mais demandez-vous quel est le risque qu'il vous faut encore trois mois, non seulement pour stabiliser la construction, mais également pour corriger tous les bugs imprévus que vous avez probablement introduits lors de votre réécriture des trois derniers mois ? J'ai écrit "réécrire", parce que je suppose que c'est ce que vous avez vraiment fait, pas un "refactoring". Il n’est pas improbable que vous puissiez résoudre votre problème actuel plus rapidement en revenant à la dernière révision où votre projet est compilé et commencez à nouveau par une refactorisation réelle (par opposition à une "réécriture").
la source
Sûr. Un changement causant une multitude d'autres changements est à peu près la définition du couplage.
Dans les pires types de bases de code, un seul changement continuera à se propager en cascade, ce qui finira par vous amener à tout (presque) changer. Une partie de tout refactor où il y a un couplage généralisé consiste à isoler la partie sur laquelle vous travaillez. Vous devez refactoriser non seulement l'endroit où votre nouvelle fonctionnalité touche ce code, mais également tout ce qui touche à ce code.
Cela signifie généralement que certains adaptateurs permettant à l’ancien code de fonctionner avec quelque chose qui ressemble à l’ancien code, mais qui utilise la nouvelle implémentation / interface. Après tout, si vous ne faites que modifier l’interface / l’implémentation tout en laissant le couplage, vous ne gagnez rien. C'est du rouge à lèvres sur un cochon.
la source
On dirait que votre refactoring était trop ambitieux. Une refactorisation doit être appliquée par petites étapes, chacune pouvant être complétée en (disons) 30 minutes - ou, dans le pire des cas, au plus une journée - et laisse le projet constructible et tous les tests réussis.
Si vous minimisez chaque modification individuelle, il ne devrait pas être possible pour un refactoring de casser votre construction pendant longtemps. Le pire des cas est probablement de remplacer les paramètres par une méthode dans une interface largement utilisée, par exemple pour ajouter un nouveau paramètre. Mais les changements qui en découlent sont mécaniques: ajouter (et ignorer) le paramètre dans chaque implémentation et ajouter une valeur par défaut à chaque appel. Même s'il y a des centaines de références, cela ne devrait pas prendre une journée pour effectuer une telle refactorisation.
la source
Conception de rêve
L'objectif est une excellente conception et implémentation de OO pour la nouvelle fonctionnalité. Éviter la refactorisation est également un objectif.
Commencez à partir de zéro et créez un design pour la nouvelle fonctionnalité que vous souhaiteriez avoir. Prenez le temps de bien le faire.
Notez cependant que la clé est "ajouter une fonctionnalité". Les nouveautés tendent à nous faire oublier en grande partie la structure actuelle de la base de code. Notre conception de vœux pieux est indépendante. Mais nous avons alors besoin de deux choses supplémentaires:
Heuristique, leçons apprises, etc.
Le refactoring a été aussi simple que d’ajouter un paramètre par défaut à un appel de méthode existant; ou un seul appel à une méthode de classe statique.
Les méthodes d'extension sur les classes existantes peuvent aider à conserver la qualité de la nouvelle conception avec un risque minimal absolu.
"Structure" est tout. La structure est la concrétisation du principe de responsabilité unique. conception qui facilite la fonctionnalité. Le code restera court et simple tout au long de la hiérarchie de classe. Le temps nécessaire pour un nouveau design est pris en charge lors des tests, des modifications, et en évitant de pirater la jungle de codes héritée.
Les classes de rêve se concentrent sur la tâche à accomplir. Généralement, oubliez l’extension d’une classe existante, vous indiquez simplement à nouveau la cascade du refactor et devez gérer les frais généraux de la classe «plus lourde».
Purgez tous les restes de cette nouvelle fonctionnalité du code existant. Ici, une nouvelle fonctionnalité complète et bien encapsulée est plus importante que d’éviter le refactoring.
la source
Du (merveilleux) livre Travailler efficacement avec Legacy Code de Michael Feathers :
la source
On dirait que (en particulier d'après les discussions dans les commentaires), vous vous êtes retrouvé avec des règles que vous vous êtes imposées, ce qui signifie que cette modification "mineure" représente la même quantité de travail qu'une réécriture complète du logiciel.
La solution doit être "ne fais pas ça, alors" . C'est ce qui se passe dans les projets réels. De nombreuses anciennes API ont par conséquent des interfaces laides ou des paramètres abandonnés (toujours nuls), ou des fonctions nommées DoThisThing2 () qui font la même chose que DoThisThing () avec une liste de paramètres totalement différente. D'autres astuces courantes consistent à stocker des informations dans des globaux ou des pointeurs étiquetés afin de les faire passer clandestinement au-delà d'une grande partie du cadre. (Par exemple, j'ai un projet dont la moitié des tampons audio ne contiennent qu'une valeur magique de 4 octets, car cela était beaucoup plus simple que de changer la façon dont une bibliothèque appelle ses codecs audio.)
Il est difficile de donner des conseils spécifiques sans code spécifique.
la source
Tests automatisés. Vous n'avez pas besoin d'être un fanatique du TDD, ni une couverture à 100%, mais les tests automatisés vous permettent d'apporter des modifications en toute confiance. De plus, il semble que vous ayez un design avec un couplage très élevé; Vous devriez lire les principes SOLID, qui sont formulés spécifiquement pour résoudre ce type de problème dans la conception de logiciels.
Je recommanderais également ces livres.
la source
Très probablement oui. Bien que vous puissiez obtenir des effets similaires avec une base de code plutôt agréable et propre lorsque les exigences changent suffisamment
À part de m'arrêter pour travailler sur le code hérité, vous ne pouvez pas craindre. Mais vous pouvez utiliser une méthode qui évite de ne pas avoir une base de code opérationnelle pendant des jours, des semaines voire des mois.
Cette méthode s'appelle "Méthode Mikado" et fonctionne comme suit:
écrivez l'objectif que vous voulez atteindre sur une feuille de papier
faire le changement le plus simple qui vous mène dans cette direction.
vérifiez si cela fonctionne avec le compilateur et votre suite de tests. Si c'est le cas, passez à l'étape 7. Sinon, passez à l'étape 4.
sur votre papier, notez les choses à changer pour que votre changement actuel fonctionne. Dessinez des flèches de votre tâche actuelle vers les nouvelles.
Annulez vos modifications C'est l'étape importante. C'est contre-intuitif et physiquement douloureux au début, mais puisque vous venez d'essayer une chose simple, ce n'est pas si grave.
choisissez l'une des tâches ne contenant pas d'erreurs sortantes (aucune dépendance connue) et retournez à 2.
valider la modification, rayer la tâche sur le papier, choisir une tâche ne contenant aucune erreur sortante (aucune dépendance connue) et revenir à 2.
De cette façon, vous aurez une base de code opérationnelle à intervalles rapprochés. Où vous pouvez également fusionner les modifications du reste de l'équipe. Et vous avez une représentation visuelle de ce que vous savez que vous devez encore faire. Cela vous aide à décider si vous voulez continuer avec le tournage ou si vous devez l'arrêter.
la source
Le refactoring est une discipline structurée, distincte du nettoyage du code comme bon vous semble. Vous devez avoir écrit des tests unitaires avant de commencer et chaque étape doit consister en une transformation spécifique qui, vous savez, ne devrait apporter aucune modification à ses fonctionnalités. Les tests unitaires doivent réussir après chaque modification.
Bien entendu, lors du processus de refactoring, vous découvrirez naturellement les modifications à appliquer susceptibles de provoquer des ruptures. Dans ce cas, faites de votre mieux pour implémenter un shim de compatibilité pour l'ancienne interface utilisant le nouveau framework. En théorie, le système devrait toujours fonctionner comme avant et les tests unitaires devaient réussir. Vous pouvez marquer la cale de compatibilité comme une interface obsolète et la nettoyer à un moment plus opportun.
la source
Comme dit @Jules, la refactorisation et l'ajout de fonctionnalités sont deux choses très différentes.
... mais en effet, vous avez parfois besoin de modifier le fonctionnement interne pour ajouter vos éléments, mais je préférerais appeler cela modification plutôt que refactorisation.
C'est là que les choses se gâtent. Les interfaces sont conçues comme des limites pour isoler l'implémentation de la manière dont elle est utilisée. Dès que vous touchez les interfaces, tout ce qui se trouve de part et d’autre (l’implémentation ou l’utilisation) devra également être modifié. Cela peut se répandre autant que vous l'avez vécu.
Qu'une interface nécessite un changement sonne bien ... qu'elle se propage à une autre implique que les changements se propagent encore plus loin. Cela ressemble à une certaine forme d’entrée / de données qui nécessite de s’écouler dans la chaîne. Est-ce le cas?
Votre conversation est très abstraite, donc difficile à comprendre. Un exemple serait très utile. Habituellement, les interfaces doivent être assez stables et indépendantes les unes des autres, ce qui permet de modifier une partie du système sans nuire au reste ... grâce aux interfaces.
En fait, le meilleur moyen d'éviter les modifications de code en cascade consiste précisément à utiliser de bonnes interfaces. ;)
la source
Je pense que vous ne pouvez généralement pas, sauf si vous êtes prêt à garder les choses comme elles sont. Cependant, dans des situations comme la vôtre, le mieux est d’informer l’équipe et de lui faire savoir pourquoi il faudrait procéder à une refactorisation afin de poursuivre un développement plus sain. Je n'irais pas simplement régler les problèmes moi-même. J'en parlerais lors des réunions Scrum (en supposant que vous en ayez), et je l'approcherais systématiquement avec d'autres développeurs.
la source