Come posso evitare i refactoring a cascata?


52

Ho un progetto. In questo progetto ho voluto riformattare per aggiungere una funzione e ho riformattato il progetto per aggiungere la funzione.

Il problema è che quando ho finito, ho scoperto che avevo bisogno di apportare una piccola modifica all'interfaccia per adattarla. Quindi ho fatto il cambiamento. E quindi la classe che consuma non può essere implementata con la sua interfaccia attuale in termini di quella nuova, quindi ha bisogno anche di una nuova interfaccia. Ora sono trascorsi tre mesi, e ho dovuto risolvere innumerevoli problemi praticamente indipendenti, e sto cercando di risolvere problemi che erano stati programmati per un anno da oggi o semplicemente elencati come non risolvibili a causa di difficoltà prima che la cosa si compili ancora.

Come posso evitare questo tipo di refactoring a cascata in futuro? È solo un sintomo delle mie lezioni precedenti che dipendono troppo strettamente l'una dall'altra?

Breve modifica: in questo caso, il refactor era la caratteristica, poiché il refactor aumentava l'estensibilità di un particolare pezzo di codice e diminuiva l'accoppiamento. Ciò significava che gli sviluppatori esterni potevano fare di più, che era la funzione che volevo offrire. Quindi lo stesso refattore originale non avrebbe dovuto essere un cambiamento funzionale.

Modifica più grande che avevo promesso cinque giorni fa:

Prima di iniziare questo refattore, avevo un sistema in cui avevo un'interfaccia, ma nell'implementazione, semplicemente dynamic_castattraverso tutte le possibili implementazioni che ho spedito. Ciò ovviamente significava che non potevi semplicemente ereditare dall'interfaccia, per prima cosa, e in secondo luogo, che sarebbe impossibile per chiunque senza accesso all'implementazione implementare questa interfaccia. Così ho deciso che volevo risolvere questo problema e aprire l'interfaccia per il consumo pubblico in modo che chiunque potesse implementarlo e che implementare l'interfaccia fosse l'intero contratto richiesto, ovviamente un miglioramento.

Quando stavo trovando e uccidendo con il fuoco tutti i posti in cui l'avevo fatto, ho trovato un posto che si è rivelato un problema particolare. Dipendeva dai dettagli di implementazione di tutte le varie classi derivate e dalle funzionalità duplicate che erano già implementate ma meglio altrove. Potrebbe invece essere stato implementato in termini di interfaccia pubblica e riutilizzato l'implementazione esistente di tale funzionalità. Ho scoperto che per funzionare correttamente richiedeva un determinato contesto. In parole povere, l'implementazione precedente chiamata sembrava un po 'come

for(auto&& a : as) {
     f(a);
}

Tuttavia, per ottenere questo contesto, avevo bisogno di cambiarlo in qualcosa di più simile

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

Ciò significa che per tutte le operazioni che ffacevano parte di alcune, alcune di esse devono essere rese parte della nuova funzione gche opera senza un contesto e alcune di esse devono essere fatte di una parte dell'ormai differito f. Ma non tutti i metodi fchiamano bisogno o vogliono questo contesto, alcuni di essi hanno bisogno di un contesto distinto che ottengono con mezzi separati. Quindi, per tutto ciò che ffinisce per chiamare (che è, in parole povere, praticamente tutto ), ho dovuto determinare quale, se del caso, contesto avessero bisogno, da dove dovrebbero ottenerlo e come dividerli da vecchi fa nuovi fe nuovi g.

Ed è così che sono finito dove sono ora. L'unica ragione per cui ho continuato è perché avevo bisogno di questo refactoring per altri motivi comunque.


67
Quando dici che "hai riformattato il progetto per aggiungere una funzione", cosa intendi esattamente? Il refactoring non cambia il comportamento dei programmi, per definizione, il che rende confusa questa affermazione.
Jules,

5
@Jules: A rigor di termini, la funzione era quella di consentire ad altri sviluppatori di aggiungere un particolare tipo di estensione, quindi la funzione era il refactor, che rendeva più aperta la struttura della classe.
DeadMG

5
Ho pensato che questo è discusso in ogni libro e articolo che parla di refactoring? Il controllo del codice sorgente viene in soccorso; se trovi che per eseguire il passaggio A devi prima eseguire il passaggio B, quindi scarta A e esegui prima B.
rwong

4
@DeadMG: questo è il libro che in origine volevo citare nel mio primo commento: "Il gioco" pick-up stick "è una buona metafora del metodo Mikado. Elimina i" debiti tecnici ", i problemi legati al passato incorporati in quasi tutti i software sistema, seguendo una serie di regole facili da implementare. Estrarre attentamente ogni dipendenza intrecciata fino a quando non si espone il problema centrale, senza comprimere il progetto. "
rwong

2
Maggio, puoi chiarire di quale linguaggio di programmazione stiamo parlando? Dopo aver letto tutti i tuoi commenti, arrivo alla conclusione che lo stai facendo a mano invece di usare un IDE per aiutarti. Quindi vorrei sapere se posso darti qualche consiglio pratico.
thepacker,

Risposte:


69

L'ultima volta che ho provato ad avviare un refactoring con conseguenze impreviste e non sono riuscito a stabilizzare la build e / o i test dopo un giorno , ho rinunciato e ho ripristinato la base di codice al punto prima del refactoring.

Quindi, ho iniziato ad analizzare ciò che è andato storto e ho sviluppato un piano migliore su come eseguire il refactoring in passaggi più piccoli. Quindi il mio consiglio per evitare rifatturazioni a cascata è solo: sapere quando fermarsi , non lasciare che le cose finiscano fuori dal tuo controllo!

A volte devi mordere il proiettile e buttare via un'intera giornata di lavoro - decisamente più facile che buttare via tre mesi di lavoro. Il giorno in cui perdi non è del tutto inutile, almeno hai imparato a non affrontare il problema. E per la mia esperienza, ci sono sempre possibilità di fare piccoli passi nel refactoring.

Nota a margine : ti sembra di trovarti in una situazione in cui devi decidere se sei disposto a sacrificare tre mesi interi di lavoro e ricominciare da capo con un nuovo (e si spera più successo) piano di refactoring. Immagino che non sia una decisione facile, ma chiediti, quanto è alto il rischio di cui hai bisogno per altri tre mesi non solo per stabilizzare la build, ma anche per correggere tutti i bug imprevisti che probabilmente hai introdotto durante la riscrittura che hai fatto negli ultimi tre mesi ? Ho scritto "riscrivere", perché immagino che sia quello che hai fatto davvero, non un "refactoring". Non è improbabile che tu possa risolvere il tuo attuale problema più rapidamente tornando all'ultima revisione in cui il tuo progetto viene compilato e ricominciare con un vero refactoring (invece di "riscrivere").


53

È solo un sintomo delle mie lezioni precedenti che dipendono troppo strettamente l'una dall'altra?

Sicuro. Un cambiamento che causa una miriade di altri cambiamenti è praticamente la definizione di accoppiamento.

Come evitare i refactor a cascata?

Nel peggior tipo di codebase, una singola modifica continuerà a cascata, causando infine la modifica (quasi) di tutto. Parte di qualsiasi refattore in cui è presente un accoppiamento diffuso consiste nell'isolare la parte su cui si sta lavorando. Devi refactoring non solo dove la tua nuova funzionalità tocca questo codice, ma dove tutto il resto tocca quel codice.

Di solito ciò significa creare alcuni adattatori per aiutare il vecchio codice a funzionare con qualcosa che assomiglia e si comporta come il vecchio codice, ma utilizza la nuova implementazione / interfaccia. Dopotutto, se tutto ciò che fai è cambiare l'interfaccia / implementazione ma lasciare l'accoppiamento non stai guadagnando nulla. È rossetto su un maiale.


33
+1 Più è necessario un refactoring, più ampiamente raggiungerà il refactoring. È la natura stessa della cosa.
Paul Draper,

4
Se stai davvero eseguendo il refactoring , tuttavia, gli altri codici non dovrebbero preoccuparsi subito delle modifiche. (Naturalmente alla fine vorrai ripulire le altre parti ... ma questo non dovrebbe essere immediatamente richiesto.) Un cambiamento che "precipita" attraverso il resto dell'app, è più grande del refactoring - a quel punto è sostanzialmente una riprogettazione o riscrittura.
cHao,

+1 Un adattatore è esattamente il modo per isolare prima il codice che si desidera modificare.
winkbrace,

17

Sembra che il tuo refactoring fosse troppo ambizioso. Un refactoring dovrebbe essere applicato a piccoli passi, ognuno dei quali può essere completato in (diciamo) 30 minuti - o, nel peggiore dei casi, al massimo un giorno - e lascia il progetto costruibile e tutti i test continuano a passare.

Se mantieni ogni singolo cambiamento minimo, in realtà non dovrebbe essere possibile per un refactoring interrompere la tua build per lungo tempo. Il caso peggiore è probabilmente quello di modificare i parametri in un metodo in un'interfaccia ampiamente utilizzata, ad esempio per aggiungere un nuovo parametro. Ma le conseguenti modifiche da questo sono meccaniche: aggiungere (e ignorare) il parametro in ogni implementazione e aggiungere un valore predefinito in ogni chiamata. Anche se ci sono centinaia di riferimenti, non dovrebbe essere necessario nemmeno un giorno per eseguire un tale refactoring.


4
Non vedo come possa sorgere una situazione del genere. Per ogni ragionevole refactoring dell'interfaccia di un metodo ci deve essere una nuova serie di parametri facilmente determinabile che può essere passata e che il comportamento della chiamata sarà lo stesso di prima della modifica.
Jules,

3
Non sono mai stato in una situazione in cui volevo eseguire un tale refactoring, ma devo dire che mi sembra abbastanza insolito. Stai dicendo che hai rimosso la funzionalità dall'interfaccia? Se è così, dove è andato? In un'altra interfaccia? O altrove?
Jules,

5
Quindi il modo per farlo è rimuovere tutti gli usi della funzione da rimuovere prima del refactoring per rimuoverlo, piuttosto che successivamente. Ciò ti consente di mantenere la creazione del codice mentre ci stai lavorando.
Jules,

11
@DeadMG: sembra strano: stai rimuovendo una funzione che non è più necessaria, come dici tu. D'altra parte, scrivi "il progetto diventa completamente non funzionale" - sembra che la funzione sia assolutamente necessaria. Si prega di precisare.
Doc Brown,

26
@DeadMG In questi casi normalmente svilupperesti la nuova funzione, aggiungeresti dei test per assicurarti che funzioni, trasferisci il codice esistente per usare la nuova interfaccia e quindi rimuovi la (vecchia) funzionalità superflua. In questo modo, non dovrebbe esserci un punto in cui le cose si rompano.
sapi,

12

Come posso evitare questo tipo di refattore a cascata in futuro?

Disegno di pensiero desideroso

L'obiettivo è la progettazione e l'implementazione di OO eccellenti per la nuova funzionalità. Evitare il refactoring è anche un obiettivo.

Inizia da zero e crea un design per la nuova funzionalità che è ciò che desideri avere. Prenditi il ​​tempo per farlo bene.

Si noti tuttavia che la chiave qui è "aggiungi una funzione". Nuove cose tendono a farci ignorare in gran parte l'attuale struttura della base di codice. Il nostro pio desiderio è indipendente. Ma abbiamo quindi bisogno di altre due cose:

  • Refactor è sufficiente per creare una cucitura necessaria per iniettare / implementare il codice della nuova funzione.
    • La resistenza al refactoring non dovrebbe guidare il nuovo design.
  • Scrivi a un cliente che si trova di fronte una classe con un'API che mantenga la nuova funzionalità e il codice esistente beatamente ignoranti l'uno dell'altro.
    • Traslittera per ottenere oggetti, dati e risultati avanti e indietro. Il principio della minima conoscenza sia dannato. Non faremo nulla di peggio di quello che già fa il codice esistente.

Euristica, lezioni apprese, ecc.

Il refactoring è stato semplice come aggiungere un parametro predefinito a una chiamata di metodo esistente; o una singola chiamata a un metodo di classe statica.

I metodi di estensione su classi esistenti possono aiutare a mantenere la qualità del nuovo design con un rischio minimo assoluto.

La "struttura" è tutto. La struttura è la realizzazione del Principio della singola responsabilità; design che facilita la funzionalità. Il codice rimarrà breve e semplice fino alla gerarchia di classi. Il tempo per il nuovo design viene recuperato durante i test, la rielaborazione e l'evitamento degli hacker nella giungla del codice legacy.

Le desiderose lezioni di pensiero si concentrano sul compito da svolgere. In genere, dimentica di estendere una classe esistente: stai solo inducendo di nuovo la cascata del refattore e devi affrontare il sovraccarico della classe "più pesante".

Elimina eventuali residui di questa nuova funzionalità dal codice esistente. In questo caso, la funzionalità di nuove funzionalità complete e ben incapsulate è più importante che evitare il refactoring.


9

Dal (meraviglioso) libro Lavorare efficacemente con il codice legacy di Michael Feathers :

Quando interrompi le dipendenze nel codice legacy, spesso devi sospendere un po 'il tuo senso estetico. Alcune dipendenze si rompono nettamente; altri finiscono per sembrare meno che ideali dal punto di vista del design. Sono come i punti di incisione in chirurgia: potrebbe esserci una cicatrice nel codice dopo il lavoro, ma tutto ciò che sta sotto può migliorare.

Se in seguito puoi coprire il codice attorno al punto in cui hai rotto le dipendenze, puoi anche curare quella cicatrice.


6

Sembra che (specialmente dalle discussioni nei commenti) ti sei inscatolato con regole autoimposte che significano che questa modifica "minore" è la stessa quantità di lavoro di una riscrittura completa del software.

La soluzione deve essere "non farlo, allora" . Questo è ciò che accade nei progetti reali. Molte vecchie API hanno di conseguenza brutte interfacce o parametri abbandonati (sempre nulli) o funzioni denominate DoThisThing2 () che fa lo stesso di DoThisThing () con un elenco di parametri completamente diverso. Altri trucchi comuni includono lo stashing delle informazioni in globi o puntatori con tag per far passare di nascosto un grosso pezzo di framework. (Ad esempio, ho un progetto in cui metà dei buffer audio contiene solo un valore magico di 4 byte perché era molto più semplice che cambiare il modo in cui una libreria ha invocato i suoi codec audio.)

È difficile dare consigli specifici senza un codice specifico.


3

Test automatizzati Non devi essere un fanatico TDD, né hai bisogno di una copertura del 100%, ma i test automatici sono ciò che ti consente di apportare modifiche con sicurezza. Inoltre, sembra che tu abbia un design con un accoppiamento molto elevato; dovresti leggere i principi SOLID, che sono formulati specificamente per affrontare questo tipo di problema nella progettazione del software.

Consiglierei anche questi libri.

  • Lavorare efficacemente con il codice legacy , Feathers
  • refactoring , Fowler
  • Software orientato agli oggetti in crescita, guidato da test , Freeman e Pryce
  • Codice pulito , Martin

3
La tua domanda è "come posso evitare questo [fallimento] in futuro?" La risposta è che, anche se al momento "hai" elementi della configurazione e dei test, non li stai applicando correttamente. Non ho avuto un errore di compilazione che è durato più di dieci minuti negli anni, perché vedo la compilazione come "il primo test unitario", e quando si interrompe, lo risolvo, perché devo essere in grado di vedere i test che passano come Sto lavorando ulteriormente sul codice.
asthasr,

6
Se sto eseguendo il refactoring di un'interfaccia molto utilizzata, aggiungo uno spessore. Questo shim gestisce le impostazioni predefinite, in modo che le chiamate legacy continuino a funzionare. Lavoro sull'interfaccia dietro lo shim, quindi, quando ho finito, comincio a cambiare classe per usare nuovamente l'interfaccia anziché lo shim.
asthasr,

5
Continuare a refactoring nonostante il fallimento della costruzione è simile alla resa dei conti morta . È una tecnica di navigazione di ultima istanza . Nel refactoring, è possibile che una direzione del refactoring sia semplicemente sbagliata, e hai già visto il segno rivelatore di ciò (nel momento in cui smette di compilare, cioè volare senza indicatori di velocità), ma hai deciso di continuare. Alla fine l'aereo cade dal radar. Fortunatamente non abbiamo bisogno di blackbox o investigatori per il refactoring: possiamo sempre "ripristinare l'ultimo stato noto".
rwong

4
@DeadMG: hai scritto "Nel mio caso, le chiamate precedenti semplicemente non hanno più senso", ma nella tua domanda "una piccola modifica dell'interfaccia per adattarla". Onestamente, solo una di quelle due frasi può essere vera. E dalla descrizione del tuo problema sembra abbastanza chiaro che il cambio di interfaccia non è stato sicuramente secondario . Dovresti davvero, davvero pensare di più a come rendere il tuo cambiamento più retrocompatibile. Per la mia esperienza, è sempre possibile, ma prima devi elaborare un buon piano.
Doc Brown,

3
@DeadMG In quel caso, penso che ciò che stai facendo non possa ragionevolmente essere definito refactoring, il cui punto fondamentale è applicare le modifiche al design come una serie di passaggi molto semplici.
Jules,

3

È solo un sintomo delle mie lezioni precedenti che dipendono troppo strettamente l'una dall'altra?

Molto probabilmente sì. Anche se puoi ottenere effetti simili con una base di codice piuttosto bella e pulita quando i requisiti cambiano abbastanza

Come posso evitare questo tipo di refactoring a cascata in futuro?

Oltre a smettere di lavorare sul codice legacy, non puoi temere. Ma quello che puoi è usare un metodo che evita l'effetto di non avere una base di codice funzionante per giorni, settimane o addirittura mesi.

Quel metodo si chiama "Metodo Mikado" e funziona così:

  1. annota l'obiettivo che vuoi raggiungere su un pezzo di carta

  2. fai il cambiamento più semplice che ti porta in quella direzione.

  3. controlla se funziona usando il compilatore e la tua suite di test. Se continua con il passaggio 7. altrimenti continua con il passaggio 4.

  4. sulla tua carta annota le cose che devono cambiare per far funzionare la tua attuale modifica. Disegna le frecce, dal tuo compito attuale, a quelli nuovi.

  5. Annulla le modifiche Questo è il passaggio importante. È contro intuitivo e fa male fisicamente all'inizio, ma poiché hai appena provato una cosa semplice, in realtà non è poi così male.

  6. scegli una delle attività che non presenta errori in uscita (nessuna dipendenza nota) e torna a 2.

  7. confermare la modifica, barrare l'attività sulla carta, selezionare un'attività che non presenta errori in uscita (nessuna dipendenza nota) e tornare a 2.

In questo modo avrai una base di codice funzionante a brevi intervalli. Dove puoi anche unire le modifiche dal resto della squadra. E hai una rappresentazione visiva di ciò che sai che devi ancora fare, questo aiuta a decidere se vuoi continuare con l'endevour o se dovresti fermarlo.


2

refactoring è una disciplina strutturata, distinta dalla pulizia del codice come meglio credi. È necessario disporre di test unitari scritti prima di iniziare e ogni passaggio dovrebbe consistere in una trasformazione specifica che si sa non dovrebbe apportare modifiche alla funzionalità. I test unitari dovrebbero passare dopo ogni modifica.

Naturalmente, durante il processo di refactoring, scoprirai naturalmente i cambiamenti che dovrebbero essere applicati e che potrebbero causare rotture. In tal caso, fai del tuo meglio per implementare uno shim di compatibilità per la vecchia interfaccia che utilizza il nuovo framework. In teoria, il sistema dovrebbe ancora funzionare come prima e i test unitari dovrebbero passare. È possibile contrassegnare lo shim di compatibilità come interfaccia obsoleta e ripulirlo in un momento più adatto.


2

... Ho riformattato il progetto per aggiungere la funzione.

Come detto @Jules, il refactoring e l'aggiunta di funzionalità sono due cose molto diverse.

  • Il refactoring consiste nel cambiare la struttura del programma senza alterarne il comportamento.
  • L'aggiunta di una funzione, invece, ne aumenta il comportamento.

... ma in effetti, a volte è necessario cambiare il funzionamento interno per aggiungere le tue cose, ma preferirei chiamarlo modificando piuttosto che refactoring.

Ho dovuto apportare una piccola modifica all'interfaccia per adattarla

Ecco dove le cose si fanno confuse. Le interfacce sono intese come limiti per isolare l'implementazione da come viene utilizzata. Non appena si toccano le interfacce, anche tutto su entrambi i lati (implementandolo o utilizzandolo) dovrà essere modificato. Questo può diffondersi fino a quando l'hai vissuto.

quindi la classe che consuma non può essere implementata con la sua interfaccia attuale in termini di quella nuova, quindi ha bisogno anche di una nuova interfaccia.

Il fatto che un'interfaccia richieda una modifica suona bene ... che si diffonde ad un'altra implica che le modifiche si diffondano ulteriormente. Sembra che una qualche forma di input / dati richieda il flusso lungo la catena. È così?


Il tuo discorso è molto astratto, quindi è difficile da capire. Un esempio sarebbe molto utile. Di solito, le interfacce dovrebbero essere piuttosto stabili e indipendenti l'una dall'altra, rendendo possibile modificare parte del sistema senza danneggiare il resto ... grazie alle interfacce.

... in realtà, il modo migliore per evitare modifiche a codice in cascata sono proprio buone interfacce. ;)


-1

Penso che di solito non puoi se non sei disposto a mantenere le cose come sono. Tuttavia, quando situazioni come la tua, penso che la cosa migliore sia informare il team e far loro sapere perché dovrebbero essere fatti dei refactoring per continuare uno sviluppo più sano. Non vorrei semplicemente andare a sistemare le cose da solo. Ne parlerei durante le riunioni Scrum (supponendo che abbiate voi ragazzi) e lo approccerei sistematicamente con altri sviluppatori.


1
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti risposte 9
moscerino del

@gnat: forse no, ma ha semplificato le risposte.
Tarik,
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.