Domanda

Sono stato abituato a fare alcuni refactoring introducendo errori di compilazione. Ad esempio, se voglio rimuovere un campo dalla mia classe e renderlo un parametro per alcuni metodi, di solito rimuovo prima il campo, che causa un errore di compilazione per la classe. Quindi introdurrei il parametro nei miei metodi, che spezzerebbe i chiamanti. E così via. Questo di solito mi ha dato un senso di sicurezza. In realtà non ho ancora letto alcun libro sul refactoring, ma pensavo che questo fosse un modo relativamente sicuro di farlo. Ma mi chiedo, è davvero sicuro? O è un brutto modo di fare le cose?

È stato utile?

Soluzione

Questa è una tecnica comune e utile per i linguaggi compilati staticamente. La versione generale di ciò che stai facendo potrebbe essere dichiarata come segue:

  

Quando si apporta una modifica a un modulo che potrebbe invalidare alcuni usi nei client di quel modulo, apportare la modifica iniziale in modo da causare un errore di compilazione.

Esistono vari corollari:

  • Se il significato di un metodo, una funzione o una procedura cambia e anche il tipo non cambia, allora cambia il nome. (Dopo aver esaminato e corretto tutti gli usi, probabilmente cambierai di nuovo il nome.)

  • Se si aggiunge un nuovo caso a un tipo di dati o un nuovo valore letterale a un'enumerazione, modificare i nomi di tutti i costruttori di tipi di dati esistenti o valori letterali di enumerazione. (Oppure, se sei abbastanza fortunato da avere un compilatore che controlla se l'analisi del caso è esaustiva, ci sono modi più semplici.)

  • Se stai lavorando in una lingua con sovraccarico, non cambia semplicemente una variante o aggiungi una nuova variante. Rischi di avere un sovraccarico che si risolve silenziosamente in un modo diverso. Se usi il sovraccarico, è abbastanza difficile far funzionare il compilatore nel modo che speri. L'unico modo che conosco per gestire il sovraccarico è ragionare a livello globale su tutti gli usi. Se il tuo IDE non ti aiuta, devi cambiare i nomi di tutte varianti sovraccaricate. Sgradevole.

Quello che stai veramente facendo è usare il compilatore per aiutarti a esaminare tutti i punti del codice che potresti dover cambiare.

Altri suggerimenti

Non faccio mai affidamento sulla semplice compilazione durante il refactoring, il codice può essere compilato ma i bug potrebbero essere stati introdotti.

Penso che solo scrivere alcuni test unitari per i metodi o le classi che si desidera refactoring sarà il migliore, quindi eseguendo il test dopo il refactoring sarai sicuro che non sono stati introdotti bug.

Non sto dicendo andare a Test Driven Development, basta scrivere i test unitari per ottenere la sicurezza necessaria per il refactoring.

Non vedo alcun problema. È sicuro e fintanto che non esegui modifiche prima della compilazione, non ha alcun effetto a lungo termine. Inoltre, Resharper e VS hanno strumenti che rendono il processo un po 'più semplice per te.

In TDD si utilizza un processo simile nell'altra direzione: si scrive codice che potrebbe non avere i metodi definiti, il che ne impedisce la compilazione, quindi si scrive abbastanza codice per la compilazione (quindi si superano i test e così via .. .)

Quando sei pronto a leggere libri sull'argomento, ti consiglio di Michael Feather " Lavorare efficacemente con il codice legacy " ;. ( Aggiunto da un non autore: anche il libro classico di Fowler " Refactoring " - e il Refactoring può essere utile. )

Parla di identificare le caratteristiche del codice che stai lavorando prima di apportare una modifica e di fare ciò che chiama refactoring scratch. Questo è refettoring per trovare le caratteristiche del codice e poi buttare via i risultati.

Quello che stai facendo è usare il compilatore come auto-test. Verificherà la compilazione del codice, ma non se il comportamento è cambiato a causa del refactoring o se ci sono stati effetti collaterali.

Consideralo

class myClass {
     void megaMethod() 
     {
         int x,y,z;
         //lots of lines of code
         z = mysideEffect(x)+y;
         //lots more lines of code 
         a = b + c;
     }
}

potresti rifatturare l'aggiunta

class myClass {
     void megaMethod() 
     {
         int a,b,c,x,y,z;
         //lots of lines of code
         z = addition(x,y);
         //lots more lines of code
         a = addition(b,c);  
     }

     int addition(int a, b)
     {
          return mysideaffect(a)+b;
     }
}

e questo funzionerebbe, ma il secondo additon sarebbe sbagliato poiché invocava il metodo. Ulteriori test sarebbero necessari oltre alla semplice compilazione.

È abbastanza facile pensare ad esempi in cui il refactoring per errori del compilatore fallisce silenziosamente e produce risultati non intenzionali.
Alcuni casi che mi vengono in mente: (suppongo che stiamo parlando di C ++)

  • Modifica degli argomenti in una funzione in cui esistono altri sovraccarichi con parametri predefiniti. Dopo il refactoring, la migliore corrispondenza per gli argomenti potrebbe non essere quella che ti aspetti.
  • Classi che hanno operatori di cast o costruttori di argomenti singoli non espliciti. La modifica, l'aggiunta, la rimozione o la modifica di argomenti in uno di questi può cambiare le migliori corrispondenze che vengono chiamate, a seconda della costellazione in questione.
  • Cambiando una funzione virtuale e senza cambiare la classe base (o cambiando la classe base in modo diverso) si tradurrà in chiamate dirette alla classe base.

basarsi sugli errori del compilatore dovrebbe essere usato solo se si è assolutamente certi che il compilatore prenderà ogni singola modifica che deve essere fatta. Tendo quasi sempre ad essere sospetti su questo.

Vorrei solo aggiungere a tutta la saggezza qui intorno che c'è un altro caso in cui questo potrebbe non essere sicuro. Riflessione. Questo riguarda ambienti come .NET e Java (e anche altri, ovviamente). Il codice verrà compilato, ma si verificherebbero comunque errori di runtime quando Reflection tenterà di accedere a variabili inesistenti. Ad esempio, questo potrebbe essere abbastanza comune se si utilizza un ORM come Hibernate e si dimentica di aggiornare i file XML di mappatura.

Potrebbe essere un po 'più sicuro cercare nell'intero file di codice il nome della variabile / metodo specifico. Certo, potrebbe far apparire molti falsi positivi, quindi non è una soluzione universale; e potresti usare anche la concatenazione di stringhe nella tua riflessione, il che renderebbe anche questo inutile. Ma è almeno un passo in più verso la sicurezza.

Non penso che esista un metodo sicuro al 100%, oltre a esaminare tutto il codice manualmente.

Penso sia un modo abbastanza comune, in quanto trova tutti i riferimenti a quella cosa specifica. Tuttavia, gli IDE moderni come Visual Studio hanno la funzione Trova tutti i riferimenti che lo rende superfluo.

Tuttavia, ci sono alcuni svantaggi di questo approccio. Per progetti di grandi dimensioni, potrebbe essere necessario molto tempo per compilare l'applicazione. Inoltre, non farlo per molto tempo (voglio dire, rimetti le cose a lavorare il prima possibile) e non farlo per più di una cosa alla volta poiché potresti dimenticare il modo corretto in cui hai sistemato le cose la prima volta.

È un modo e non può esserci alcuna affermazione definitiva sul fatto che sia sicuro o insicuro senza sapere come appare il codice che si sta refactoring e le scelte che si fanno.

Se funziona per te, non c'è motivo di cambiare solo per motivi di cambiamento, ma quando hai il tempo di leggerlo, le risorse qui potrebbero darti nuove idee che potresti voler esplorare nel tempo.

http://www.refactoring.com/

Un'altra opzione che potresti prendere in considerazione, se stai usando una delle lingue dotnet, è quella di contrassegnare il "vecchio" metodo con l'attributo Obsoleto, che introdurrà tutti gli avvisi del compilatore, ma lascerà comunque il codice richiamabile nel caso in cui ci sia codice al di fuori del tuo controllo (se stai scrivendo un'API o se non usi l'opzione rigorosa in VB.Net, per esempio). È possibile che allegramente refactoring, avendo la versione obsoleta chiama quella nuova; come esempio:

    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    public int Login()
    {
        /* do stuff */
    }

diventa:

    [ObsoleteAttribute()]
    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    [ObsoleteAttribute("Replaced by Login(username, password)")]
    public int Login()
    {
        Login(Username, Pasword);
    }

    public int Login(string username, string password)
    {
        /* do stuff */
    }

È così che tendo a farlo, comunque ...

Questo è un approccio comune, ma i risultati possono variare a seconda che la tua lingua sia statica o dinamica.

In un linguaggio tipicamente statico questo approccio ha un certo senso poiché eventuali discrepanze introdotte verranno colte al momento della compilazione. Tuttavia, i linguaggi dinamici incontrano spesso questi problemi solo in fase di esecuzione. Questi problemi non verrebbero colti dal compilatore, ma piuttosto dalla tua suite di test; supponendo che tu ne abbia scritto uno.

Ho l'impressione che tu stia lavorando con un linguaggio statico come C # o Java, quindi continua con questo approccio fino a quando non incontri qualche tipo di grave problema che dice che dovresti fare diversamente.

Faccio refactoring soliti, ma faccio ancora refactoring introducendo errori di compilatore. Di solito li faccio quando i cambiamenti non sono così semplici e quando questo refactoring non è un vero refactoring (sto cambiando funzionalità). Questi errori del compilatore mi stanno dando dei punti di cui ho bisogno per dare un'occhiata e apportare alcune modifiche più complicate rispetto al cambio di nome o parametro.

Sembra simile a un metodo assolutamente standard utilizzato nello sviluppo test-driven: scrivere il test facendo riferimento a una classe inesistente, in modo che il primo passo per effettuare il test test sia aggiungere la classe, quindi i metodi e così via . Vedi il libro di Beck per esempi esaustivi su Java.

Il tuo metodo di refactoring sembra pericoloso perché non hai alcun test per la sicurezza (o almeno non dici che ne hai). È possibile creare un codice di compilazione che non fa effettivamente ciò che si desidera o che interrompe altre parti dell'applicazione.

Ti suggerirei di aggiungere una semplice regola alla tua pratica: apporta modifiche non compilanti solo nel codice unit test . In questo modo sei sicuro di avere almeno un test locale per ogni modifica e stai registrando l'intento della modifica nel tuo test prima di eseguirlo.

A proposito, Eclipse fa questo "fallire, stub, scrivere " metodo assurdamente facile in Java: ogni oggetto inesistente è contrassegnato per te, e Ctrl-1 più una scelta di menu dice a Eclipse di scrivere uno stub (compilabile) per te! Sarei interessato a sapere se altre lingue e IDE forniscono un supporto simile.

È " sicuro " nel senso che in un linguaggio sufficientemente controllato in fase di compilazione, ti costringe ad aggiornare tutti i riferimenti attivi alla cosa che hai cambiato.

Può ancora andare storto se si dispone di codice compilato in modo condizionale, ad esempio se si è utilizzato il preprocessore C / C ++. Quindi assicurati di ricostruire in tutte le configurazioni possibili e su tutte le piattaforme, se applicabile.

Non rimuove la necessità di testare le modifiche. Se hai aggiunto un parametro a una funzione, il compilatore non può dirti se il valore fornito quando hai aggiornato ciascun sito di chiamata per quella funzione era il valore corretto. Se hai rimosso un parametro, puoi comunque commettere errori, ad esempio cambiare:

void foo(int a, int b);

a

void foo(int a);

Quindi cambia la chiamata da:

foo(1,2);

a:

foo(2);

Questo si compila bene, ma è sbagliato.

Personalmente, utilizzo gli errori di compilazione (e collegamento) come metodo di ricerca del codice per riferimenti attivi alla funzione che sto cambiando. Ma devi tenere presente che questo è solo un dispositivo per risparmiare lavoro. Non garantisce che il codice risultante sia corretto.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top