Qual è il modo migliore per gestire il refactoring di un file di grandi dimensioni?


41

Attualmente sto lavorando a un progetto più grande che sfortunatamente contiene alcuni file in cui le linee guida sulla qualità del software non sono sempre state seguite. Ciò include file di grandi dimensioni (leggi 2000-4000 righe) che contengono chiaramente più funzionalità distinte.

Ora voglio riformattare questi file di grandi dimensioni in più file di piccole dimensioni. Il problema è che, essendo così grandi, su questi file lavorano più persone (me incluso) su diversi rami. Quindi non posso davvero derivare dallo sviluppo e dal refactoring, poiché la fusione di questi refactoring con i cambiamenti di altre persone diventerà difficile.

Ovviamente potremmo richiedere a tutti di ricollegarsi per sviluppare, "congelare" i file (ovvero non consentire più a nessuno di modificarli), refactor e quindi "sbloccare". Ma questo non è neanche molto buono, dal momento che ciò richiederebbe praticamente a tutti di interrompere il proprio lavoro su questi file fino al completamento del refactoring.

Quindi c'è un modo per refactoring, non è necessario che nessun altro smetta di funzionare (per troppo tempo) o unisca nuovamente i rami delle sue caratteristiche per svilupparsi?


6
Penso che ciò dipenda anche dal linguaggio di programmazione utilizzato.
Robert Andrzejuk,

8
Mi piacciono i check-in "piccoli incrementali". A meno che qualcuno non mantenga aggiornata la propria copia del repository, questa pratica minimizzerà i conflitti di unione per tutti.
Matt Raffel,

5
Come sono i tuoi test? Se hai intenzione di refactoring di un grosso (e probabilmente importante!) Codice, assicurati che la tua suite di test sia in ottime condizioni prima di refactoring. Ciò renderà molto più semplice assicurarti di averlo correttamente nei file più piccoli.
corsiKa

1
Ci sono numerosi approcci che potresti adottare con questo e l'approccio migliore dipenderà dalla tua situazione.
Stephen

3
Ho aderito al progetto in cui il file più grande è lungo 10k linee che contengono tra l'altro una classe che è lunga 6k linee e tutti hanno paura di toccarlo. Voglio dire che la tua domanda è fantastica. Abbiamo persino inventato una battuta che questa singola classe è una buona ragione per sbloccare la rotella di scorrimento nei nostri mouse.
ElmoVanKielmo

Risposte:


41

Hai correttamente capito che questo non è tanto un problema tecnico quanto un problema sociale: se vuoi evitare conflitti di fusione eccessivi, il team deve collaborare in modo da evitare questi conflitti.

Questo fa parte di un problema più grande con Git, in quanto la ramificazione è molto semplice, ma la fusione può ancora richiedere molto sforzo. I team di sviluppo tendono a lanciare molte filiali e sono quindi sorpresi dal fatto che fonderle sia difficile, probabilmente perché stanno cercando di emulare Git Flow senza comprenderne il contesto.

La regola generale per fusioni veloci e facili è quella di prevenire l'accumulo di grandi differenze, in particolare che i rami delle caratteristiche dovrebbero avere una vita molto breve (ore o giorni, non mesi). Un team di sviluppo in grado di integrare rapidamente i propri cambiamenti vedrà meno conflitti di unione. Se un codice non è ancora pronto per la produzione, potrebbe essere possibile integrarlo ma disattivarlo tramite un flag di funzionalità. Non appena il codice è stato integrato nel ramo principale, diventa accessibile al tipo di refactoring che si sta tentando di eseguire.

Potrebbe essere troppo per il tuo problema immediato. Ma può essere fattibile chiedere ai colleghi di unire le loro modifiche che incidono su questo file fino alla fine della settimana in modo da poter eseguire il refactoring. Se aspettano più a lungo, dovranno affrontare da soli i conflitti di unione. Non è impossibile, è solo un lavoro evitabile.

Potresti anche voler evitare la rottura di ampie strisce di codice dipendente e apportare solo modifiche compatibili con l'API. Ad esempio, se si desidera estrarre alcune funzionalità in un modulo separato:

  1. Estrai la funzionalità in un modulo separato.
  2. Modifica le vecchie funzioni per inoltrare le loro chiamate alla nuova API.
  3. Nel tempo, codice dipendente dalla porta per la nuova API.
  4. Infine, puoi eliminare le vecchie funzioni.
  5. (Ripeti per il prossimo gruppo di funzionalità)

Questo processo in più passaggi può evitare molti conflitti di unione. In particolare, ci saranno conflitti solo se qualcun altro sta cambiando anche la funzionalità che hai estratto. Il costo di questo approccio è che è molto più lento rispetto alla modifica di tutto in una volta e che hai temporaneamente due API duplicate. Questo non è poi così grave fino a quando qualcosa di urgente non interrompe questo refactoring, la duplicazione viene dimenticata o depriorizzata e si finisce con un mucchio di debiti tecnologici.

Ma alla fine, qualsiasi soluzione richiederà di coordinarti con il tuo team.


1
@Laiv Purtroppo questo è un consiglio estremamente generale, ma alcune idee fuori dallo spazio agile come l'integrazione continua hanno chiaramente i loro meriti. I team che lavorano insieme (e integrano il loro lavoro frequentemente) avranno più tempo a fare grandi cambiamenti trasversali rispetto ai team che lavorano solo uno di fianco all'altro. Non si tratta necessariamente dell'SDLC in generale, ma piuttosto della collaborazione all'interno del team. Alcuni approcci rendono il lavoro a fianco più fattibile (pensa a principi aperti / chiusi, microservizi) ma il team di OP non è ancora lì.
am

22
Non direi che un ramo delle caratteristiche deve avere una vita breve - semplicemente che non dovrebbe divergere dal ramo principale per lunghi periodi di tempo. La fusione regolare delle modifiche dal ramo principale al ramo della funzione funziona nei casi in cui il ramo della funzione deve rimanere più a lungo. Tuttavia, è una buona idea mantenere i rami delle funzionalità non più del necessario.
Dan Lyons,

1
@Laiv Nella mia esperienza, ha senso discutere in anticipo un progetto post refactoring con il team, ma di solito è più semplice se una sola persona apporta le modifiche al codice. Altrimenti, sei tornato al problema che devi unire le cose. Le linee 4k sembrano molto, ma in realtà non è per refactoring mirati come la classe di estrazione . (Scarterei il libro di Refactoring di Martin Fowler così duramente qui se lo avessi letto.) Ma le righe 4k sono molto solo per i refactoring non mirati come "vediamo come posso migliorare questo".
amon

1
@DanLyons In linea di principio hai ragione: questo può diffondere parte dello sforzo di fusione. In pratica, la fusione di Git dipende molto dall'ultimo commit antenato comune delle filiali che vengono unite. L'unione della funzione master → non ci fornisce un nuovo antenato comune su master, ma l'unione della funzione → master lo fa. Con ripetute fusioni di master → feature, può succedere che dobbiamo risolvere gli stessi conflitti ancora e ancora (ma vedi git rerere per automatizzare questo). Rebasing è strettamente superiore qui perché la punta del maestro diventa il nuovo antenato comune, ma la riscrittura della storia ha altri problemi.
am

1
La risposta è OK per me, tranne per il fatto che Git rende troppo facile ramificarsi, e quindi gli sviluppatori si ramificano troppo spesso. Ricordo bene i tempi di SVN e persino CVS in cui la ramificazione era abbastanza (o almeno ingombrante) abbastanza che la gente generalmente lo evitava, se possibile, con tutti i relativi problemi. In effetti, essendo un sistema distribuito , avere molti rami non è in realtà niente di diverso che avere molti repository separati (cioè su ogni sviluppatore). La soluzione sta altrove, essere facili da ramificare non è il problema. (E sì, vedo che è solo un lato ... ma comunque).
AnoE

30

Esegui il refactoring a piccoli passi. Supponiamo che il tuo file di grandi dimensioni abbia il nome Foo:

  1. Aggiungi un nuovo file vuoto Bare assegnalo a "trunk".

  2. Trova una piccola parte del codice in Foocui può essere spostato Bar. Applica lo spostamento, aggiorna da trunk, crea e verifica il codice e esegui il commit su "trunk".

  3. Ripetere il passaggio 2 fino a quando Fooe Baravere dimensioni uguali (o qualunque dimensione che preferite)

In questo modo, la prossima volta che i tuoi compagni di squadra aggiornano i loro rami dal tronco, ottengono le tue modifiche in "piccole porzioni" e possono unirle una per una, il che è molto più facile che dover unire una divisione completa in un solo passaggio. Lo stesso vale quando nel passaggio 2 si ottiene un conflitto di unione perché qualcun altro ha aggiornato il trunk in mezzo.

Ciò non eliminerà i conflitti di unione o la necessità di risolverli manualmente, ma limita ogni conflitto a una piccola area di codice, che è molto più gestibile.

E ovviamente: comunica il refactoring nella squadra. Informa i tuoi compagni su ciò che stai facendo, in modo che sappiano perché devono aspettarsi unire conflitti per il file specifico.


2
Ciò è particolarmente utile con l' rerereopzione
gits

@ D.BenKnoble: grazie per quell'aggiunta. Devo ammettere che non sono un esperto di git (ma il problema descritto non è specifico per git, si applica a qualsiasi VCS che consente la ramificazione e la mia risposta dovrebbe adattarsi alla maggior parte di quei sistemi).
Doc Brown,

Ho calcolato in base alla terminologia; in effetti, con git, questo tipo di unione viene ancora eseguita una sola volta (se si tira e si fonde). Ma si può sempre tirare e selezionare la ciliegia, o unire i singoli commit, o rebase a seconda delle preferenze dello sviluppatore. Ci vuole più tempo, ma è certamente possibile se la fusione automatica sembra non riuscire.
D. Ben Knoble

18

Stai pensando di dividere il file come un'operazione atomica, ma puoi apportare modifiche intermedie. Il file è diventato gradualmente enorme nel tempo, può gradualmente diventare piccolo nel tempo.

Scegli una parte che non ha dovuto cambiare da molto tempo (git blame può aiutarti in questo), e prima dividi. Ottieni quel cambiamento unito nei rami di tutti, quindi scegli la parte successiva più semplice da dividere. Forse anche dividere una parte è un passo troppo grande e dovresti prima fare un po 'di riarrangiamento all'interno del file di grandi dimensioni.

Se le persone non si ricongiungono frequentemente per svilupparsi, dovresti incoraggiarlo, quindi dopo essersi unito, cogli l'occasione per dividere le parti che hanno appena cambiato. Oppure chiedi loro di eseguire la divisione come parte della revisione della richiesta pull.

L'idea è di avanzare lentamente verso il tuo obiettivo. Ti sembrerà che i progressi siano lenti, ma all'improvviso ti renderai conto che il tuo codice è molto meglio. Ci vuole molto tempo per girare un transatlantico.


Il file potrebbe essere stato avviato in grande. I file di quelle dimensioni possono essere creati rapidamente. Conosco persone che sanno scrivere migliaia di LoC in un giorno o settimana. E OP non ha menzionato i test automatici, il che mi indica che mancano.
ChuckCottrill

9

Sto per suggerire una soluzione diversa dalla normale a questo problema.

Usa questo come evento del codice team. Chiedi a tutti di effettuare il check-in del proprio codice, quindi di aiutare gli altri che stanno ancora lavorando con il file. Una volta che tutti i soggetti rilevanti hanno verificato il proprio codice, trova una sala conferenze con un proiettore e lavora insieme per iniziare a spostare le cose in nuovi file.

Potresti voler impostare un determinato periodo di tempo su questo, in modo che non finisca per essere una settimana di discussioni senza fine in vista. Invece, questo potrebbe anche essere un evento settimanale di 1-2 ore fino a quando tutti non vedranno le cose come devono essere. Forse hai solo bisogno di 1-2 ore per il refactoring del file. Probabilmente non lo saprai fino a quando non ci proverai.

Ciò ha il vantaggio di essere tutti sulla stessa pagina (nessun gioco di parole previsto) con il refactoring, ma può anche aiutarti a evitare errori e ottenere input da altri su possibili raggruppamenti di metodi da mantenere, se necessario.

Farlo in questo modo può essere considerato avere una revisione del codice integrata, se lo fai. Ciò consente alla quantità appropriata di sviluppatori di approvare il tuo codice non appena lo fai fare il check-in e pronto per la loro revisione. Potresti comunque voler controllare il codice per tutto ciò che hai perso, ma è molto utile per assicurarsi che il processo di revisione sia più breve.

Questo potrebbe non funzionare in tutte le situazioni, i team o le aziende, poiché il lavoro non è distribuito in modo tale da consentire che ciò avvenga facilmente. Può anche essere (erroneamente) interpretato come un uso improprio del tempo di sviluppo. Questo codice di gruppo richiede il buy-in da parte del manager e del refactor stesso.

Per aiutare a vendere questa idea al tuo manager, menziona il bit di revisione del codice e tutti quelli che sanno dove sono fin dall'inizio. Può essere utile evitare che gli sviluppatori perdano tempo a cercare una serie di nuovi file. Inoltre, impedire agli sviluppatori di ottenere PO in merito a dove le cose sono finite o "completamente mancanti" è di solito una buona cosa. (Minore è il tracollo, meglio IMO.)

Una volta ottenuto il refactoring di un file in questo modo, è possibile ottenere più facilmente l'approvazione per più refactor, se ha avuto successo e utile.

Comunque decidi di fare il tuo refactor, buona fortuna!


Questo è un suggerimento fantastico che cattura un ottimo modo per raggiungere il coordinamento del team che sarà fondamentale per farlo funzionare. Inoltre, se alcuni rami non possono essere ricollegati per masterprimi, almeno hai tutti nella stanza per aiutare a gestire le fusioni in quei rami.
Colin Young,

+1 per aver suggerito il codice mob
Jon Raynor il

1
Questo affronta esattamente l'aspetto sociale del problema.
ChuckCottrill

4

Per risolvere questo problema è necessario il buy-in dagli altri team perché si sta tentando di modificare una risorsa condivisa (il codice stesso). Detto questo, penso che ci sia un modo per "migrare via" dall'avere enormi file monolitici senza disturbare le persone.

Consiglio anche di non scegliere come target tutti i file di grandi dimensioni contemporaneamente meno che il numero di file di grandi dimensioni non cresca in modo incontrollato oltre alle dimensioni dei singoli file.

Il refactoring di file di grandi dimensioni come questo spesso causa problemi imprevisti. Il primo passo è impedire che i file di grandi dimensioni accumulino funzionalità aggiuntive oltre a ciò che è attualmente in master o nei rami di sviluppo .

Penso che il modo migliore per farlo sia con hook di commit che bloccano alcune aggiunte ai file di grandi dimensioni per impostazione predefinita, ma possono essere annullati con un commento magico nel messaggio di commit, come @bigfileoko qualcosa del genere. È importante essere in grado di scavalcare la politica in modo indolore ma tracciabile. Idealmente, dovresti essere in grado di eseguire l'hook di commit localmente e dovrebbe dirti come sovrascrivere questo particolare errore nel messaggio di errore stesso . Inoltre, questa è solo la mia preferenza, ma i commenti magici non riconosciuti o i commenti magici che sopprimono gli errori che in realtà non si sono attivati nel messaggio di commit dovrebbero essere un avviso o un errore di commit-time in modo da non addestrare inavvertitamente le persone a sopprimere gli hook indipendentemente da se ne hanno bisogno o no.

Il hook di commit può verificare la presenza di nuove classi o eseguire altre analisi statiche (ad hoc o meno). Puoi anche scegliere una riga o un numero di caratteri maggiore del 10% rispetto al file attualmente e dire che il file di grandi dimensioni non può crescere oltre il nuovo limite. Puoi anche rifiutare singoli commit che aumentano il file di grandi dimensioni di troppe righe o troppi caratteri o w / e.

Una volta che il file di grandi dimensioni smette di accumulare nuove funzionalità, è possibile eseguire il refactoring delle cose una alla volta (e ridurre contemporaneamente i treshold imposti dagli hook di commit per evitare che cresca di nuovo).

Alla fine, i file di grandi dimensioni saranno abbastanza piccoli da poter rimuovere completamente gli hook di commit.


-3

Aspetta fino al momento. Dividi il file, esegui il commit e unisci al master.

Altre persone dovranno inserire i cambiamenti nei loro rami delle funzioni al mattino come qualsiasi altro cambiamento.


3
Significherebbe comunque che avrebbero dovuto fondere i miei refactoring con i loro cambiamenti ...
Hoff,


1
Bene, in realtà devono comunque fare i conti con le fusioni se cambiano tutti questi file.
Laiv

9
Questo ha il problema di "Sorpresa, ho rotto tutte le tue cose." L'OP deve ottenere il buy-in e l'approvazione prima di farlo, e farlo in un orario programmato che nessun altro ha il file "in corso" sarebbe di aiuto.
computercarguy

6
Per amore di Cthulhu non farlo. È il modo peggiore in cui puoi lavorare in gruppo.
Corse di leggerezza con Monica
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.