Que faire d'un fichier source C ++ de 11 000 lignes?

229

Nous avons donc cet énorme fichier source (11000 lignes énorme?) Mainmodule.cpp dans notre projet et chaque fois que je dois le toucher, je grince des dents.

Comme ce fichier est si central et volumineux, il continue d'accumuler de plus en plus de code et je ne peux pas penser à un bon moyen de le faire commencer à rétrécir.

Le fichier est utilisé et modifié activement dans plusieurs (> 10) versions de maintenance de notre produit et il est donc très difficile de le refactoriser. Si je devais "simplement" le diviser, disons pour commencer, en 3 fichiers, la fusion des modifications des versions de maintenance deviendrait un cauchemar. Et aussi, si vous divisez un fichier avec une histoire aussi longue et riche, le suivi et la vérification des anciens changements de l' SCChistorique deviennent soudainement beaucoup plus difficiles.

Le fichier contient essentiellement la "classe principale" (répartition et coordination du travail interne principal) de notre programme, donc chaque fois qu'une fonctionnalité est ajoutée, elle affecte également ce fichier et chaque fois qu'elle se développe. :-(

Que feriez-vous dans cette situation? Avez-vous des idées sur la façon de déplacer de nouvelles fonctionnalités vers un fichier source séparé sans perturber le SCCflux de travail?

(Remarque sur les outils: nous utilisons C ++ avec Visual Studio; nous utilisons AccuRevcomme SCCmais je pense que le type de SCCn'a pas vraiment d'importance ici; nous utilisons Araxis Mergepour faire la comparaison et la fusion de fichiers)

Martin Ba
la source
15
@BoltClock: En fait, Vim l'ouvrira assez rapidement.
1er
58
69305 lignes et comptage. Un fichier dans notre application dans lequel mon collègue vide la plupart de son code. Je n'ai pas pu résister à publier ceci ici. Je n'ai personne dans ma société à qui signaler cela.
Agnel Kurian
204
Je ne comprends pas. Comment le commentaire «quitter ce travail» peut-il obtenir autant de votes positifs? Certaines personnes semblent vivre dans un pays des fées, où tous les projets sont écrits à partir de zéro et / ou utilisent 100% agile, TDD, ... (mettez n'importe lequel de vos mots à la mode ici).
Stefan
39
@Stefan: Face à une base de code similaire, c'est exactement ce que j'ai fait. Je n'avais pas envie de passer 95% de mon temps à travailler autour du truc dans une base de code vieille de 10 ans, et 5% à écrire du code. Il était en fait impossible de tester certains aspects du système (et je ne parle pas de test unitaire, je veux dire exécuter le code pour voir s'il fonctionnait). Je n'ai pas duré ma période d'essai de 6 mois, j'étais fatigué de mener des batailles perdues et d'écrire du code que je ne pouvais pas supporter.
Binary Worrier
50
en ce qui concerne le suivi de l'historique du fractionnement du fichier: utilisez la commande de copie de votre système de contrôle de version pour copier le fichier entier autant de fois que vous le souhaitez, puis supprimez tout le code de chacune des copies dont vous ne voulez pas dans ce fichier. Cela préserve l'historique global, car chacun des fichiers divisés peut retracer son historique à travers la division (qui ressemblera à une suppression géante de la plupart du contenu du fichier).
rmeador

Réponses:

86
  1. Trouvez du code dans le fichier qui est relativement stable (ne change pas rapidement et ne varie pas beaucoup entre les branches) et pourrait être une unité indépendante. Déplacez-le dans son propre fichier, et d'ailleurs dans sa propre classe, dans toutes les branches. Parce qu'il est stable, cela ne provoquera pas (beaucoup) de fusions "maladroites" qui doivent être appliquées à un fichier différent de celui sur lequel elles ont été faites à l'origine, lorsque vous fusionnez le changement d'une branche à une autre. Répéter.

  2. Trouvez du code dans le fichier qui ne s'applique essentiellement qu'à un petit nombre de branches et pourrait être autonome. Peu importe que cela change rapidement ou non, en raison du petit nombre de branches. Déplacez-le dans ses propres classes et fichiers. Répéter.

Donc, nous nous sommes débarrassés du code qui est le même partout, et du code spécifique à certaines branches.

Cela vous laisse un noyau de code mal géré - il est nécessaire partout, mais il est différent dans chaque branche (et / ou il change constamment de sorte que certaines branches courent derrière d'autres), et pourtant c'est dans un seul fichier que vous êtes tentative de fusion sans succès entre les branches. Arrêter de faire ça. Branche le fichier de façon permanente , peut-être en le renommant dans chaque branche. Ce n'est plus "principal", c'est "principal pour la configuration X". OK, vous perdez donc la possibilité d'appliquer la même modification à plusieurs branches en fusionnant, mais c'est en tout cas le cœur du code où la fusion ne fonctionne pas très bien. Si vous devez malgré tout gérer manuellement les fusions pour gérer les conflits, il n'est pas inutile de les appliquer manuellement indépendamment sur chaque branche.

Je pense que vous avez tort de dire que le type de SCC n'a pas d'importance, car par exemple les capacités de fusion de git sont probablement meilleures que l'outil de fusion que vous utilisez. Ainsi, le problème central, «la fusion est difficile» se produit à différents moments pour différents CCS. Cependant, il est peu probable que vous puissiez changer les SCC, donc le problème n'est probablement pas pertinent.

Steve Jessop
la source
Quant à la fusion: j'ai regardé GIT et j'ai regardé SVN et j'ai regardé Perforce et laissez-moi vous dire que rien de ce que j'ai vu ne bat AccuRev + Araxis pour ce que nous faisons. :-) (Bien que GIT puisse le faire [ stackoverflow.com/questions/1728922/… ] et AccuRev ne le peut pas - tout le monde doit décider par lui-même si cela fait partie de la fusion ou de l'analyse de l'historique.)
Martin Ba
Assez juste - peut-être avez-vous déjà le meilleur outil disponible. La capacité de Git à fusionner un changement survenu dans le fichier A sur la branche X, dans le fichier B sur la branche Y, devrait faciliter le fractionnement des fichiers branchés, mais le système que vous utilisez présente probablement les avantages que vous aimez. Quoi qu'il en soit, je ne vous propose pas de passer à git, je dis simplement que SCC fait une différence ici, mais même si je suis d'accord avec vous que cela peut être escompté :-)
Steve Jessop
129

La fusion ne sera pas un si grand cauchemar qu'il le sera lorsque vous obtiendrez 30000 fichiers LOC à l'avenir. Alors:

  1. Arrêtez d'ajouter plus de code à ce fichier.
  2. Sépare le.

Si vous ne pouvez pas simplement arrêter le codage pendant le processus de refactoring, vous pouvez laisser ce gros fichier tel quel pendant au moins sans y ajouter plus de code: puisqu'il contient une "classe principale", vous pouvez en hériter et conserver la classe héritée ( es) avec des fonctions surchargées dans plusieurs nouveaux petits fichiers bien conçus.

Kirill V. Lyadvinsky
la source
@Martin: heureusement, vous n'avez pas collé votre fichier ici, donc je n'ai aucune idée de sa structure. Mais l'idée générale est de le diviser en parties logiques. Ces parties logiques peuvent contenir des groupes de fonctions de votre "classe principale" ou vous pouvez la diviser en plusieurs classes auxiliaires.
Kirill V. Lyadvinsky
3
Avec 10 versions de maintenance et de nombreux développeurs actifs, il est peu probable que le fichier puisse être gelé assez longtemps.
Kobi
9
@Martin, vous avez quelques modèles GOF qui feraient l'affaire, une seule façade qui mappe les fonctions du mainmodule.cpp, ou bien (j'ai recommandé ci-dessous) de créer une suite de classes de commandes que chaque mappe à une fonction / fonctionnalité de mainmodule.app. (J'ai développé cela dans ma réponse.)
ocodo
2
Oui, tout à fait d'accord, à un moment donné, vous devez arrêter d'y ajouter du code ou, éventuellement, son 30k, 40k, 50k, kaboom mainmodule vient d'être défectueux. :-)
Chris
67

Il me semble que vous faites face à un certain nombre d'odeurs de code ici. Tout d'abord, la classe principale semble violer le principe ouvert / fermé . Il semble également qu'il assume trop de responsabilités . Pour cette raison, je suppose que le code est plus fragile qu'il ne devrait l'être.

Bien que je puisse comprendre vos préoccupations concernant la traçabilité après une refactorisation, je m'attendrais à ce que cette classe soit plutôt difficile à maintenir et à améliorer et que tout changement que vous apportez soit susceptible de provoquer des effets secondaires. Je suppose que le coût de ceux-ci l'emporte sur le coût de refactorisation de la classe.

Dans tous les cas, puisque les odeurs du code ne feront que s'aggraver avec le temps, au moins à un certain point, le coût de celles-ci l'emportera sur le coût de la refactorisation. D'après votre description, je suppose que vous avez dépassé le point de basculement.

La refactorisation doit être effectuée par petites étapes. Si possible, ajoutez des tests automatisés pour vérifier le comportement actuel avant de refactoriser quoi que ce soit. Sélectionnez ensuite de petites zones de fonctionnalités isolées et extrayez-les en tant que types afin de déléguer la responsabilité.

En tout cas, ça sonne comme un projet majeur, alors bonne chance :)

Brian Rasmussen
la source
18
Ça sent beaucoup: ça sent comme l'anti-pattern Blob est dans la maison ... en.wikipedia.org/wiki/God_object . Son repas préféré est le code de spaghetti: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan
@jdehaan: J'essayais d'être diplomatique à ce sujet :)
Brian Rasmussen
+1 De moi aussi, je n'ose même pas toucher au code complexe que j'ai écrit sans tests pour le couvrir.
Danny Thomas
49

La seule solution que j'aie jamais imaginée pour de tels problèmes est la suivante. Le gain réel par la méthode décrite est la progressivité des évolutions. Pas de révolutions ici, sinon vous aurez très vite des ennuis.

Insérez une nouvelle classe cpp au-dessus de la classe principale d'origine. Pour l'instant, il redirigerait essentiellement tous les appels vers la classe principale actuelle, mais viserait à rendre l'API de cette nouvelle classe aussi claire et succincte que possible.

Une fois cela fait, vous avez la possibilité d'ajouter de nouvelles fonctionnalités dans de nouvelles classes.

Quant aux fonctionnalités existantes, vous devez les déplacer progressivement dans de nouvelles classes à mesure qu'elles deviennent suffisamment stables. Vous perdrez l'aide SCC pour ce morceau de code, mais il n'y a pas grand-chose à faire à ce sujet. Choisissez le bon moment.

Je sais que ce n'est pas parfait, mais j'espère que cela peut aider, et le processus doit être adapté à vos besoins!

Information additionnelle

Notez que Git est un SCC qui peut suivre des morceaux de code d'un fichier à un autre. J'ai entendu de bonnes choses à ce sujet, donc cela pourrait aider pendant que vous déplacez progressivement votre travail.

Git est construit autour de la notion de blobs qui, si je comprends bien, représentent des morceaux de fichiers de code. Déplacez ces pièces dans différents fichiers et Git les trouvera, même si vous les modifiez. Hormis la vidéo de Linus Torvalds mentionnée dans les commentaires ci-dessous, je n'ai pas pu trouver quelque chose de clair à ce sujet.

Benoît
la source
Une référence sur la façon dont GIT fait cela / comment vous le faites avec GIT serait la bienvenue.
Martin Ba
@Martin Git le fait automatiquement.
Matthew
4
@Martin: Git le fait automatiquement - car il ne suit pas les fichiers, il suit le contenu. Il est en fait plus difficile dans git de simplement "obtenir l'historique d'un seul fichier".
Arafangion
1
@Martin youtube.com/watch?v=4XpnKHJAok8 est un discours où Torvalds parle de git. Il en parle plus tard dans l'exposé.
Matthew
6
@Martin, regardez cette question: stackoverflow.com/questions/1728922/…
Benjol
30

Confucius dit: "la première étape pour sortir du trou est d'arrêter de creuser le trou."

fdasfasdfdas
la source
25

Laissez-moi deviner: dix clients avec des fonctionnalités différentes et un directeur des ventes qui promeut la "personnalisation"? J'ai déjà travaillé sur des produits comme ça auparavant. Nous avons eu essentiellement le même problème.

Vous reconnaissez qu'avoir un énorme fichier est un problème, mais encore plus de problèmes sont dix versions que vous devez garder "à jour". C'est une maintenance multiple. Le CCN peut rendre cela plus facile, mais il ne peut pas le faire correctement.

Avant d'essayer de diviser le fichier en plusieurs parties, vous devez synchroniser les dix branches afin de pouvoir voir et façonner tout le code à la fois. Vous pouvez le faire une branche à la fois, en testant les deux branches par rapport au même fichier de code principal. Pour appliquer le comportement personnalisé, vous pouvez utiliser #ifdef et friends, mais il vaut mieux autant que possible d'utiliser if / else ordinaire contre des constantes définies. De cette façon, votre compilateur vérifiera tous les types et éliminera très probablement le code objet "mort" de toute façon. (Vous pouvez cependant désactiver l'avertissement concernant le code mort.)

Une fois qu'il n'y a qu'une seule version de ce fichier partagée implicitement par toutes les branches, il est alors plus facile de commencer les méthodes de refactoring traditionnelles.

Les #ifdefs sont principalement meilleurs pour les sections où le code affecté n'a de sens que dans le contexte d'autres personnalisations par branche. On peut affirmer que ceux-ci présentent également une opportunité pour le même schéma de fusion de branches, mais ne deviennent pas fous. Un projet colossal à la fois, s'il vous plaît.

À court terme, le fichier semble augmenter. C'est acceptable. Ce que vous faites, c'est rassembler des choses qui doivent être réunies. Ensuite, vous commencerez à voir des zones clairement identiques quelle que soit la version; ceux-ci peuvent être laissés seuls ou refactorisés à volonté. Les autres domaines seront clairement différents selon la version. Vous avez un certain nombre d'options dans ce cas. Une méthode consiste à déléguer les différences aux objets de stratégie par version. Une autre consiste à dériver les versions client d'une classe abstraite commune. Mais aucune de ces transformations n'est possible tant que vous disposez de dix "astuces" de développement dans différentes branches.

Ian
la source
2
Je conviens que l'objectif devrait être d'avoir une version du logiciel, mais ne serait-il pas préférable d'utiliser des fichiers de configuration (runtime) et de ne pas compiler la personnalisation du temps
Esben Skov Pedersen
Ou même des "classes de configuration" pour la construction de chaque client.
tc.
Je pense que la configuration au moment de la compilation ou de l'exécution est fonctionnellement non pertinente, mais je ne veux pas limiter les possibilités. La configuration au moment de la compilation a l'avantage que le client ne peut pas pirater avec un fichier de configuration pour activer des fonctionnalités supplémentaires, car il place toute la configuration dans l'arborescence source au lieu d'un code "objet textuel" déployable. Le revers de la médaille est que vous avez tendance à utiliser AlternateHardAndSoftLayers s'il s'agit de l'exécution.
Ian
22

Je ne sais pas si cela résout votre problème, mais ce que je suppose que vous voulez faire, c'est migrer le contenu du fichier vers des fichiers plus petits indépendants les uns des autres (résumé). Ce que j'obtiens aussi, c'est que vous avez environ 10 versions différentes du logiciel flottant et que vous devez les prendre en charge sans gâcher les choses.

Tout d'abord, il n'y a aucun moyen que cela soit facile et se résoudra en quelques minutes de brainstorming. Les fonctions liées dans votre fichier sont toutes vitales pour votre application, et simplement les couper et les migrer vers d'autres fichiers ne sauveront pas votre problème.

Je pense que vous n'avez que ces options:

  1. Ne migrez pas et restez avec ce que vous avez. Quittez éventuellement votre travail et commencez à travailler sur des logiciels sérieux avec une bonne conception en plus. La programmation extrême n'est pas toujours la meilleure solution si vous travaillez sur un projet à long terme avec suffisamment de fonds pour survivre à un crash ou deux.

  2. Élaborez une présentation de l'apparence de votre fichier une fois qu'il sera divisé. Créez les fichiers nécessaires et intégrez-les dans votre application. Renommez les fonctions ou surchargez-les pour prendre un paramètre supplémentaire (peut-être juste un simple booléen?). Une fois que vous devez travailler sur votre code, migrez les fonctions sur lesquelles vous devez travailler vers le nouveau fichier et mappez les appels de fonction des anciennes fonctions vers les nouvelles fonctions. Vous devriez toujours avoir votre fichier principal de cette façon, et toujours être en mesure de voir les modifications qui y ont été apportées, une fois qu'il s'agit d'une fonction spécifique, vous savez exactement quand il a été externalisé, etc.

  3. Essayez de convaincre vos collègues avec un bon gâteau que le flux de travail est surfait et que vous devez réécrire certaines parties de l'application afin de faire des affaires sérieuses.

Robin
la source
19

Exactement, ce problème est traité dans l'un des chapitres du livre "Working Effectively with Legacy Code" ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).

Patrick
la source
informit.com/store/product.aspx?isbn=0131177052 permet de voir la table des matières de ce livre (et 2 exemples de chapitres). Quelle est la longueur du chapitre 20? (Juste pour avoir une idée de son utilité.)
Martin Ba
17
le chapitre 20 est de 10 000 lignes, mais l'auteur travaille sur la façon de le diviser en morceaux digestibles ... 8)
Tony Delroy
1
C'est environ 23 pages, mais avec 14 images. Je pense que vous devriez l'obtenir, vous vous sentirez beaucoup plus confiant en essayant de décider quoi faire à mon humble avis.
Emile Vrijdags
Un excellent livre pour le problème, mais les recommandations qu'il fait (et d'autres recommandations dans ce fil) partagent toutes une exigence commune: si vous voulez refactoriser ce fichier pour toutes vos branches, alors la seule façon de le faire est de geler le fichier pour toutes les branches et effectuer les modifications structurelles initiales. Il n'y a aucun moyen de contourner cela. Le livre décrit une approche itérative pour extraire les sous-classes en toute sécurité sans prise en charge de la refactorisation automatique, en créant des méthodes en double et en déléguant les appels, mais tout cela est théorique si vous ne pouvez pas modifier les fichiers.
Dan Bryant
2
@Martin, le livre est excellent, mais il repose assez fortement sur le test, le refactorisateur, le cycle de test qui peut être assez difficile par rapport à l'endroit où vous vous trouvez actuellement. J'ai été dans une situation similaire et ce livre a été le plus utile que j'ai trouvé. Il a de bonnes suggestions pour le vilain problème que vous avez. Mais si vous ne pouvez pas obtenir une sorte de harnais de test dans l'image, toutes les suggestions de refactoring dans le monde ne vous aideront pas.
14

Je pense que vous feriez mieux de créer un ensemble de classes de commandes qui correspondent aux points API du mainmodule.cpp.

Une fois qu'elles sont en place, vous devrez refactoriser la base de code existante pour accéder à ces points d'API via les classes de commande, une fois cela fait, vous êtes libre de refactoriser l'implémentation de chaque commande dans une nouvelle structure de classe.

Bien sûr, avec une seule classe de 11 KLOC, le code est probablement fortement couplé et fragile, mais la création de classes de commande individuelles aidera beaucoup plus que toute autre stratégie de proxy / façade.

Je n'envie pas la tâche, mais avec le temps, ce problème ne fera qu'empirer s'il n'est pas résolu.

Mettre à jour

Je dirais que le modèle de commande est préférable à une façade.

Il est préférable de maintenir / organiser un grand nombre de classes de commandement différentes sur une façade (relativement) monolithique. Le mappage d'une seule façade sur un fichier 11 KLOC devra probablement être divisé en plusieurs groupes différents.

Pourquoi prendre la peine de comprendre ces groupes de façade? Avec le modèle de commande, vous pourrez regrouper et organiser ces petites classes de manière organique, vous aurez donc beaucoup plus de flexibilité.

Bien sûr, les deux options sont meilleures que le seul fichier 11 KLOC et croissant.

Slomojo
la source
+1 une alternative à la solution que j'ai proposée, avec la même idée: changer l'API afin de séparer le gros problème en petits.
Benoît
13

Un conseil important: ne mélangez pas le refactoring et les corrections de bugs. Ce que vous voulez, c'est une version de votre programme identique à la version précédente, sauf que le code source est différent.

Une façon pourrait être de commencer à fractionner la fonction / pièce la moins volumineuse dans son propre fichier, puis à l'inclure avec un en-tête (transformant ainsi main.cpp en une liste de #includes, qui sonne une odeur de code en soi * je ne suis pas un gourou C ++), mais au moins il est maintenant divisé en fichiers).

Vous pouvez ensuite essayer de basculer toutes les versions de maintenance vers le "nouveau" main.cpp ou quelle que soit votre structure. Encore une fois: pas d'autres changements ou corrections de bugs, car le suivi de ceux-ci est déroutant.

Une autre chose: autant que vous souhaitiez faire un grand passage pour refactoriser le tout en une seule fois, vous pourriez mordre plus que vous ne pouvez mâcher. Peut-être choisissez-vous simplement une ou deux "parties", insérez-les dans toutes les versions, puis ajoutez un peu plus de valeur pour votre client (après tout, la refactorisation n'ajoute pas de valeur directe, c'est donc un coût qui doit être justifié), puis choisissez une autre une ou deux parties.

Évidemment, cela nécessite une certaine discipline dans l'équipe pour utiliser les fichiers divisés et non seulement ajouter de nouvelles choses à main.cpp tout le temps, mais encore une fois, essayer de faire un refactor massif ne peut pas être le meilleur plan d'action.

Michael Stum
la source
1
+1 pour l'affacturage et #inclusion à nouveau. Si vous faisiez cela pour les 10 succursales (peu de travail là-bas, mais gérable), vous auriez toujours l'autre problème, celui de publier des modifications dans toutes vos succursales, mais ce problème ne se poserait pas. t ont augmenté (nécessairement). C'est moche? Oui, c'est toujours le cas, mais cela pourrait apporter un peu de rationalité au problème. Ayant passé plusieurs années à faire la maintenance et l'entretien d'un produit vraiment, vraiment gros, je sais que la maintenance implique beaucoup de douleur. À tout le moins, en tirer des leçons et servir de récit édifiant aux autres.
Jay
10

Rofl, cela me rappelle mon ancien travail. Il semble que, avant de rejoindre, tout était dans un énorme fichier (également C ++). Ensuite, ils l'ont divisé (à des points complètement aléatoires en utilisant des inclusions) en trois (fichiers encore énormes). La qualité de ce logiciel était, comme on pouvait s'y attendre, horrible. Le projet a totalisé environ 40 000 LOC. (ne contenant presque aucun commentaire mais BEAUCOUP de code en double)

Finalement, j'ai fait une réécriture complète du projet. J'ai commencé par refaire la pire partie du projet à partir de zéro. J'avais bien sûr en tête une possible (petite) interface entre cette nouvelle pièce et le reste. J'ai ensuite inséré cette partie dans l'ancien projet. Je n'ai pas refactorisé l'ancien code pour créer l'interface nécessaire, mais je l'ai simplement remplacé. Ensuite, j'ai fait de petits pas à partir de là, en réécrivant l'ancien code.

Je dois dire que cela a pris environ six mois et qu'il n'y a pas eu de développement de l'ancienne base de code à côté des corrections de bogues pendant cette période.


Éditer:

La taille est restée à environ 40k LOC, mais la nouvelle application contenait beaucoup plus de fonctionnalités et probablement moins de bugs dans sa version initiale que le logiciel vieux de 8 ans. L'une des raisons de la réécriture était également que nous avions besoin des nouvelles fonctionnalités et les introduire dans l'ancien code était presque impossible.

Le logiciel était destiné à un système embarqué, une imprimante d'étiquettes.

Un autre point que je dois ajouter est qu'en théorie le projet était en C ++. Mais ce n'était pas du tout OO, ça aurait pu être C. La nouvelle version était orientée objet.

ziggystar
la source
9
Chaque fois que j'entends "à partir de zéro" dans le sujet sur la refactorisation je tue un chaton!
Kugel
J'ai été dans une situation de sondage très similaire, bien que la boucle de programme principale avec laquelle je devais m'attaquer n'était que de ~ 9000 LOC. Et c'était déjà assez grave.
AndyUK
8

OK, donc pour la plupart, la réécriture de l'API du code de production est une mauvaise idée pour commencer. Deux choses doivent arriver.

Premièrement, votre équipe doit réellement décider de geler le code de la version de production actuelle de ce fichier.

Deuxièmement, vous devez prendre cette version de production et créer une branche qui gère les versions à l'aide de directives de prétraitement pour fractionner le gros fichier. Il est plus facile de fractionner la compilation à l'aide des directives du préprocesseur JUST (#ifdefs, #includes, #endifs) que de recoder l'API. C'est certainement plus facile pour vos SLA et votre support continu.

Ici, vous pouvez simplement supprimer les fonctions liées à un sous-système particulier de la classe et les placer dans un fichier, par exemple mainloop_foostuff.cpp, et l'inclure dans mainloop.cpp au bon emplacement.

OU

Un moyen plus long mais plus robuste serait de concevoir une structure de dépendances internes avec double indirection dans la façon dont les choses sont incluses. Cela vous permettra de diviser les choses tout en prenant soin des co-dépendances. Notez que cette approche nécessite un codage positionnel et doit donc être associée à des commentaires appropriés.

Cette approche comprendrait des composants qui sont utilisés en fonction de la variante que vous compilez.

La structure de base est que votre mainclass.cpp inclura un nouveau fichier appelé MainClassComponents.cpp après un bloc d'instructions comme celui-ci:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

La structure principale du fichier MainClassComponents.cpp serait là pour déterminer les dépendances au sein des sous-composants comme ceci:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

Et maintenant, pour chaque composant, vous créez un fichier component_xx.cpp.

Bien sûr, j'utilise des chiffres, mais vous devriez utiliser quelque chose de plus logique en fonction de votre code.

L'utilisation du préprocesseur vous permet de diviser les choses sans avoir à vous soucier des changements d'API, ce qui est un cauchemar en production.

Une fois la production réglée, vous pouvez réellement travailler sur la refonte.

Elf King
la source
Cela ressemble aux résultats de l'expérience, mais qui sont initialement douloureux.
JBRWilkinson
En fait, c'est une technique qui a été utilisée dans les compilateurs Borland C ++ pour émuler les utilisations de style Pascal pour la gestion des fichiers d'en-tête. Surtout quand ils ont fait le port initial de leur système de fenêtrage basé sur le texte.
Elf King
8

Eh bien, je comprends votre douleur :) J'ai aussi participé à quelques projets de ce type et ce n'est pas joli. Il n'y a pas de réponse simple à cela.

Une approche qui peut fonctionner pour vous consiste à commencer à ajouter des gardes de sécurité dans toutes les fonctions, c'est-à-dire à vérifier les arguments, les pré / post-conditions dans les méthodes, puis à ajouter éventuellement des tests unitaires afin de capturer la fonctionnalité actuelle des sources. Une fois que vous avez cela, vous êtes mieux outillé pour re-factoriser le code car vous aurez des assertions et des erreurs qui vous alerteront si vous avez oublié quelque chose.

Parfois, bien qu'il y ait des moments où la refactorisation peut simplement apporter plus de douleur que d'avantages. Ensuite, il peut être préférable de simplement laisser le projet d'origine et dans un état de pseudo maintenance et de recommencer à zéro, puis d'ajouter progressivement la fonctionnalité de la bête.

claptrap
la source
4

Vous ne devez pas vous préoccuper de réduire la taille du fichier, mais plutôt de réduire la taille de la classe. Cela revient à peu près au même, mais vous fait regarder le problème sous un angle différent (comme @Brian Rasmussen le suggère , votre classe semble avoir de nombreuses responsabilités).

Björn Pollex
la source
Comme toujours, j'aimerais avoir une explication sur le downvote.
Björn Pollex
4

Ce que vous avez est un exemple classique d'un anti-modèle de conception connu appelé le blob . Prenez le temps de lire l'article que je pointe ici, et vous trouverez peut-être quelque chose d'utile. En outre, si ce projet est aussi grand qu'il en a l'air, vous devriez envisager une conception pour éviter de devenir du code que vous ne pouvez pas contrôler.

David Conde
la source
4

Ce n'est pas une réponse au gros problème, mais une solution théorique à un élément spécifique de celui-ci:

  • Déterminez où vous souhaitez diviser le gros fichier en sous-fichiers. Mettez des commentaires dans un format spécial à chacun de ces points.

  • Écrivez un script assez trivial qui divisera le fichier en sous-fichiers à ces points. (Les commentaires spéciaux contiennent peut-être des noms de fichiers intégrés que le script peut utiliser comme instructions pour le diviser.) Il doit conserver les commentaires dans le cadre du fractionnement.

  • Exécutez le script. Supprimez le fichier d'origine.

  • Lorsque vous devez fusionner à partir d'une branche, recréez d'abord le gros fichier en concaténant les morceaux ensemble, effectuez la fusion, puis divisez-le à nouveau.

De plus, si vous souhaitez conserver l'historique des fichiers SCC, je m'attends à ce que la meilleure façon de le faire soit d'indiquer à votre système de contrôle de code source que les fichiers pièce individuels sont des copies de l'original. Ensuite, il conservera l'historique des sections qui ont été conservées dans ce fichier, bien qu'il enregistrera également que de grandes parties ont été "supprimées".

Brooks Moses
la source
4

Une façon de le diviser sans trop de danger serait de jeter un regard historique sur tous les changements de ligne. Y a-t-il certaines fonctions plus stables que d'autres? Points chauds de changement si vous voulez.

Si une ligne n'a pas été modifiée depuis quelques années, vous pouvez probablement la déplacer vers un autre fichier sans trop de soucis. Je regarderais la source annotée avec la dernière révision qui a touché une ligne donnée et voir s'il y a des fonctions que vous pourriez retirer.

Paul Rubel
la source
Je pense que d'autres ont proposé des choses similaires. C'est court et pertinent et je pense que cela peut être un point de départ valide pour le problème d'origine.
Martin Ba
3

Wow, ça sonne bien. Je pense qu'expliquer à votre patron que vous avez besoin de beaucoup de temps pour refaçonner la bête vaut la peine d'être essayé. S'il n'est pas d'accord, arrêter est une option.

Quoi qu'il en soit, ce que je suggère est de jeter toute l'implémentation et de la regrouper dans de nouveaux modules, appelons ces "services globaux". Le «module principal» ne transmettrait qu'à ces services et TOUT nouveau code que vous écrivez les utilisera à la place du «module principal». Cela devrait être possible dans un délai raisonnable (car il s'agit principalement de copier-coller), vous ne cassez pas le code existant et vous pouvez le faire une version de maintenance à la fois. Et s'il vous reste encore du temps, vous pouvez le passer à remanier tous les anciens modules dépendants pour utiliser également les services globaux.

back2dos
la source
3

Mes sympathies - dans mon travail précédent, j'ai rencontré une situation similaire avec un fichier qui était plusieurs fois plus grand que celui avec lequel vous devez traiter. La solution était:

  1. Écrivez du code pour tester de manière exhaustive la fonction dans le programme en question. On dirait que vous ne l'avez pas déjà en main ...
  2. Identifiez du code pouvant être extrait dans une classe d'aide / utilitaires. Pas besoin d'être gros, juste quelque chose qui ne fait pas vraiment partie de votre classe «principale».
  3. Refactorisez le code identifié en 2. dans une classe distincte.
  4. Relancez vos tests pour vous assurer que rien ne s'est cassé.
  5. Lorsque vous avez le temps, passez à 2. et répétez au besoin pour rendre le code gérable.

Les classes que vous créez à l'étape 3. les itérations se développeront probablement pour absorber plus de code approprié à leur fonction nouvellement claire.

Je pourrais également ajouter:

0: acheter le livre de Michael Feathers sur l'utilisation du code hérité

Malheureusement, ce type de travail est trop courant, mais mon expérience est qu'il est très utile de pouvoir rendre le code horrible qui fonctionne mais moins horrible tout en le maintenant.

Steve Townsend
la source
2

Envisagez des moyens de réécrire l'intégralité de l'application de manière plus judicieuse. Peut-être en réécrire une petite section comme un prototype pour voir si votre idée est réalisable.

Si vous avez identifié une solution viable, refactorisez l'application en conséquence.

Si toutes les tentatives pour produire une architecture plus rationnelle échouent, alors vous savez au moins que la solution consiste probablement à redéfinir les fonctionnalités du programme.

wallyk
la source
+1 - réécrivez-le à votre rythme, sinon quelqu'un pourrait cracher son mannequin.
Jon Black
2

Mes 0,05 centimes d'euro:

Reconcevoir l'ensemble du désordre, le diviser en sous-systèmes en tenant compte des exigences techniques et commerciales (= de nombreuses pistes de maintenance parallèle avec une base de code potentiellement différente pour chacune, il y a évidemment un besoin de haute modifiabilité, etc.).

Lors de la division en sous-systèmes, analysez les endroits qui ont le plus changé et séparez-les des parties immuables. Cela devrait vous montrer les points chauds. Séparez les parties les plus changeantes de leurs propres modules (par exemple, dll) de manière à ce que l'API du module puisse être conservée intacte et que vous n'ayez pas besoin de casser BC tout le temps. De cette façon, vous pouvez déployer différentes versions du module pour différentes branches de maintenance, si nécessaire, tout en conservant le noyau inchangé.

La refonte devra probablement être un projet distinct, essayer de le faire sur une cible mobile ne fonctionnera pas.

Quant à l'historique du code source, mon avis: oubliez-le pour le nouveau code. Mais gardez l'historique quelque part afin de pouvoir le vérifier, si nécessaire. Je parie que vous n'en aurez pas besoin autant après le début.

Vous devez très probablement obtenir l'adhésion de la direction pour ce projet. Vous pouvez peut-être discuter avec un temps de développement plus rapide, moins de bogues, un entretien plus facile et moins de chaos global. Quelque chose dans le sens de «permettre de manière proactive la pérennité et la viabilité de la maintenance de nos actifs logiciels critiques» :)

C'est ainsi que je commencerais à aborder le problème au moins.

Slinky
la source
2

Commencez par y ajouter des commentaires. En référence à l'endroit où les fonctions sont appelées et si vous pouvez déplacer les choses. Cela peut faire bouger les choses. Vous devez vraiment évaluer le degré de fragilité du code. Ensuite, déplacez les éléments communs de fonctionnalité ensemble. Petits changements à la fois.

Jesper Smith
la source
2

Quelque chose que je trouve utile de faire (et je le fais maintenant mais pas à l'échelle à laquelle vous faites face), est d'extraire des méthodes en tant que classes (refactoring d'objet de méthode). Les méthodes qui diffèrent selon vos différentes versions deviendront des classes différentes qui peuvent être injectées dans une base commune pour fournir le comportement différent dont vous avez besoin.

Channing Walton
la source
2

J'ai trouvé que cette phrase était la partie la plus intéressante de votre message:

> Le fichier est utilisé et activement modifié dans plusieurs (> 10) versions de maintenance de notre produit et il est donc très difficile de le refactoriser

Tout d'abord, je vous recommande d'utiliser un système de contrôle de source pour développer ces versions de maintenance 10+ qui prennent en charge la ramification.

Deuxièmement, je créerais dix succursales (une pour chacune de vos versions de maintenance).

Je peux déjà te sentir grincer des dents! Mais soit votre contrôle de code source ne fonctionne pas pour votre situation en raison d'un manque de fonctionnalités, soit il n'est pas utilisé correctement.

Maintenant, pour la branche sur laquelle vous travaillez - refactorisez-la comme bon vous semble, sachant que vous ne bouleverserez pas les neuf autres branches de votre produit.

Je serais un peu inquiet que vous ayez tant de choses dans votre fonction main ().

Dans tous les projets que j'écris, j'utiliserais main () uniquement pour effectuer l'initialisation des objets de base - comme un objet de simulation ou d'application - ces classes sont là où le vrai travail devrait continuer.

Je voudrais également initialiser un objet de journalisation d'application dans main pour une utilisation globale dans tout le programme.

Enfin, dans le principal, j'ajoute également du code de détection de fuite dans les blocs de préprocesseur qui garantit qu'il n'est activé que dans les versions DEBUG. C'est tout ce que j'ajouterais à main (). Main () devrait être court!

Vous dites que

> Le fichier contient essentiellement la "classe principale" (répartition et coordination des tâches internes principales) de notre programme

Il semble que ces deux tâches puissent être divisées en deux objets distincts - un coordinateur et un répartiteur de travail.

Lorsque vous les divisez, vous pouvez gâcher votre «flux de travail SCC», mais il semble que le respect strict de votre flux de travail SCC entraîne des problèmes de maintenance logicielle. Abandonnez-le maintenant et ne regardez pas en arrière, car dès que vous le réparerez, vous commencerez à dormir facilement.

Si vous n'êtes pas en mesure de prendre la décision, combattez bec et ongles avec votre manager pour cela - votre application doit être refactorisée - et mal par le bruit! Ne prenez pas non pour réponse!

user206705
la source
Si je comprends bien, le problème est le suivant: si vous mordez la balle et refactorisez, vous ne pouvez plus transporter de correctifs entre les versions. SCC pourrait être parfaitement mis en place.
peterchen
@peterchen - exactement le problème. Les SCC fusionnent au niveau du fichier. (Fusion à 3 voies) Si vous déplacez le code entre les fichiers, vous devrez commencer à manipuler manuellement les blocs de code modifiés d'un fichier à un autre. (La fonctionnalité GIT que quelqu'un d'autre a mentionnée dans un autre commentaire est juste bonne pour l'histoire, pas pour la fusion pour autant que je sache)
Martin Ba
2

Comme vous l'avez décrit, le problème principal est différent entre le pré-partage et le post-partage, la fusion des corrections de bugs, etc. Il ne faudra pas si longtemps pour coder en dur un script en Perl, Ruby, etc. pour extraire la plupart du bruit provenant de la pré-division différente contre une concaténation de la post-division. Faites ce qui est le plus simple en termes de gestion du bruit:

  • supprimer certaines lignes avant / pendant la concaténation (par exemple, inclure des gardes)
  • supprimer d'autres éléments de la sortie diff si nécessaire

Vous pouvez même faire en sorte que chaque fois qu'il y a un archivage, la concaténation s'exécute et vous avez quelque chose de prêt à faire la différence avec les versions à fichier unique.

Tony D
la source
2
  1. Ne touchez plus jamais ce fichier et le code!
  2. Le traitement est comme quelque chose avec lequel vous êtes coincé. Commencez à écrire des adaptateurs pour les fonctionnalités qui y sont encodées.
  3. Écrivez un nouveau code dans différentes unités et ne parlez qu'aux adaptateurs qui encapsulent les fonctionnalités du monstre.
  4. ... si un seul des éléments ci-dessus n'est pas possible, quittez le travail et obtenez-en un nouveau.
paul_71
la source
2
+/- 0 - sérieusement, où vivez-vous les gens que vous recommanderiez de quitter un emploi sur la base d'un détail technique comme celui-ci?
Martin Ba
1

"Le fichier contient essentiellement la" classe principale "(répartition et coordination du travail interne principal) de notre programme, donc chaque fois qu'une fonctionnalité est ajoutée, elle affecte également ce fichier et chaque fois qu'elle se développe."

Si ce gros SWITCH (que je pense qu'il existe) devient le principal problème de maintenance, vous pouvez le refactoriser pour utiliser le dictionnaire et le modèle de commande et supprimer toute la logique de commutation du code existant vers le chargeur, qui remplit cette carte, c'est-à-dire:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();
Grozz
la source
2
Non, il n'y a pas vraiment de gros interrupteur. La phrase est juste la plus proche que je puisse en venir pour décrire ce gâchis :)
Martin Ba
1

Je pense que la façon la plus simple de suivre l'historique des sources lors du fractionnement d'un fichier serait quelque chose comme ceci:

  1. Faites des copies du code source d'origine en utilisant les commandes de copie préservant l'historique fournies par votre système SCM. Vous devrez probablement soumettre à ce stade, mais il n'est pas encore nécessaire de parler à votre système de construction des nouveaux fichiers, donc ça devrait aller.
  2. Supprimez le code de ces copies. Cela ne devrait pas briser l'historique des lignes que vous conservez.
Christopher Creutzig
la source
"en utilisant les commandes de copie préservant l'historique fournies par votre système SCM" ... mauvaise chose, il n'en fournit aucune
Martin Ba
Dommage. Cela seul semble être une bonne raison de passer à quelque chose de plus moderne. :-)
Christopher Creutzig
1

Je pense que ce que je ferais dans cette situation est un peu la balle et:

  1. Découvrez comment je voulais diviser le fichier (basé sur la version de développement actuelle)
  2. Mettez un verrou administratif sur le fichier ("Personne ne touche mainmodule.cpp après 17h vendredi !!!"
  3. Passez votre long week-end à appliquer ce changement aux versions de maintenance> 10 (de la plus ancienne à la plus récente), jusqu'à la version actuelle incluse.
  4. Supprimez mainmodule.cpp de toutes les versions prises en charge du logiciel. C'est un nouvel âge - il n'y a plus de mainmodule.cpp.
  5. Convaincre la direction que vous ne devez pas prendre en charge plus d'une version de maintenance du logiciel (au moins sans un gros contrat de support $$$). Si chacun de vos clients a sa propre version unique .... yeeeeeshhhh. J'ajouterais des directives de compilation plutôt que d'essayer de maintenir 10+ fourches.

Le suivi des anciennes modifications apportées au fichier est simplement résolu par votre premier commentaire d'enregistrement disant quelque chose comme "split from mainmodule.cpp". Si vous devez revenir à quelque chose de récent, la plupart des gens se souviendront du changement, si c'est dans 2 ans, le commentaire leur dira où chercher. Bien sûr, quelle sera la valeur de remonter plus de 2 ans pour voir qui a changé le code et pourquoi?

BIBD
la source