Quelle est la meilleure façon de gérer le refactoring d'un gros fichier?

41

Je travaille actuellement sur un projet plus important qui contient malheureusement des fichiers dont les consignes de qualité logicielle n'étaient pas toujours suivies. Cela inclut les gros fichiers (lire 2000-4000 lignes) qui contiennent clairement plusieurs fonctionnalités distinctes.

Maintenant, je veux refactoriser ces gros fichiers en plusieurs petits. Le problème est que, vu leur taille, plusieurs personnes (moi inclus) de différentes branches travaillent sur ces fichiers. Donc, je ne peux pas vraiment me séparer de développement et de refactorisation, car il deviendra difficile de fusionner ces refactorisations avec les changements des autres peuples.

Nous pourrions bien sûr demander à tout le monde de fusionner pour développer, "geler" les fichiers (c'est-à-dire ne plus permettre à personne de les éditer), refactoriser, puis "débloquer". Mais ce n’est pas non plus une bonne chose, car cela obligerait tout le monde à arrêter son travail sur ces fichiers jusqu’à ce que le refactoring soit terminé.

Donc, y a-t-il un moyen de refactoriser, de ne demander à personne d’arrêter de travailler (trop longtemps) ou de fusionner leurs branches de fonctions pour se développer?

Hoff
la source
6
Je pense que cela dépend aussi du langage de programmation utilisé.
Robert Andrzejuk Le
8
J'aime les checkins "petits incrémentaux". À moins que quelqu'un ne garde pas sa copie de la pension livrée, cette pratique minimisera les conflits de fusion pour tout le monde.
Matt Raffel
5
À quoi ressemblent vos tests? Si vous voulez refactoriser un gros morceau de code (et probablement important!), Assurez-vous que votre suite de tests est en très bon état avant de refactoriser. Cela rendra beaucoup plus facile de vous assurer que vous avez bien compris dans les fichiers plus petits.
CorsiKa
1
Vous pouvez adopter de nombreuses approches à cet égard et la meilleure approche dépend de votre situation.
Stephen
3
J'ai rejoint le projet où le fichier le plus important contient 10 000 lignes et contient, entre autres, une classe de 6 000 lignes et que tout le monde a peur de le toucher. Ce que je veux dire, c'est que votre question est excellente. Nous avons même inventé une blague selon laquelle cette classe est une bonne raison de déverrouiller la molette de défilement de nos souris.
ElmoVanKielmo

Réponses:

41

Vous avez bien compris qu'il ne s'agissait pas d'un problème technique, mais plutôt social: si vous souhaitez éviter des conflits de fusion excessifs, l'équipe doit collaborer de manière à éviter ces conflits.

Cela fait partie d'un problème plus important avec Git, dans lequel la création de branches est très facile mais la fusion peut encore demander beaucoup d'efforts. Les équipes de développement ont tendance à lancer de nombreuses branches et sont ensuite surprises de la difficulté de les fusionner, peut-être parce qu’elles essaient d’imiter le flux Git sans comprendre son contexte.

La règle générale pour les fusions rapides et faciles est d'empêcher l'accumulation de grandes différences, en particulier que les branches de fonctions doivent être de très courte durée (heures ou jours, pas mois). Une équipe de développement capable d'intégrer rapidement leurs changements verra moins de conflits de fusion. Si du code n'est pas encore prêt pour la production, il peut être possible de l'intégrer mais de le désactiver via un indicateur de fonctionnalité. Dès que le code a été intégré dans votre branche principale, il devient accessible au type de refactoring que vous essayez de faire.

C'est peut-être trop pour votre problème immédiat. Cependant, il est peut-être possible de demander à des collègues de fusionner leurs modifications ayant une incidence sur ce fichier jusqu'à la fin de la semaine afin que vous puissiez effectuer la refactorisation. S'ils attendent plus longtemps, ils devront eux-mêmes faire face aux conflits de fusion. Ce n'est pas impossible, c'est juste un travail évitable.

Vous voudrez peut-être également éviter de briser de larges bandes de code dépendant et effectuer uniquement des modifications compatibles avec les API. Par exemple, si vous souhaitez extraire certaines fonctionnalités dans un module séparé:

  1. Extraire la fonctionnalité dans un module séparé.
  2. Modifiez les anciennes fonctions pour transférer leurs appels vers la nouvelle API.
  3. Au fil du temps, portez le code dépendant de la nouvelle API.
  4. Enfin, vous pouvez supprimer les anciennes fonctions.
  5. (Répétez l'opération pour le prochain groupe de fonctionnalités)

Ce processus en plusieurs étapes peut éviter de nombreux conflits de fusion. En particulier, il y aura des conflits uniquement si quelqu'un d'autre modifie également la fonctionnalité que vous avez extraite. Le coût de cette approche est qu’il est beaucoup plus lent que de tout changer en même temps et que vous avez temporairement deux API en double. Ce n’est pas si grave tant que quelque chose d’urgence n’interrompt cette refactorisation, la duplication est oubliée ou dépériorisée, et vous vous retrouvez avec un tas de dettes technologiques.

Mais au final, toute solution nécessitera une coordination avec votre équipe.

Amon
la source
1
@ Laiv Malheureusement, tous ces conseils sont extrêmement généraux, mais certaines idées hors de l'espace agile-ish comme l'intégration continue ont clairement leurs mérites. Les équipes qui travaillent ensemble (et intègrent souvent leur travail) auront plus de facilité à effectuer de grands changements transversaux que les équipes qui travaillent uniquement les unes à côté des autres. Cela ne concerne pas nécessairement le SDLC en général, mais plutôt la collaboration au sein de l'équipe. Certaines approches rendent le travail parallèle plus faisable (pensez principe ouvert / fermé, microservices) mais l’équipe d’OP n’est pas encore là.
amon
22
Je n'irais pas jusqu'à dire qu'une branche de fonctionnalité doit avoir une durée de vie courte, mais simplement qu'elle ne doit pas s'écarter de sa branche mère pendant de longues périodes. La fusion régulière des modifications de la branche parent vers la branche fonctionnelle fonctionne dans les cas où la branche doit rester plus longtemps. Néanmoins, il est judicieux de ne pas laisser plus de branches de fonctionnalités que nécessaire.
Dan Lyons
1
@ Laiv D'après mon expérience, il est logique de discuter au préalable d'une conception post-refactoring avec l'équipe, mais il est généralement plus facile si une seule personne modifie le code. Sinon, vous vous retrouvez avec le problème de fusionner des éléments. Les lignes 4k ont ​​l'air de beaucoup, mais ce n'est vraiment pas pour des refactorings ciblés comme extrait . (Je lirais si fort le livre de Refactoring de Martin Fowler ici si je l'avais lu.) Mais 4 k lignes n'est beaucoup que pour les refactorings non ciblés du type "voyons comment je peux améliorer cela".
amon
1
@DanLyons En principe, vous avez raison: cela peut répartir une partie de l'effort de fusion. En pratique, la fusion de Git dépend en grande partie du dernier commit commun des ancêtres en cours de fusion. La fusion maître → fonctionnalité ne nous donne pas un nouvel ancêtre commun sur le maître, mais fusion fonctionnalité → maître le fait. Avec des fusions répétées de maîtres → de fonctionnalités, il peut arriver que nous devions résoudre les mêmes conflits encore et encore (mais voir git rerere pour automatiser cela) Ici, la réfection est strictement supérieure car la pointe du maître devient le nouvel ancêtre commun, mais la réécriture de l’histoire pose d’autres problèmes.
amon
1
La réponse est OK pour moi, à l'exception de la diatribe sur Git qui rend la création de branche trop facile, et la branche devient trop souvent. Je me souviens très bien de l'époque de SVN, et même de CVS, lorsque la création de branches était assez difficile (ou du moins fastidieuse) pour que les gens l'évitent généralement, si possible, avec tous les problèmes qui y sont liés. Dans git, être un système distribué , avoir de nombreuses branches n’est vraiment pas différent d’avoir plusieurs référentiels séparés (c’est-à-dire sur chaque dev). La solution est ailleurs, être facile à créer n’est pas le problème. (Et oui, je vois que c'est juste un aparté ... mais quand même).
AnoE
30

Faites la refactorisation par petites étapes. Disons que votre gros fichier a le nom Foo:

  1. Ajouter un nouveau fichier vide Baret le valider pour "trunk".

  2. Trouvez une petite partie du code dans Foolaquelle vous pouvez être déplacé Bar. Appliquez le déplacement, mettez à jour à partir du tronc, générez et testez le code, puis validez le "tronc".

  3. Répétez l' étape 2 jusqu'à ce que Fooet Baravoir une taille égale (ou quelle que soit la taille que vous préférez)

Ainsi, la prochaine fois que vos coéquipiers mettront à jour leurs branches à partir du tronc, ils recevront vos modifications par «petites portions» et pourront les fusionner un par un, ce qui est beaucoup plus facile que de devoir fusionner une division complète en une étape. Il en va de même lorsque, à l'étape 2, vous obtenez un conflit de fusion, car une autre personne a mis à jour la liaison entre les deux.

Cela n'éliminera pas les conflits de fusion ni la nécessité de les résoudre manuellement, mais limitera chaque conflit à une petite zone de code, ce qui est beaucoup plus facile à gérer.

Et bien sûr - communiquer le refactoring dans l'équipe. Informez vos collègues de ce que vous faites pour qu'ils sachent pourquoi ils doivent s'attendre à des conflits de fusion pour le fichier en question.

Doc Brown
la source
2
Ceci est particulièrement utile lorsque l' rerereoption gits est activée
D. Ben Knoble
@ D.BenKnoble: merci pour cet ajout. Je dois admettre que je ne suis pas un expert git (mais le problème décrit ne concerne pas spécifiquement git, il s'applique à tout VCS autorisant la création de branches, et ma réponse doit correspondre à la plupart de ces systèmes).
Doc Brown
J'ai pensé basé sur la terminologie; En fait, avec git, ce type de fusion n’est encore effectué qu’une fois (si l’on tire et fusionne). Mais on peut toujours tirer et choisir, ou fusionner des commits individuels, ou rebaser en fonction des préférences du développeur. Cela prend plus de temps mais est certainement faisable si la fusion automatique risque d’échouer.
D. Ben Knoble
18

Vous envisagez de scinder le fichier en une opération atomique, mais vous pouvez apporter des modifications intermédiaires. Le fichier est devenu petit à petit avec le temps, il peut devenir petit au fil du temps.

Choisissez une pièce qui n'a pas dû changer depuis longtemps ( git blamepeut vous aider) et séparez-la en premier. Obtenez ce changement fusionné dans les branches de chacun, puis choisissez la prochaine partie la plus facile à scinder. Peut-être même que diviser une partie est une étape trop importante et que vous devriez commencer par réorganiser d'abord le fichier volumineux.

Si les gens ne fusionnent pas souvent pour se développer, vous devriez l'encourager, puis, après leur fusion, saisissez cette occasion pour séparer les parties qu'ils viennent de modifier. Ou demandez-leur de faire la scission dans le cadre de l'examen de la demande de tir.

L'idée est de progresser lentement vers votre objectif. Vous aurez l'impression que les progrès sont lents, mais vous réaliserez alors que votre code est bien meilleur. Il faut beaucoup de temps pour transformer un paquebot.

Karl Bielefeldt
la source
Le fichier a peut-être commencé volumineux. Les fichiers de cette taille peuvent être créés rapidement. Je connais des gens qui peuvent écrire des milliers de lettres de crédit en un jour ou une semaine. Et OP n'a pas mentionné les tests automatisés, ce qui m'indique qu'ils font défaut.
ChuckCottrill
9

Je vais suggérer une solution différente de la normale à ce problème.

Utilisez-le comme un événement de code d'équipe. Demandez à chacun d'inscrire son code dans la mesure du possible, puis aidez les autres qui travaillent encore avec le fichier. Une fois que le code de chaque personne concernée a été enregistré, trouvez une salle de conférence avec un projecteur et travaillez ensemble pour commencer à déplacer des éléments et dans de nouveaux fichiers.

Vous voudrez peut-être fixer un délai précis pour que cela ne soit pas une semaine d'arguments sans fin. Au lieu de cela, il pourrait même s'agir d'un événement hebdomadaire d'une à deux heures jusqu'à ce que vous obteniez ce qui se passe comme il se doit. Peut-être n’avez-vous besoin que d’une à deux heures pour refactoriser le fichier. Vous ne saurez pas jusqu'à ce que vous essayez, probablement.

Cela présente l'avantage de mettre tout le monde sur la même page (sans jeu de mots) lors de la refactorisation, mais cela peut également vous aider à éviter les erreurs et à obtenir l'avis d'autres personnes sur les groupements de méthodes possibles à maintenir, si nécessaire.

Faire cela de cette façon peut être considéré comme une révision de code intégrée, si vous faites ce genre de chose. Cela permet à la quantité appropriée de développeurs de signer votre code dès que vous l'avez enregistré et prêt à être revu. Vous voudrez peut-être quand même qu'ils vérifient le code pour tout ce que vous avez oublié, mais cela contribue grandement à faire en sorte que le processus de révision soit plus court.

Cela peut ne pas fonctionner dans toutes les situations, équipes ou entreprises, car le travail n'est pas distribué de manière à rendre cela facile. Cela peut également être interprété (à tort) comme une utilisation abusive du temps de développement. Ce code de groupe nécessite l’adhésion du responsable ainsi que du refactor lui-même.

Pour aider à vendre cette idée à votre responsable, mentionnez le bit de révision du code ainsi que tout le monde sachant où en sont les choses depuis le début. Empêcher les développeurs de perdre du temps à rechercher une foule de nouveaux fichiers peut être intéressant à éviter. Empêcher les développeurs de savoir où les choses se sont finalement terminées ou "complètement manquantes" est généralement une bonne chose. (Moins il y a de crises, mieux c'est, IMO.)

Une fois que vous obtenez un fichier refactorisé de cette manière, vous pourrez peut-être obtenir plus facilement l'approbation de plusieurs refactors, si cela réussit et est utile.

Cependant vous décidez de faire votre refactor, bonne chance!

calcul informatique
la source
C’est une suggestion fantastique qui représente un très bon moyen de parvenir à la coordination d’équipe qui sera essentielle pour que cela fonctionne. De plus, si certaines des branches ne peuvent pas être fusionnées en masterpremier, vous avez au moins tout le monde dans la salle pour vous aider à gérer les fusions dans ces branches.
Colin Young
+1 pour suggérer le code mob
Jon Raynor
1
Cela aborde exactement l'aspect social du problème.
ChuckCottrill
4

Résoudre ce problème nécessite l'adhésion des autres équipes, car vous essayez de modifier une ressource partagée (le code lui-même). Cela étant dit, je pense qu’il ya un moyen de "s’éloigner" des énormes fichiers monolithiques sans perturber les gens.

Je recommanderais également de ne pas cibler tous les fichiers volumineux en même temps, à moins que le nombre de fichiers volumineux augmente de manière incontrôlable en plus de la taille des fichiers individuels.

Refactoriser de tels fichiers provoque souvent des problèmes inattendus. La première étape consiste à empêcher les gros fichiers d’accumuler des fonctionnalités supplémentaires allant au-delà de ce qui se trouve actuellement dans les branches maître ou de développement .

Je pense que la meilleure façon de procéder consiste à utiliser des crochets de validation qui bloquent certains ajouts aux fichiers volumineux par défaut, mais qui peuvent être remplacés par un commentaire magique dans le message de validation, par exemple @bigfileok. Il est important d’être en mesure d’annuler la politique de manière simple, mais traçable. Idéalement, vous devriez pouvoir exécuter le hook de validation localement et indiquer comment remplacer cette erreur particulière dans le message d'erreur lui-même . De plus, cela est juste ma préférence, mais les commentaires magiques non reconnus ou les commentaires magiques supprimant les erreurs qui ne se sont pas réellement déclenchées dans le message de validation doivent constituer un avertissement ou une erreur au moment de la validation, de sorte que vous ne formez pas par inadvertance des personnes à la suppression des points d'ancrage, qu'ils en aient besoin ou non.

Le hook de validation peut rechercher de nouvelles classes ou effectuer une autre analyse statique (ad hoc ou non). Vous pouvez également simplement choisir un nombre de lignes ou de caractères supérieur de 10% à celui du fichier et indiquer que le fichier volumineux ne peut pas dépasser la nouvelle limite. Vous pouvez également rejeter des validations individuelles qui agrandissent le fichier volumineux de trop de lignes ou de caractères ou de w / e.

Une fois que le fichier volumineux cesse d’accumuler de nouvelles fonctionnalités, vous pouvez en refactoriser un élément à la fois (et réduire les seuils imposés par les crochets de validation en même temps pour empêcher sa croissance).

Finalement, les gros fichiers seront suffisamment petits pour que les crochets de validation puissent être complètement supprimés.

Gregory Nisbet
la source
-3

Attendez jusqu'à la maison. Fractionner le fichier, valider et fusionner en maître.

D'autres personnes devront intégrer les modifications apportées à leurs branches de fonctionnalités le matin, comme pour toute autre modification.

Ewan
la source
3
Cela voudrait dire qu'ils devront fusionner mes modifications avec leurs modifications ...
Hoff
1
De toute façon, ils doivent effectivement faire face aux fusions s’ils modifient tous ces fichiers.
Laiv
9
Cela a le problème de "Surprise, j'ai cassé toutes vos affaires." Le PO doit obtenir l’approbation et l’approbation avant de le faire, et le faire à une heure programmée telle que personne d’autre n’ait le fichier "en cours" aiderait.
computercarguy
6
Pour l'amour de cthulhu, ne faites pas cela. C'est la pire façon de travailler en équipe.
Courses de légèreté avec Monica