Come si esegue il refactoring sicuro in una lingua con ambito dinamico?


13

Per quelli di voi che hanno la fortuna di non lavorare in una lingua con portata dinamica, lasciate che vi dia un piccolo aggiornamento su come funziona. Immagina uno pseudo-linguaggio, chiamato "RUBELLA", che si comporta in questo modo:

function foo() {
    print(x); // not defined locally => uses whatever value `x` has in the calling context
    y = "tetanus";
}
function bar() {
    x = "measles";
    foo();
    print(y); // not defined locally, but set by the call to `foo()`
}
bar(); // prints "measles" followed by "tetanus"

Cioè, le variabili si propagano su e giù per lo stack di chiamate liberamente - tutte le variabili definite in foosono visibili (e mutabili da) al suo chiamante bar, e anche il contrario è vero. Ciò ha serie implicazioni per la rifattabilità del codice. Immagina di avere il seguente codice:

function a() { // defined in file A
    x = "qux";
    b();
}
function b() { // defined in file B
    c();
}
function c() { // defined in file C
    print(x);
}

Ora, le chiamate a a()verranno stampate qux. Ma poi, un giorno, deciderai che devi cambiare bun po '. Non conosci tutti i contesti chiamanti (alcuni dei quali potrebbero in effetti essere al di fuori della tua base di codice), ma ciò dovrebbe andare bene - le tue modifiche saranno completamente interne b, giusto? Quindi lo riscrivi in ​​questo modo:

function b() {
    x = "oops";
    c();
}

E potresti pensare di non aver cambiato nulla, dal momento che hai appena definito una variabile locale. Ma, in effetti, hai rotto a! Ora, astampa oopspiuttosto che qux.


Riportandolo fuori dal regno degli pseudo-linguaggi, questo è esattamente come si comporta MUMPS, sebbene con una sintassi diversa.

Le versioni moderne ("moderne") di MUMPS includono la cosiddetta NEWistruzione, che consente di evitare che le variabili passino da una chiamata a un chiamante. Quindi, nel primo esempio di cui sopra, se avessimo fatto NEW y = "tetanus"in foo(), poi print(y)in bar()sarebbe stampare nulla (in MUMPS, tutti i nomi indicano la stringa vuota se non esplicitamente impostato a qualcos'altro). Ma non c'è nulla che possa impedire la fuoriuscita di variabili da un chiamante a una chiamata: se function p() { NEW x = 3; q(); print(x); }, per quanto ne sappiamo, q()potremmo mutare x, nonostante non ricevessimo esplicitamente xcome parametro. Questa è ancora una brutta situazione, ma non così grave come probabilmente una volta.

Tenendo conto di questi pericoli, come possiamo eseguire il refactoring sicuro del codice in MUMPS o in qualsiasi altra lingua con ambito dinamico?

Esistono alcune buone pratiche ovvie per rendere più semplice il refactoring, come non usare mai variabili in una funzione diversa da quelle che si inizializza ( NEW) o vengono passate come parametro esplicito e documentare esplicitamente tutti i parametri che sono implicitamente passati dai chiamanti di una funzione. Ma in una base di codice ~ 10 8 -LOC vecchia di decenni , questi sono lussi che spesso non si hanno.

E, naturalmente, essenzialmente tutte le buone pratiche per il refactoring in lingue con ambito lessicale sono applicabili anche in lingue con ambito dinamico: test di scrittura e così via. La domanda, quindi, è questa: come possiamo mitigare i rischi specificamente associati alla maggiore fragilità del codice con ambito dinamico durante il refactoring?

(Si noti che mentre come si naviga e si refactifica il codice scritto in un linguaggio dinamico? Ha un titolo simile a questa domanda, è del tutto indipendente.)



@gnat Non vedo come quella domanda / le sue risposte siano rilevanti per questa domanda.
senshin,

1
@gnat Stai dicendo che la risposta è "usa processi diversi e altre cose pesanti"? Voglio dire, probabilmente non è sbagliato, ma è anche troppo generale al punto da non essere particolarmente utile.
senshin,

2
Onestamente, non credo che ci sia una risposta a questa se non "passare a una lingua in cui le variabili hanno regole di scoping" o "usare il bastard figliastro della notazione ungherese in cui ogni variabile è preceduta dal suo file e / o dal nome del metodo piuttosto di tipo o tipo ". Il problema che descrivi è così terribile che non riesco a immaginare una buona soluzione.
Ixrec,

4
Almeno non puoi accusare MUMPS di false pubblicità per essere stato chiamato dopo una brutta malattia.
Carson63000,

Risposte:


4

Wow.

Non conosco MUMPS come lingua, quindi non so se il mio commento si applica qui. In generale, è necessario rifattorizzare dall'interno verso l'esterno. Quei consumatori (lettori) dello stato globale (variabili globali) devono essere sottoposti a refactoring in metodi / funzioni / procedure utilizzando parametri. Il metodo c dovrebbe apparire così dopo il refactoring:

function c(c_scope_x) {
   print c(c_scope_x);
}

tutti gli usi di c devono essere riscritti (che è un compito meccanico)

c(x)

questo per isolare il codice "interno" dallo stato globale usando lo stato locale. Al termine, dovrai riscrivere b in:

function b() {
   x="oops"
   print c(x);
}

l'assegnazione x = "oops" è lì per mantenere gli effetti collaterali. Ora dobbiamo considerare b come inquinante dello stato globale. Se hai un solo elemento inquinato, considera questo refactoring:

function b() {
   x="oops"
   print c(x);
   return x;
}

end riscrive ogni utilizzo di b con x = b (). La funzione b deve usare solo metodi già ripuliti (potresti voler chiarire il nome della ro roaming) quando esegui questo refactoring. Dopodiché dovresti refactor b per non inquinare l'ambiente globale.

function b() {
   newvardefinition b_scoped_x="oops"
   print c_cleaned(b_scoped_x);
   return b_scoped_x;
}

rinominare b in b_cleaned. Immagino che dovrai giocarci un po 'per abituarti a quel refactoring. Sicuramente non tutti i metodi possono essere rifattorizzati da questo, ma dovrai iniziare dalle parti interne. Prova con Eclipse e java (metodi di estrazione) e "stato globale", ovvero i membri della classe per avere un'idea.

function x() {
  fifth_to_refactor();
  {
    forth_to_refactor()
    ....
    {
      second_to_refactor();
    }
    ...
    third_to_refactor();
  }
  first_to_refactor()
}

hth.

Domanda: Tenendo presente questi pericoli, come possiamo eseguire il refactoring sicuro del codice in MUMPS o in qualsiasi altra lingua con ambito dinamico?

  • Forse qualcun altro può dare un suggerimento.

Domanda: Come mitigare i rischi specificamente associati alla maggiore fragilità del codice con ambito dinamico durante il refactoring?

  • Scrivi un programma che esegua i refactoring sicuri per te.
  • Scrivi un programma che identifichi candidati / primi candidati sicuri.

Ah, c'è un ostacolo specifico di MUMPS al tentativo di automatizzare il processo di refactoring: MUMPS non ha funzioni di prima classe, né ha puntatori di funzioni o nozioni simili. Ciò significa che qualsiasi base di codice MUMPS di grandi dimensioni avrà inevitabilmente molti usi di eval (in MUMPS, chiamato EXECUTE), a volte anche su input utente sterilizzati - il che significa che può essere impossibile trovare e riscrivere staticamente tutti gli usi di una funzione.
senshin,

Ok, considera la mia risposta non adeguata. Un video di YouTube penso che refactoring @ google scale abbia fatto un approccio davvero unico. Usarono il clang per analizzare un AST e poi usarono il proprio motore di ricerca per trovare qualsiasi (anche uso nascosto) per riformattare il loro codice. Questo potrebbe essere un modo per trovare ogni utilizzo. Intendo un approccio di analisi e ricerca sul codice di parotite.
thepacker,

2

Immagino che il tuo colpo migliore sia quello di portare la base di codice completa sotto il tuo controllo e assicurarti di avere una panoramica dei moduli e delle loro dipendenze.

Quindi almeno hai la possibilità di fare ricerche globali e hai la possibilità di aggiungere test di regressione per le parti del sistema in cui ti aspetti un impatto da una modifica del codice.

Se non vedi la possibilità di realizzare il primo, il mio miglior consiglio è: non rifattorizzare i moduli che vengono riutilizzati da altri moduli o per i quali non sai che gli altri fanno affidamento su di essi . In qualsiasi base di codice di dimensioni ragionevoli le probabilità sono alte che puoi trovare moduli da cui nessun altro modulo dipende. Quindi se hai una mod A dipendente da B, ma non viceversa, e nessun altro modulo dipende da A, anche in un linguaggio con ambito dinamico, puoi apportare modifiche ad A senza interrompere B o altri moduli.

Questo ti dà la possibilità di sostituire la dipendenza da A a B con una dipendenza da A a B2, dove B2 è una versione sanificata e riscritta di B. B2 dovrebbe essere una nuova scritta con le regole in mente che hai menzionato sopra per rendere il codice più evolvibile e più facile da refactoring.


Questo è un buon consiglio, anche se aggiungerò a parte che ciò è intrinsecamente difficile in MUMPS poiché non esiste una nozione di specificatori di accesso né alcun altro meccanismo di incapsulamento, il che significa che le API che specifichiamo nella nostra base di codice sono effettivamente solo suggerimenti per i consumatori del codice su quali funzioni dovrebbero chiamare. (Naturalmente, questa particolare difficoltà non è correlata allo scoping dinamico; sto solo prendendo atto di questo come punto di interesse.)
senshin

Dopo aver letto questo articolo , sono sicuro di non invidiarti per il tuo compito.
Doc Brown,

0

Per affermare l'ovvio: come eseguire il refactoring qui? Procedere con molta attenzione.

(Come l'hai descritto, lo sviluppo e il mantenimento della base di codice esistente dovrebbe essere abbastanza difficile, per non parlare del tentativo di refactoring.)

Credo che applicherei retroattivamente un approccio basato sui test qui. Ciò comporterebbe la scrittura di una serie di test per garantire che la funzionalità corrente rimanga attiva quando si avvia il refactoring, in primo luogo solo per semplificare i test. (Sì, mi aspetto un problema con pollo e uova qui, a meno che il tuo codice non sia già abbastanza modulare da testare senza modificarlo affatto.)

Quindi puoi procedere con altri refactoring, controllando che non hai superato alcun test mentre procedi.

Infine, puoi iniziare a scrivere test che prevedono nuove funzionalità e quindi scrivere il codice per far funzionare quei test.

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.