Come evitare i rifattori a cascata?

52

Ho un progetto. In questo progetto ho voluto refactoring per aggiungere una funzionalità, e ho refactored il progetto per aggiungere la funzionalità.

Il problema è che quando ho finito, ho scoperto che dovevo apportare una piccola modifica all'interfaccia per adattarla. Così ho fatto il cambiamento. E quindi la classe consumante non può essere implementata con la sua interfaccia attuale in termini di quella nuova, quindi ha bisogno anche di una nuova interfaccia. Ora sono passati tre mesi e ho dovuto risolvere innumerevoli problemi praticamente non correlati, e sto cercando di risolvere problemi che sono stati tracciati per un anno da adesso o semplicemente elencati come non risolveranno a causa di difficoltà prima che la cosa si compili ancora una volta.

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

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

Modifica più grande che ho promesso cinque giorni fa:

Prima di iniziare questo refactator, avevo un sistema in cui avevo un'interfaccia, ma nell'implementazione, ho semplicemente dynamic_cast attraverso tutte le possibili implementazioni che ho spedito. Questo ovviamente significava che non si poteva semplicemente ereditare dall'interfaccia, per una cosa, e in secondo luogo, che sarebbe impossibile per chiunque senza l'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 l'implementazione dell'interfaccia richiedesse l'intero contratto, ovviamente un miglioramento.

Quando stavo cercando e uccidendo con il fuoco tutti i posti in cui l'avevo fatto, ho trovato un posto che si è rivelato un problema particolare. Dipende dai dettagli di implementazione di tutte le varie classi derivate e dalle funzionalità duplicate già implementate ma migliori altrove. Potrebbe invece essere implementato in termini di interfaccia pubblica e riutilizzare l'implementazione esistente di tale funzionalità. Ho scoperto che richiedeva un particolare contesto per funzionare correttamente. In parole povere, l'implementazione precedente della 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 a

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 erano una parte di f , alcune di esse devono essere rese parte della nuova funzione g che opera senza contesto, e alcune di esse devono essere fatte di una parte del% co_de ora differito. Ma non tutti i metodi di chiamata di f necessitano o vogliono questo contesto - alcuni di essi necessitano di un contesto distinto che ottengono attraverso mezzi separati. Quindi, per tutto ciò che f finisce per chiamare (che è, grosso modo, tutto ), ho dovuto determinare quali, se del caso, il contesto di cui avevano bisogno, da dove avrebbero dovuto riceverlo, e come dividerli dal vecchio f in nuovo f e nuovo f .

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.

    
posta DeadMG 11.01.2015 - 15:53
fonte

11 risposte

69

L'ultima volta che ho provato ad avviare un refactoring con conseguenze impreviste, e non sono riuscito a stabilizzare la build e / oi test dopo un giorno , ho rinunciato e ho ripristinato il codebase fino al punto precedente refactoring.

Poi, ho iniziato ad analizzare cosa è andato storto e ho sviluppato un piano migliore su come eseguire il refactoring a piccoli passi. Quindi il mio consiglio per evitare refactoring a cascata è solo: sapere quando smettere , non lasciare che le cose finiscano il 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 è completamente inutile, almeno hai imparato come non ad affrontare il problema. E per la mia esperienza, ci sono sempre possibilità di fare piccoli passi nel refactoring.

Nota a margine : ti sembra di essere in una situazione in cui devi decidere se sei disposto a sacrificare tre mesi completi di lavoro e ricominciare da capo con un nuovo (e sperabilmente più efficace) refactoring Piano. Posso immaginare che non è una decisione facile, ma chiediti, quanto è alto il rischio che hai bisogno di altri tre mesi non solo per stabilizzare la build, ma anche per correggere tutti i bug imprevisti che probabilmente hai introdotto durante la riscrittura hai fatto gli ultimi tre mesi? Ho scritto "riscrivi", perché immagino sia quello che hai fatto davvero, non un "refactoring". Non è improbabile che tu possa risolvere il tuo attuale problema più velocemente tornando all'ultima revisione in cui il tuo progetto si compila e inizia di nuovo con un reale refactoring (opposto a "riscrivere") di nuovo.

    
risposta data 12.01.2015 - 08:30
fonte
53

Is it just a symptom of my previous classes depending too tightly on each other?

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

How do I avoid cascading refactors?

Nel peggiore tipo di codebase, una singola modifica continuerà a sovrapporsi, causando alla fine la modifica (quasi) di tutto. Parte di ogni refactoring dove c'è un accoppiamento diffuso è quello di isolare la parte su cui stai lavorando. Devi refactoring non solo dove la tua nuova funzionalità sta toccando questo codice, ma dove tutto il resto tocca quel codice.

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

    
risposta data 11.01.2015 - 17:53
fonte
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, nella peggiore delle ipotesi, al massimo un giorno - e lascia il progetto costruibile e tutti i test ancora in corso.

Se mantieni minime le modifiche individuali, non dovrebbe essere possibile per un refactoring interrompere la build per un lungo periodo. Il caso peggiore probabilmente sta cambiando i parametri in un metodo in un'interfaccia ampiamente utilizzata, ad es. per aggiungere un nuovo parametro. Ma i conseguenti cambiamenti da questo sono meccanici: 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 impiegare nemmeno un giorno per eseguire tali refactoring.

    
risposta data 11.01.2015 - 19:16
fonte
12

How can I avoid this kind of cascading refactor in the future?

Desiderio di progettazione dei desideri

L'obiettivo è un eccellente design e implementazione OO per la nuova funzionalità. Evitare il refactoring è anche un obiettivo.

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

Si noti tuttavia che la chiave qui è "aggiungi una funzionalità". Le nuove cose tendono a farci ignorare in gran parte l'attuale struttura del codice di base. Il nostro design di desiderio è indipendente. Ma abbiamo bisogno di altre due cose:

  • Refactor solo quanto basta per creare una cucitura necessaria per iniettare / implementare il codice della nuova funzione.
    • La resistenza al refactoring non dovrebbe guidare il nuovo design.
  • Scrivi una classe rivolta al cliente con un'API che mantiene la nuova funzione & codez esistenti ignoranti beatamente l'uno dell'altro.
    • Trasloca per ottenere oggetti, dati e risultati avanti e indietro. Il principio della conoscenza minima è dannato. Non faremo nulla di peggio di quello che il codice esistente già fa.

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 statico.

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

"Struttura" è tutto. La struttura è la realizzazione del Principio di Responsabilità Unica; design che facilita la funzionalità. Il codice rimarrà breve e semplice fino alla gerarchia delle classi. Il tempo per il nuovo design è costituito da test, ri-lavori ed evitando l'hacking attraverso la giungla del codice legacy.

Le classi di pensiero desiderabili si concentrano sul compito da svolgere. In generale, dimentica di estendere una classe esistente: stai solo inducendo di nuovo il refactoring a cascata e amp; avere a che fare con la "pesante" lezione "in testa.

Elimina qualsiasi residuo di questa nuova funzionalità dal codice esistente. Qui, la funzionalità di nuove funzionalità complete e ben incapsulate è più importante che evitare il refactoring.

    
risposta data 11.01.2015 - 22:53
fonte
9

Da (il meraviglioso) libro Funzionante in modo efficace con il codice legacy di Michael Feathers :

When you break dependencies in legacy code, you often have to suspend your sense of aesthetics a bit. Some dependencies break cleanly; others end up looking less than ideal from a design point of view. They are like the incision points in surgery: there might be a scar left in your code after your work, but everything beneath it can get better.

If later you can cover code around the point where you broke the dependencies, you can heal that scar, too.

    
risposta data 13.01.2015 - 05:25
fonte
6

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

La soluzione deve essere "non farlo, quindi" . Questo è quello che succede nei progetti reali. Un sacco di vecchie API hanno interfacce brutte o parametri abbandonati (sempre nulli) come risultato, o funzioni chiamate DoThisThing2 () che fa lo stesso di DoThisThing () con un elenco di parametri completamente diverso. Altri trucchi comuni includono la memorizzazione di informazioni in globals o puntatori con tag per farle passare di nascosto una grande porzione di framework. (Ad esempio, ho un progetto in cui metà dei buffer audio contengono solo un valore magico di 4 byte, perché è molto più semplice che cambiare il modo in cui una libreria ha richiamato i suoi codec audio.)

È difficile dare consigli specifici senza codice specifico.

    
risposta data 12.01.2015 - 18:04
fonte
3

Test automatici. Non è necessario essere uno zelot TDD, né è necessaria una copertura del 100%, ma sono i test automatici che ti consentono di apportare modifiche in modo sicuro. Inoltre, sembra che tu abbia un design con un accoppiamento molto alto; dovresti leggere i principi SOLID, che sono formulati specificamente per affrontare questo tipo di problema nella progettazione del software.

Raccomando anche questi libri.

  • Funzionante in modo efficace con il codice legacy , piume
  • Refactoring , Fowler
  • Software orientato agli oggetti in crescita, guidato da test , Freeman and Pryce
  • Pulisci codice , Martin
risposta data 11.01.2015 - 16:26
fonte
3

Is it just a symptom of my previous classes depending too tightly on each other?

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

How can I avoid this kind of cascading refactorings in the future?

Oltre a smettere di lavorare su un codice legacy, non posso aver paura. 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.

Questo metodo è denominato "Metodo Mikado" e funziona così:

  1. annota l'obiettivo che vuoi ottenere su un foglio di carta

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

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

  4. sulla carta prendi nota delle cose che devono essere cambiate per far funzionare la tua modifica attuale. Disegna le frecce, dall'attività corrente a quelle nuove.

  5. Ripristina le modifiche questo è il passaggio importante. È contro intuitivo e fisicamente fa male all'inizio, ma dal momento che hai appena provato una cosa semplice, non è poi così male.

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

  7. conferma la modifica, elimina l'attività sul foglio, scegli un'attività che non ha errori in uscita (nessuna dipendenza nota) e torna a 2.

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

    
risposta data 14.01.2015 - 12:25
fonte
2

Refactoring è una disciplina strutturata, distinta dal ripulire il codice come meglio credi. È necessario che siano scritti dei test unitari prima di iniziare e ogni passaggio dovrebbe consistere in una trasformazione specifica che non dovrebbe apportare modifiche alla funzionalità. I test unitari dovrebbero passare dopo ogni modifica.

Naturalmente, durante il processo di refactoring, scoprirai naturalmente le modifiche da applicare che potrebbero causare la rottura. 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 funzionare come prima, e i test unitari dovrebbero passare. Puoi contrassegnare lo shim di compatibilità come un'interfaccia deprecata e ripulirlo in un momento più adatto.

    
risposta data 12.01.2015 - 08:04
fonte
2

...I refactored the project to add the feature.

Come detto @Jules, Refactoring e aggiunta di funzionalità sono due cose molto diverse.

  • Il refactoring riguarda la modifica della struttura del programma senza alterarne il comportamento.
  • L'aggiunta di una funzione, d'altra parte, sta aumentando il suo comportamento.

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

I needed to make a minor interface change to accommodate it

Ecco dove le cose si complicano. Le interfacce sono intese come limiti per isolare l'implementazione dal modo in cui viene utilizzata. Non appena si toccano le interfacce, tutto dovrà essere modificato su entrambi i lati (implementandolo o usandolo). Questo può diffondersi lontano come lo hai sperimentato.

then the consuming class can't be implemented with its current interface in terms of the new one, so it needs a new interface as well.

Quell'unica interfaccia richiede che un cambiamento suoni bene ... che si diffonde a un altro implica che i cambiamenti si diffondano ulteriormente. Sembra che una qualche forma di input / dati richieda di scorrere lungo la catena. È così?

Il tuo discorso è molto astratto, quindi è difficile da capire. Un esempio sarebbe molto utile. Di solito, le interfacce dovrebbero essere abbastanza 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 le modifiche a cascata del codice sono precisamente le buone interfacce. ;)

    
risposta data 12.01.2015 - 18:30
fonte
-1

Penso che di solito non puoi, a meno che tu non sia disposto a mantenere le cose come sono. Tuttavia, in situazioni come la tua, penso che la cosa migliore sia informare il team e far sapere loro perché ci dovrebbe essere un po 'di refactoring per continuare lo sviluppo più sano. Non andrei solo a sistemare le cose da solo. Ne parlerei durante gli incontri di Scrum (supponendo che voi li abbiate), e affrontiamolo sistematicamente con altri sviluppatori.

    
risposta data 14.01.2015 - 04:09
fonte

Leggi altre domande sui tag