Cosa fare con un file sorgente C ++ a 11000 righe?


229

Quindi abbiamo questo enorme file sorgente mainmodule.cpp (11000 righe?) Nel nostro progetto e ogni volta che devo toccarlo mi faccio rabbrividire.

Poiché questo file è così centrale e grande, continua ad accumulare sempre più codice e non riesco a pensare a un buon modo per farlo effettivamente iniziare a ridursi.

Il file viene utilizzato e modificato attivamente in diverse (> 10) versioni di manutenzione del nostro prodotto, quindi è davvero difficile riformattarlo. Se dovessi "semplicemente" suddividerlo, diciamo per cominciare, in 3 file, quindi fondere le modifiche dalle versioni di manutenzione diventerebbe un incubo. E anche se dividi un file con una storia così lunga e ricca, rintracciare e controllare le vecchie modifiche nella SCCstoria diventa improvvisamente molto più difficile.

Il file contiene fondamentalmente la "classe principale" (invio e coordinamento del lavoro interno principale) del nostro programma, quindi ogni volta che viene aggiunta una funzione, influisce anche su questo file e ogni volta che cresce. :-(

Cosa faresti in questa situazione? Qualche idea su come spostare nuove funzionalità in un file sorgente separato senza creare confusione nel SCCflusso di lavoro?

(Nota sugli strumenti: usiamo il C ++ con Visual Studio; Usiamo AccuRevcome SCCma penso che il tipo di SCCnon abbia molta importanza qui; Usiamo Araxis Mergeper fare un confronto e una fusione effettivi dei file)


15
@BoltClock: In realtà Vim lo aprirà abbastanza velocemente.
Il

58
69305 linee e conteggio. Un file nella nostra applicazione in cui il mio collega scarica la maggior parte del suo codice. Non ho potuto resistere a pubblicarlo qui. Non ho nessuno nella mia azienda a cui segnalare questo.
Agnel Kurian,

204
Non capisco In che modo il commento "abbandona quel lavoro" può ottenere così tanti voti positivi? Alcune persone sembrano vivere in un paese delle fate, dove tutti i progetti sono scritti da zero e / o usano il 100% agile, TDD, ... (inserisci qui le tue parole d'ordine).
Stefan,

39
@Stefan: Di fronte a una base di codice simile ho fatto esattamente questo. Non mi andava di passare il 95% del mio tempo a lavorare intorno al greggio in una base di codice di 10 anni e il 5% in realtà a scrivere codice. In realtà era impossibile testare alcuni aspetti del sistema (e non intendo test unitari, intendo effettivamente eseguire il codice per vedere se ha funzionato). Non sono durato il mio periodo di prova di 6 mesi, mi sono stancato di combattere per perdere battaglie e scrivere codice che non potevo sopportare.
Binary Worrier,

50
per quanto riguarda l'aspetto del tracciamento della cronologia della divisione del file: utilizzare il comando copia del sistema di controllo della versione per copiare l'intero file per quante volte si desidera dividerlo, quindi rimuovere tutto il codice da ciascuna delle copie che non si desidera in quel file. Ciò preserva la cronologia generale, poiché ognuno dei file divisi può risalire alla sua cronologia attraverso la suddivisione (che sembrerà una gigantesca cancellazione della maggior parte del contenuto del file).
rmeador,

Risposte:


86
  1. Trova del codice nel file che è relativamente stabile (non cambia rapidamente e non varia molto tra i rami) e potrebbe essere considerato un'unità indipendente. Spostalo nel suo file e, per quello, nella sua classe, in tutti i rami. Poiché è stabile, ciò non causerà (molti) fusioni "scomode" che devono essere applicate a un file diverso da quello su cui erano state originariamente fatte, quando si uniscono le modifiche da un ramo all'altro. Ripetere.

  2. Trova un po 'di codice nel file che sostanzialmente si applica solo a un piccolo numero di rami e potrebbe essere autonomo. Non importa se sta cambiando velocemente o no, a causa del piccolo numero di rami. Spostalo nelle sue classi e file. Ripetere.

Quindi, ci siamo sbarazzati del codice che è lo stesso ovunque, e del codice specifico per determinati rami.

Questo ti lascia con un nucleo di codice mal gestito: è necessario ovunque, ma è diverso in ogni ramo (e / o cambia costantemente in modo che alcuni rami siano in esecuzione dietro ad altri), eppure è in un singolo file che sei tentando senza successo di fondersi tra i rami. Smettila. Ramifica il file in modo permanente , magari rinominandolo in ciascun ramo. Non è più "principale", è "principale per la configurazione X". OK, quindi perdi la possibilità di applicare la stessa modifica a più rami unendo, ma questo è comunque il nucleo del codice in cui la fusione non funziona molto bene. Se devi comunque gestire manualmente le fusioni per gestire i conflitti, non è una perdita applicarle manualmente in modo indipendente su ciascun ramo.

Penso che ti sbagli nel dire che il tipo di SCC non ha importanza, perché ad esempio le abilità di fusione di git sono probabilmente migliori dello strumento di unione che stai usando. Quindi il problema principale, "la fusione è difficile" si verifica in momenti diversi per SCC diversi. Tuttavia, è improbabile che tu sia in grado di cambiare SCC, quindi il problema è probabilmente irrilevante.


Per quanto riguarda la fusione: ho guardato GIT e ho guardato SVN e ho guardato Perforce e lascia che ti dica che nulla di ciò che ho visto battere AccuRev + Araxis per quello che facciamo. :-) (Anche se GIT può fare questo [ stackoverflow.com/questions/1728922/... ] e AccuRev non può - ognuno deve decidere per se stesso se questo è parte di fusione o di analisi della storia.)
Martin Ba

Abbastanza giusto - forse hai già lo strumento migliore disponibile. La capacità di Git di unire una modifica avvenuta nel File A sul ramo X, nel File B sul ramo Y, dovrebbe facilitare la suddivisione dei file ramificati, ma presumibilmente il sistema che usi presenta dei vantaggi che ti piacciono. Comunque, non sto proponendo di passare a git, sto solo dicendo che SCC fa la differenza qui, ma anche così sono d'accordo con te sul fatto che questo può essere scontato :-)
Steve Jessop

129

La fusione non sarà un incubo così grande come lo sarà quando in futuro otterrai 30000 file LOC. Così:

  1. Smetti di aggiungere altro codice a quel file.
  2. Dividilo.

Se non si può semplicemente smettere di codifica durante il processo di refactoring, si potrebbe lasciare questa grande file come è per un po 'almeno senza l'aggiunta più il codice a esso: poiché contiene una "classe principale" si potrebbe ereditare da essa e mantenere classe ereditata ( es) con funzioni sovraccaricate in diversi nuovi file piccoli e ben progettati.


@Martin: fortunatamente non hai incollato il tuo file qui, quindi non ho idea della sua struttura. Ma l'idea generale è di dividerlo in parti logiche. Tali parti logiche potrebbero contenere gruppi di funzioni della tua "classe principale" o potresti dividerlo in più classi ausiliarie.
Kirill V. Lyadvinsky,

3
Con 10 versioni di manutenzione e molti sviluppatori attivi, è improbabile che il file possa essere bloccato per un tempo abbastanza lungo.
Kobi,

9
@Martin, hai un paio di modelli GOF che farebbero il trucco, una singola facciata che mappa le funzioni di mainmodule.cpp, in alternativa (ti ho consigliato di seguito) creare una suite di classi Command che ciascuna mappa su una funzione / caratteristica di mainmodule.app. (Mi sono ampliato su questo sulla mia risposta.)
Ocodo

2
Sì, sono assolutamente d'accordo, a un certo punto devi smettere di aggiungere codice o alla fine sarà 30k, 40k, 50k, il modulo principale di Kaboom è appena stato danneggiato. :-)
Chris

67

Mi sembra che stai affrontando una serie di odori di codice qui. Innanzitutto la classe principale sembra violare il principio aperto / chiuso . Sembra anche che stia gestendo troppe responsabilità . A causa di ciò, suppongo che il codice sia più fragile di quanto debba essere.

Sebbene possa comprendere le tue preoccupazioni in merito alla tracciabilità a seguito di un refactoring, mi aspetto che questa classe sia piuttosto difficile da mantenere e migliorare e che eventuali modifiche apportate possano causare effetti collaterali. Suppongo che il costo di questi superi il costo del refactoring della classe.

In ogni caso, poiché gli odori del codice peggioreranno solo con il tempo, almeno a un certo punto il loro costo supererà il costo del refactoring. Dalla tua descrizione presumo che tu abbia superato il punto critico.

Il refactoring dovrebbe essere fatto a piccoli passi. Se possibile, aggiungere test automatici per verificare il comportamento corrente prima di eseguire il refactoring di qualsiasi cosa. Quindi selezionare piccole aree di funzionalità isolata ed estrarre questi come tipi al fine di delegare la responsabilità.

In ogni caso, sembra un grande progetto, quindi buona fortuna :)


18
Odora molto: sa che l'anti-pattern Blob è in casa ... en.wikipedia.org/wiki/God_object . Il suo pasto preferito è il codice spaghetti: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan,

@jdehaan: stavo cercando di essere diplomatico a riguardo :)
Brian Rasmussen,

+1 Anche da me, non oso toccare nemmeno il codice complesso che ho scritto senza prove per coprirlo.
Danny Thomas,

49

L'unica soluzione che abbia mai immaginato a tali problemi è la seguente. Il guadagno effettivo con il metodo descritto è la progressività delle evoluzioni. Nessuna rivoluzione qui, altrimenti sarai nei guai molto velocemente.

Inserisci una nuova classe cpp sopra la classe principale originale. Per ora, sostanzialmente reindirizzerebbe tutte le chiamate all'attuale classe principale, ma mirava a rendere l'API di questa nuova classe il più chiara e concisa possibile.

Fatto ciò, avrai la possibilità di aggiungere nuove funzionalità in nuove classi.

Per quanto riguarda le funzionalità esistenti, è necessario spostarle progressivamente in nuove classi man mano che diventano abbastanza stabili. Perderai l'aiuto di SCC per questo pezzo di codice, ma non c'è molto da fare al riguardo. Scegli il momento giusto.

So che questo non è perfetto, anche se spero che possa essere d'aiuto, e il processo deve essere adattato alle tue esigenze!

Informazioni aggiuntive

Nota che Git è un SCC che può seguire pezzi di codice da un file all'altro. Ne ho sentito parlare bene, quindi potrebbe esserti d'aiuto mentre stai progressivamente spostando il tuo lavoro.

Git è costruito attorno al concetto di BLOB che, se ho capito bene, rappresentano pezzi di file di codice. Sposta questi pezzi in diversi file e Git li troverà, anche se li modifichi. A parte il video di Linus Torvalds menzionato nei commenti qui sotto, non sono stato in grado di trovare qualcosa di chiaro su questo.


Un riferimento su come GIT lo fa / come lo fai con GIT sarebbe il benvenuto.
Martin Ba

@Martin Git lo fa automaticamente.
Matteo

4
@Martin: Git lo fa automaticamente - perché non tiene traccia dei file, tiene traccia dei contenuti. In realtà è più difficile in git semplicemente "ottenere la cronologia di un file".
Arafangion,

1
@Martin youtube.com/watch?v=4XpnKHJAok8 è un discorso in cui Torvalds parla di git. Lo menziona più avanti nel discorso.
Matteo

6
@ Martin, un'occhiata a questa domanda: stackoverflow.com/questions/1728922/...
Benjol

30

Confucio dice: "il primo passo per uscire dal buco è smettere di scavare buca".


25

Fammi indovinare: dieci clienti con set di funzionalità divergenti e un responsabile delle vendite che promuove la "personalizzazione"? Ho lavorato su prodotti del genere prima. Abbiamo avuto essenzialmente lo stesso problema.

Riconosci che avere un file enorme è un problema, ma ancora più problemi sono dieci versioni che devi mantenere "attuali". Questa è una manutenzione multipla. SCC può renderlo più semplice, ma non può farlo nel modo giusto.

Prima di provare a suddividere il file in parti, è necessario riportare i dieci rami in sincronia tra loro in modo da poter vedere e modellare tutto il codice in una volta. È possibile eseguire questo ramo uno alla volta, testando entrambi i rami sullo stesso file di codice principale. Per imporre il comportamento personalizzato, puoi usare #ifdef e amici, ma è meglio usare il normale if / else contro costanti definite. In questo modo, il compilatore verificherà tutti i tipi e molto probabilmente eliminerà il codice oggetto "morto". (Tuttavia, potresti voler disattivare l'avviso sul codice morto.)

Una volta che c'è una sola versione di quel file condivisa implicitamente da tutti i rami, allora è piuttosto più semplice iniziare i tradizionali metodi di refactoring.

I #ifdefs sono principalmente migliori per le sezioni in cui il codice interessato ha senso solo nel contesto di altre personalizzazioni per ramo. Si potrebbe obiettare che anche questi presentano un'opportunità per lo stesso schema di fusione dei rami, ma non vanno allo scoperto. Un progetto colossale alla volta, per favore.

A breve termine, il file sembrerà crescere. Questo va bene. Quello che stai facendo è riunire le cose che devono stare insieme. Successivamente, inizierai a vedere aree chiaramente identiche indipendentemente dalla versione; questi possono essere lasciati soli o refactored a volontà. Altre aree differiranno chiaramente a seconda della versione. Hai un numero di opzioni in questo caso. Un metodo consiste nel delegare le differenze agli oggetti strategia per versione. Un altro è derivare le versioni client da una classe astratta comune. Ma nessuna di queste trasformazioni è possibile finché si hanno dieci "suggerimenti" di sviluppo in diversi settori.


2
Sono d'accordo che l'obiettivo dovrebbe essere quello di avere una versione del software, ma non sarebbe meglio usare i file di configurazione (runtime) e non compilare la personalizzazione del tempo
Esben Skov Pedersen

O anche "classi di configurazione" per la build di ogni cliente.
tc.

Immagino che la configurazione in fase di compilazione o di runtime sia funzionalmente irrilevante, ma non voglio limitare le possibilità. La configurazione in fase di compilazione ha il vantaggio che il client non può hackerare con un file di configurazione per attivare funzionalità aggiuntive, poiché inserisce tutte le configurazioni nella struttura di origine anziché come codice "oggetto testuale" distribuibile. Il rovescio della medaglia è che tendi verso AlternateHardAndSoftLayers se è runtime.
Ian,

22

Non so se questo risolva il tuo problema, ma immagino che tu voglia fare è migrare il contenuto del file in file più piccoli indipendenti l'uno dall'altro (riassunto). Quello che ho anche è che hai circa 10 diverse versioni del software fluttuante e devi supportarle tutte senza fare confusione.

Prima di tutto non c'è modo che questo sia facile e si risolva da solo in pochi minuti di brainstorming. Le funzioni collegate al tuo file sono tutte fondamentali per la tua applicazione, e semplicemente tagliandole e migrandole su altri file non salverai il tuo problema.

Penso che tu abbia solo queste opzioni:

  1. Non migrare e rimanere con quello che hai. Forse lascia il tuo lavoro e inizia a lavorare su un software serio con un buon design in aggiunta. La programmazione estrema non è sempre la soluzione migliore se stai lavorando a un progetto a lungo con fondi sufficienti per sopravvivere a un incidente o due.

  2. Elabora un layout di come vorresti che il tuo file apparisse una volta diviso. Crea i file necessari e integrali nella tua applicazione. Rinominare le funzioni o sovraccaricarle per prendere un parametro aggiuntivo (forse solo un semplice booleano?). Una volta che devi lavorare sul tuo codice, migra le funzioni di cui hai bisogno per lavorare sul nuovo file e associa le chiamate di funzione delle vecchie funzioni alle nuove funzioni. Dovresti comunque avere il tuo file principale in questo modo ed essere ancora in grado di vedere le modifiche che sono state apportate ad esso, una volta che si tratta di una funzione specifica che sai esattamente quando è stato esternalizzato e così via.

  3. Cerca di convincere i tuoi colleghi con una buona torta che il flusso di lavoro è sopravvalutato e che devi riscrivere alcune parti dell'applicazione per fare affari seri.


19

Esattamente questo problema è affrontato in uno dei capitoli del libro "Lavorare in modo efficace con il codice legacy" ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).


informit.com/store/product.aspx?isbn=0131177052 consente di vedere il sommario di questo libro (e 2 capitoli di esempio). Quanto dura il capitolo 20? (Solo per avere un'idea di quanto possa essere utile.)
Martin Ba

17
il capitolo 20 è lungo 10.000 righe, ma l'autore sta studiando come dividerlo in pezzi digeribili ... 8)
Tony Delroy,

1
Sono circa 23 pagine, ma con 14 immagini. Penso che dovresti prenderlo, ti sentirai molto più sicuro cercando di decidere cosa fare imho.
Emile Vrijdags,

Un libro eccellente per il problema, ma i consigli che fa (e altri consigli in questo thread) condividono tutti un requisito comune: se si desidera riformattare questo file per tutti i rami, l'unico modo per farlo è bloccare il file per tutti i rami e apportare le modifiche strutturali iniziali. Non c'è modo di aggirare questo. Il libro delinea un approccio iterativo per l'estrazione di sottoclassi in modo sicuro senza supporto di refactoring automatico, creando metodi duplicati e delegando le chiamate, ma tutto ciò è discutibile se non è possibile modificare i file.
Dan Bryant,

2
@Martin, il libro è eccellente ma si basa abbastanza pesantemente sul test, sul refactor, sul ciclo di test che potrebbe essere piuttosto difficile da dove ti trovi ora. Sono stato in una situazione simile e questo libro è stato il più utile che ho trovato. Ha buoni suggerimenti per il brutto problema che hai. Ma se non riesci a ottenere un cablaggio di prova di qualche tipo nell'immagine, tutti i suggerimenti di refactoring nel mondo non ti aiuteranno.

14

Penso che sarebbe meglio creare un set di classi di comandi che si associno ai punti API di mainmodule.cpp.

Una volta che sono a posto, dovrai rifattorizzare la base di codice esistente per accedere a questi punti API tramite le classi di comando, una volta fatto, sei libero di refactificare l'implementazione di ciascun comando in una nuova struttura di classe.

Ovviamente, con una singola classe di 11 KLOC il codice è probabilmente molto accoppiato e fragile, ma la creazione di classi di comando individuali aiuterà molto più di qualsiasi altra strategia proxy / facciata.

Non invidio il compito, ma col passare del tempo questo problema peggiorerà solo se non viene affrontato.

Aggiornare

Suggerirei che il modello di comando sia preferibile a una facciata.

È preferibile mantenere / organizzare molte diverse classi di comando su una facciata (relativamente) monolitica. La mappatura di una singola facciata su un file 11 KLOC dovrà probabilmente essere suddivisa in alcuni gruppi diversi.

Perché preoccuparsi di cercare di capire questi gruppi di facciate? Con il modello di comando sarai in grado di raggruppare e organizzare organicamente queste piccole classi, in modo da avere molta più flessibilità.

Naturalmente, entrambe le opzioni sono migliori del singolo 11 KLOC e del file in crescita.


+1 un'alternativa alla soluzione che ho proposto, con la stessa idea: cambiare l'API per separare il problema più grande in quelli piccoli.
Benoît,

13

Un consiglio importante: non mescolare refactoring e correzioni di bug. Quello che vuoi è una versione del tuo programma identica alla versione precedente, tranne per il fatto che il codice sorgente è diverso.

Un modo potrebbe essere quello di iniziare a suddividere la funzione / parte meno grande nel proprio file e quindi includerli con un'intestazione (trasformando così main.cpp in un elenco di #includes, che suona un odore di codice in sé * Non lo sono un Guru C ++), ma almeno ora è diviso in file).

Potresti quindi provare a passare tutte le versioni di manutenzione al "nuovo" main.cpp o qualunque sia la tua struttura. Di nuovo: nessun altro cambiamento o correzione di errori perché il monitoraggio di questi è confuso da morire.

Un'altra cosa: per quanto tu possa desiderare di fare un grande passaggio nel refactoring del tutto in una volta, potresti mordere più di quanto puoi masticare. Forse basta scegliere una o due "parti", inserirle in tutte le versioni, quindi aggiungere un po 'più di valore per il cliente (dopo tutto, il refactoring non aggiunge un valore diretto, quindi è un costo che deve essere giustificato) e quindi sceglierne un altro una o due parti.

Ovviamente ciò richiede una certa disciplina nel team per utilizzare effettivamente i file divisi e non solo per aggiungere nuove cose a main.cpp per tutto il tempo, ma, ancora una volta, provare a fare un grosso refattore potrebbe non essere il miglior modo di agire.


1
+1 per il factoring e #include di nuovo. Se lo facessi per tutti e 10 i rami (un po 'di lavoro lì, ma gestibile) avresti ancora l'altro problema, quello di pubblicare le modifiche in tutti i tuoi rami, ma quel problema non' si sono espansi (necessariamente). È brutto? Sì, lo è ancora, ma potrebbe portare un po 'di razionalità al problema. Dopo aver trascorso diversi anni a fare manutenzione e assistenza per un prodotto davvero grande, so che la manutenzione comporta molto dolore. Per lo meno, impara da esso e funge da ammonimento per gli altri.
Jay,

10

Rofl, questo mi ricorda il mio vecchio lavoro. Sembra che, prima di unirmi, tutto fosse contenuto in un unico enorme file (anche C ++). Poi l'hanno suddiviso (in punti completamente casuali usando include) in circa tre (file ancora enormi). La qualità di questo software è stata, come prevedibile, orribile. Il progetto è ammontato a circa 40k LOC. (contenente quasi nessun commento ma MOLTO codice duplicato)

Alla fine ho fatto una completa riscrittura del progetto. Ho iniziato rifacendo da zero la parte peggiore del progetto. Ovviamente avevo in mente una possibile (piccola) interfaccia tra questa nuova parte e il resto. Quindi ho inserito questa parte nel vecchio progetto. Non ho riformattato il vecchio codice per creare l'interfaccia necessaria, ma l'ho sostituito. Poi ho fatto piccoli passi da lì, riscrivendo il vecchio codice.

Devo dire che ciò ha richiesto circa un anno e mezzo e che a quel tempo non vi era sviluppo della vecchia base di codici accanto alle correzioni dei bug.


modificare:

Le dimensioni sono rimaste a circa 40k LOC ma la nuova applicazione conteneva molte più funzionalità e presumibilmente meno bug nella sua versione iniziale rispetto al software di 8 anni. Uno dei motivi della riscrittura era anche che avevamo bisogno delle nuove funzionalità e introdurle nel vecchio codice era quasi impossibile.

Il software era per un sistema incorporato, una stampante di etichette.

Un altro punto che dovrei aggiungere è che in teoria il progetto era C ++. Ma non era affatto OO, avrebbe potuto essere C. La nuova versione era orientata agli oggetti.


9
Ogni volta che sento "da zero" nell'argomento del refactoring, uccido un gattino!
Kugel,

Sono stato in una situazione di suono molto simile, anche se il ciclo principale del programma che ho dovuto affrontare era solo ~ 9000 LOC. E questo è stato abbastanza male.
AndyUK,

8

OK, quindi per la maggior parte riscrivere l'API del codice di produzione è una cattiva idea come inizio. Devono succedere due cose.

Uno, devi effettivamente far decidere al tuo team di fare un blocco del codice sull'attuale versione di produzione di questo file.

Due, è necessario prendere questa versione di produzione e creare un ramo che gestisca le build utilizzando le direttive di preelaborazione per suddividere il file di grandi dimensioni. Dividere la compilazione usando le direttive del preprocessore JUST (#ifdefs, #includes, #endifs) è più facile che ricodificare l'API. È decisamente più semplice per gli SLA e il supporto continuo.

Qui puoi semplicemente tagliare le funzioni relative a un particolare sottosistema all'interno della classe e metterle in un file dire mainloop_foostuff.cpp e includerlo in mainloop.cpp nella posizione corretta.

O

Un modo più lungo ma robusto sarebbe quello di escogitare una struttura di dipendenze interne con doppia indiretta nel modo in cui le cose vengono incluse. Ciò ti consentirà di dividere le cose e di occuparti ancora delle dipendenze. Si noti che questo approccio richiede la codifica posizionale e pertanto deve essere associato a commenti appropriati.

Questo approccio include componenti che vengono utilizzati in base alla variante che si sta compilando.

La struttura di base è che mainclass.cpp includerà un nuovo file chiamato MainClassComponents.cpp dopo un blocco di istruzioni come il seguente:

#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 struttura principale del file MainClassComponents.cpp sarebbe lì per elaborare le dipendenze all'interno dei componenti secondari come questo:

#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  */

E ora per ogni componente crei un file component_xx.cpp.

Ovviamente sto usando i numeri ma dovresti usare qualcosa di più logico in base al tuo codice.

L'uso del preprocessore consente di dividere le cose senza doversi preoccupare delle modifiche alle API, il che è un incubo nella produzione.

Una volta stabilita la produzione, puoi effettivamente lavorare sulla riprogettazione.


Sembrano i risultati dell'esperienza, ma inizialmente dolorosi.
JBR Wilkinson,

In realtà, è una tecnica utilizzata nei compilatori C ++ di Borland per emulare gli usi in stile Pascal per la gestione dei file di intestazione. Soprattutto quando hanno fatto la porta iniziale del loro sistema Windowing basato su testo.
Re degli Elfi,

8

Bene, capisco il tuo dolore :) Ho partecipato anche ad alcuni di questi progetti e non è carino. Non c'è una risposta facile per questo.

Un approccio che potrebbe funzionare per te è quello di iniziare ad aggiungere protezioni sicure in tutte le funzioni, ovvero controllare gli argomenti, pre / post-condizioni nei metodi, quindi aggiungere infine unit test per catturare l'attuale funzionalità delle fonti. Una volta che hai questo sei meglio equipaggiato per ricodificare il codice perché avrai affermazioni ed errori spuntati che ti avviseranno se hai dimenticato qualcosa.

A volte anche se ci sono momenti in cui il refactoring può portare più dolore che beneficio. Quindi potrebbe essere meglio lasciare il progetto originale e in uno stato di pseudo manutenzione e ricominciare da zero e quindi aggiungere in modo incrementale la funzionalità dall'animale.


4

Non dovresti preoccuparti di ridurre le dimensioni del file, ma piuttosto di ridurre le dimensioni della classe. Si riduce quasi allo stesso modo, ma ti fa guardare il problema da una prospettiva diversa (come suggerisce @Brian Rasmussen , la tua classe sembra avere molte responsabilità).


Come sempre, vorrei ottenere una spiegazione per il downvote.
Björn Pollex,

4

Quello che hai è un classico esempio di un modello di design noto chiamato blob . Prenditi del tempo per leggere l'articolo che indico qui, e forse potresti trovare qualcosa di utile. Inoltre, se questo progetto è grande come sembra, dovresti prendere in considerazione alcuni progetti per impedire la crescita in codice che non puoi controllare.


4

Questa non è una risposta al grosso problema, ma una soluzione teorica a un suo specifico pezzo:

  • Scopri dove vuoi dividere il file di grandi dimensioni in file secondari. Inserisci commenti in un formato speciale in ciascuno di questi punti.

  • Scrivi uno script abbastanza banale che suddividerà il file in sottofile in quei punti. (Forse i commenti speciali hanno nomi di file incorporati che lo script può usare come istruzioni su come dividerlo.) Dovrebbe preservare i commenti come parte della divisione.

  • Esegui lo script. Elimina il file originale.

  • Quando è necessario unire da un ramo, ricreare prima il file di grandi dimensioni concatenando nuovamente i pezzi, eseguendo l'unione e quindi suddividendoli nuovamente.

Inoltre, se si desidera preservare la cronologia dei file SCC, mi aspetto che il modo migliore per farlo sia quello di dire al sistema di controllo del codice sorgente che i singoli file pezzo sono copie dell'originale. Quindi conserverà la cronologia delle sezioni che erano conservate in quel file, anche se ovviamente registrerà che le parti di grandi dimensioni sono state "cancellate".


4

Un modo per dividerlo senza troppi pericoli sarebbe quello di dare uno sguardo storico a tutti i cambi di linea. Ci sono alcune funzioni più stabili di altre? Punti caldi di cambiamento se vuoi.

Se una linea non è stata cambiata da alcuni anni, probabilmente puoi spostarla in un altro file senza troppe preoccupazioni. Darei un'occhiata alla fonte annotata con l'ultima revisione che ha toccato una determinata riga e vedere se ci sono funzioni che potresti estrarre.


Penso che altri abbiano proposto cose simili. Questo è breve e al punto e penso che questo possa essere un valido punto di partenza per il problema originale.
Martin Ba,

3

Wow, suona alla grande. Penso che spiegare al tuo capo che valga la pena provare molto tempo per rifattare la bestia. Se non è d'accordo, smettere è un'opzione.

Ad ogni modo, ciò che suggerisco è fondamentalmente buttare via tutta l'implementazione e raggrupparla in nuovi moduli, chiamiamo questi "servizi globali". Il "modulo principale" inoltrerebbe solo a tali servizi e QUALSIASI nuovo codice che scrivi li utilizzerà al posto del "modulo principale". Questo dovrebbe essere fattibile in un ragionevole lasso di tempo (perché è principalmente copia e incolla), non rompi il codice esistente e puoi farlo una versione di manutenzione alla volta. E se hai ancora tempo, puoi spenderlo per il refactoring di tutti i vecchi moduli dipendenti per utilizzare anche i servizi globali.


3

Le mie simpatie: nel mio lavoro precedente ho riscontrato una situazione simile con un file che era più volte più grande di quello che devi affrontare. La soluzione era:

  1. Scrivi il codice per testare in modo esaustivo la funzione nel programma in questione. Sembra che non lo avrai già in mano ...
  2. Identifica del codice che può essere estratto in una classe helper / utilities. Non è necessario essere grandi, solo qualcosa che non fa veramente parte della tua classe "principale".
  3. Rifattorizzare il codice identificato in 2. in una classe separata.
  4. Rieseguire i test per assicurarsi che nulla si sia rotto.
  5. Quando hai tempo, vai a 2. e ripeti quanto basta per rendere gestibile il codice.

Le classi create nel passaggio 3. probabilmente aumenteranno le iterazioni per assorbire più codice appropriato per la loro nuova funzione.

Potrei anche aggiungere:

0: acquista il libro di Michael Feathers su come lavorare con il codice legacy

Sfortunatamente questo tipo di lavoro è fin troppo comune, ma la mia esperienza è che c'è un grande valore nel riuscire a rendere il lavoro ma il codice orrendo incrementalmente meno orrendo mentre lo fa funzionare.


2

Considera i modi per riscrivere l'intera applicazione in modo più sensato. Magari riscrivine una piccola parte come prototipo per vedere se la tua idea è fattibile.

Se hai identificato una soluzione praticabile, refactoring l'applicazione di conseguenza.

Se tutti i tentativi di produrre un'architettura più razionale falliscono, almeno sai che la soluzione sta probabilmente nel ridefinire la funzionalità del programma.


+1: riscrivilo a tuo tempo, anche se altrimenti qualcuno potrebbe sputare il proprio manichino.
Jon Black,

2

I miei 0,05 centesimi di euro:

Riprogettare l'intero disordine, dividerlo in sottosistemi tenendo conto dei requisiti tecnici e di business (= molte tracce di manutenzione parallele con base di codice potenzialmente diversa per ciascuna, è ovviamente necessaria un'elevata modificabilità, ecc.).

Quando si divide in sottosistemi, analizzare i luoghi che sono maggiormente cambiati e separarli da parti immutabili. Questo dovrebbe mostrarti i punti problematici. Separare le parti più mutevoli nei propri moduli (ad es. Dll) in modo tale che l'API del modulo possa essere mantenuta intatta e non sia necessario interrompere continuamente BC. In questo modo è possibile distribuire versioni diverse del modulo per diversi rami di manutenzione, se necessario, mantenendo invariato il core.

La riprogettazione dovrà probabilmente essere un progetto separato, il tentativo di farlo su un obiettivo mobile non funzionerà.

Per quanto riguarda la storia del codice sorgente, la mia opinione: dimenticala per il nuovo codice. Ma conserva la cronologia da qualche parte in modo da poterla controllare, se necessario. Scommetto che non ti servirà così tanto dopo l'inizio.

Molto probabilmente è necessario ottenere un buy-in di gestione per questo progetto. Puoi discutere con tempi di sviluppo più rapidi, meno bug, manutenzione più semplice e meno caos complessivo. Qualcosa sulla falsariga di "Abilitare in modo proattivo la sostenibilità futura e la fattibilità della manutenzione dei nostri asset software critici" :)

È così che inizierei ad affrontare il problema almeno.


2

Inizia aggiungendo commenti ad esso. Con riferimento a dove vengono chiamate le funzioni e se è possibile spostare le cose. Questo può far muovere le cose. Devi davvero valutare quanto sia fragile il codice base. Quindi spostare insieme bit comuni di funzionalità. Piccoli cambiamenti alla volta.



2

Qualcosa che trovo utile da fare (e lo sto facendo ora anche se non nella scala che affronti), è estrarre metodi come classi (refactoring dell'oggetto metodo). I metodi che differiscono tra le diverse versioni diventeranno classi diverse che possono essere iniettate in una base comune per fornire il diverso comportamento necessario.


2

Ho trovato questa frase la parte più interessante del tuo post:

> Il file viene utilizzato e modificato attivamente in diverse (> 10) versioni di manutenzione del nostro prodotto, quindi è davvero difficile riformattarlo

Innanzitutto, consiglierei di utilizzare un sistema di controllo del codice sorgente per lo sviluppo di queste versioni di manutenzione 10+ che supportano la ramificazione.

In secondo luogo, creerei dieci rami (uno per ciascuna delle versioni di manutenzione).

Sento che ti senti già in difficoltà! Ma il controllo del codice sorgente non funziona per la tua situazione a causa della mancanza di funzionalità o non viene utilizzato correttamente.

Ora per il ramo su cui lavori - riformattalo come ritieni opportuno, sicuro nella consapevolezza che non sconvolgerai gli altri nove rami del tuo prodotto.

Sarei un po 'preoccupato che tu abbia così tanto nella tua funzione main ().

In tutti i progetti che scrivo, userei main () solo per eseguire l'inizializzazione di oggetti core - come una simulazione o un oggetto applicativo - in queste classi è dove dovrebbe continuare il vero lavoro.

Vorrei anche inizializzare un oggetto di registrazione dell'applicazione in linea di principio da utilizzare a livello globale in tutto il programma.

Infine, aggiungo anche il codice di rilevamento delle perdite nei blocchi del preprocessore che assicurano che sia abilitato solo nelle build DEBUG. Questo è tutto ciò che aggiungerei a main (). Main () dovrebbe essere corto!

Lo dici tu

> Il file contiene sostanzialmente la "classe principale" (invio e coordinamento del lavoro interno principale) del nostro programma

Sembra che questi due compiti possano essere suddivisi in due oggetti separati: un coordinatore e un committente.

Quando li dividi, potresti incasinare il tuo "flusso di lavoro SCC", ma sembra che l'adesione rigorosa al flusso di lavoro SCC stia causando problemi di manutenzione del software. Abbandona, ora e non guardare indietro, perché non appena lo aggiusti, inizierai a dormire facilmente.

Se non sei in grado di prendere la decisione, combatti con il tuo manager con le unghie e con i denti - la tua applicazione deve essere riformattata - e malamente dai suoni di essa! Non prendere no per una risposta!


A quanto ho capito, il problema è questo: se mordi il proiettile e il refactor, non puoi più portare patch tra le versioni. SCC potrebbe essere impostato perfettamente.
Peter

@peterchen - esattamente il problema. Gli SCC si fondono a livello di file. (Unione a 3 vie) Se si sposta il codice tra i file, è necessario iniziare a manipolare manualmente i blocchi di codice modificati da un file a un altro. (La caratteristica GIT che qualcun altro ha menzionato in un altro commento è semplicemente buona per la storia, non per fondersi per quanto ne so)
Martin Ba

2

Come l'hai descritto, il problema principale è la differenza tra pre-split e post-split, unendo correzioni di bug ecc. Strumento che lo circonda. Non ci vorrà molto per codificare una sceneggiatura in Perl, Ruby, ecc. Per strappare la maggior parte del rumore dal diffondere pre-split contro una concatenazione di post-split. Fai ciò che è più semplice in termini di gestione del rumore:

  • rimuovere alcune linee prima / durante la concatenazione (ad esempio includere protezioni)
  • rimuovere altre cose dall'output diff, se necessario

Potresti anche farlo in modo che ogni volta che c'è un check-in, la concatenazione viene eseguita e hai qualcosa pronto a diffrare rispetto alle versioni a file singolo.


2
  1. Non toccare mai più questo file e il codice!
  2. Il trattamento è come qualcosa in cui sei bloccato. Inizia a scrivere adattatori per la funzionalità codificata lì.
  3. Scrivi nuovo codice in unità diverse e parla solo con adattatori che incapsulano la funzionalità del mostro.
  4. ... se non è possibile solo uno dei precedenti, abbandonare il lavoro e procurartene uno nuovo.

2
+/- 0 - seriamente, dove vivi che consiglieresti di abbandonare un lavoro sulla base di un dettaglio tecnico come questo?
Martin Ba,

1

"Il file contiene fondamentalmente la" classe principale "(invio e coordinamento del lavoro interno principale) del nostro programma, quindi ogni volta che viene aggiunta una funzione, influisce anche su questo file e ogni volta che cresce."

Se quel grande SWITCH (che penso ci sia) diventa il principale problema di manutenzione, potresti rifattorizzarlo per usare il dizionario e il modello di comando e rimuovere tutta la logica di commutazione dal codice esistente al caricatore, che popola quella mappa, cioè:

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

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

    ...
    // using
    dispatchTable[id]->Execute();

2
No, in realtà non esiste un grande interruttore. La frase è proprio la più vicina a cui riesco a descrivere questo casino :)
Martin Ba,

1

Penso che il modo più semplice per tenere traccia della cronologia dei sorgenti quando si divide un file sarebbe qualcosa del genere:

  1. Crea copie del codice sorgente originale, utilizzando qualsiasi comando di copia per la conservazione della cronologia fornito dal tuo sistema SCM. Probabilmente dovrai inviare a questo punto, ma non c'è ancora bisogno di dire al tuo sistema di compilazione i nuovi file, quindi dovrebbe andare bene.
  2. Elimina il codice da queste copie. Ciò non dovrebbe interrompere la cronologia delle linee che mantieni.

"usando qualunque comando di copia per preservare la storia che il tuo sistema SCM fornisce" ... cosa negativa che non fornisce
Martin Ba

Peccato. Questo da solo sembra un buon motivo per passare a qualcosa di più moderno. :-)
Christopher Creutzig

1

Penso che cosa farei in questa situazione è un po 'il proiettile e:

  1. Scopri come volevo dividere il file (in base all'attuale versione di sviluppo)
  2. Metti un blocco amministrativo sul file ("Nessuno tocca mainmodule.cpp dopo le 17:00 di venerdì !!!"
  3. Trascorri il tuo lungo weekend applicando tale modifica alle> 10 versioni di manutenzione (dalla più vecchia alla più recente), fino alla versione corrente inclusa.
  4. Elimina mainmodule.cpp da tutte le versioni supportate del software. È una nuova era - non c'è più mainmodule.cpp.
  5. Convincere il management che non dovresti supportare più di una versione di manutenzione del software (almeno senza un grosso contratto di supporto per i prezzi). Se ognuno dei tuoi clienti ha la propria versione unica .... yeeeeeshhhh. Aggiungerei le direttive del compilatore piuttosto che cercare di mantenere più di 10 fork.

Tracciare le vecchie modifiche al file è semplicemente risolto dal tuo primo commento di check-in che dice qualcosa come "diviso da mainmodule.cpp". Se devi tornare a qualcosa di recente, la maggior parte delle persone ricorderà il cambiamento, se è tra 2 anni, il commento dirà loro dove cercare. Naturalmente, quanto sarà prezioso tornare indietro di più di 2 anni per vedere chi ha cambiato il codice e perché?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.