Jak bezpiecznie refaktoryzować w języku z dynamicznym zakresem?

13

Dla tych z was, którzy mają szczęście nie pracować w języku z dynamicznym zakresem, pozwólcie, że trochę odświeżę, jak to działa. Wyobraź sobie pseudo-język o nazwie „RUBELLA”, który zachowuje się tak:

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"

Oznacza to, że zmienne swobodnie propagują się w górę i w dół stosu wywołań - wszystkie zmienne zdefiniowane w foosą widoczne dla jego wywołującego (i mogą być przez niego mutowane) bar, a odwrotność jest również prawdą. Ma to poważne konsekwencje dla refaktowalności kodu. Wyobraź sobie, że masz następujący kod:

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

Teraz wywołania a()zostaną wydrukowane qux. Ale pewnego dnia zdecydujesz, że musisz bsię trochę zmienić . Nie znasz wszystkich kontekstów wywoływania (niektóre z nich mogą faktycznie znajdować się poza bazą kodu), ale to powinno być w porządku - twoje zmiany będą całkowicie wewnętrzne b, prawda? Więc przepisujesz to w ten sposób:

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

I możesz myśleć, że nic nie zmieniłeś, ponieważ właśnie zdefiniowałeś zmienną lokalną. Ale tak naprawdę złamałeś a! Teraz adrukuje oopszamiast qux.


Wydobywając to z królestwa pseudojęzyków, dokładnie tak zachowuje się MUMPS, aczkolwiek z inną składnią.

Nowoczesne („nowoczesne”) wersje MUMPS zawierają tak zwaną NEWinstrukcję, która pozwala zapobiegać wyciekaniu zmiennych z odbiorcy do dzwoniącego. Tak więc w pierwszym przykładzie powyżej, gdybyśmy zrobili NEW y = "tetanus"w foo(), a następnie print(y)w bar()wydrukuje nic (w śwince, wszystkie nazwy wskazują na pusty ciąg, chyba że wyraźnie ustawiony na coś innego). Ale nic nie stoi na przeszkodzie, aby zmienne wyciekły z dzwoniącego do odbierającego: gdybyśmy function p() { NEW x = 3; q(); print(x); }, o ile wiemy, q()mogliby mutować x, mimo że nie otrzymywali jawnie xjako parametru. Wciąż jest to zła sytuacja, ale nie tak zła, jak kiedyś.

Biorąc pod uwagę te niebezpieczeństwa, w jaki sposób możemy bezpiecznie refaktoryzować kod w MUMPS lub innym języku z dynamicznym określaniem zakresu?

Istnieją pewne oczywiste dobre praktyki ułatwiające refaktoryzację, takie jak nigdy nie używanie zmiennych w funkcji innej niż te, które inicjujesz ( NEW) samodzielnie lub są przekazywane jako jawny parametr, i jawne dokumentowanie wszelkich parametrów, które niejawnie przekazywane przez funkcje wywołujące funkcję. Ale w dziesięcioletniej bazie kodu ~ 10 8- LOC są to luksusy, których często nie ma.

I, oczywiście, zasadniczo wszystkie dobre praktyki dotyczące refaktoryzacji w językach o zakresie leksykalnym mają również zastosowanie w językach o zakresie dynamicznym - testy zapisu i tak dalej. Pytanie zatem brzmi: w jaki sposób ograniczamy ryzyko związane ze zwiększoną niestabilnością dynamicznie zakodowanego kodu podczas refaktoryzacji?

(Pamiętaj, że chociaż sposób nawigacji i zmiany kodu napisanego w dynamicznym języku? Ma podobny tytuł do tego pytania, jest całkowicie niezwiązany).

senshin
źródło
@gnat Nie rozumiem, w jaki sposób to pytanie / jego odpowiedzi są istotne dla tego pytania.
senshin
1
@gnat Czy mówisz, że odpowiedź brzmi „użyj różnych procesów i innych ciężkich rzeczy”? Mam na myśli, że to chyba nie jest źle, ale jest też zbyt ogólne, że nie jest szczególnie użyteczne.
senshin
2
Szczerze mówiąc, nie sądzę, aby istniała odpowiedź na to inaczej niż „przejść na język, w którym zmienne faktycznie mają reguły określania zakresu”, lub „użyć drania pasierba węgierskiej notacji, w którym każda zmienna jest poprzedzona nazwą pliku i / lub metody niż typ lub rodzaj ". Opisany przez ciebie problem jest tak straszny, że nie wyobrażam sobie dobrego rozwiązania.
Ixrec
4
Przynajmniej nie możesz oskarżyć MUMPSA o fałszywe reklamy za to, że został nazwany po paskudnej chorobie.
Carson63000,

Odpowiedzi:

4

Łał.

Nie znam MUMPS jako języka, więc nie wiem, czy mój komentarz dotyczy tutaj. Ogólnie rzecz biorąc - Musisz refaktoryzować od wewnątrz. Tych konsumentów (czytników) stanu globalnego (zmienne globalne) należy przekształcić w metody / funkcje / procedury wykorzystujące parametry. Metoda c powinna wyglądać tak po refaktoryzacji:

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

wszystkie zastosowania c należy przepisać na (co jest zadaniem mechanicznym)

c(x)

służy to odizolowaniu „wewnętrznego” kodu od stanu globalnego przy użyciu stanu lokalnego. Kiedy to zrobisz, będziesz musiał przepisać b na:

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

przypisanie x = „ups” służy do zachowania efektów ubocznych. Teraz musimy uznać b za zanieczyszczenie państwa globalnego. Jeśli masz tylko jeden zanieczyszczony element, rozważ to refaktoryzacja:

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

koniec przepisuj każde użycie b za pomocą x = b (). Funkcja b musi używać tylko metod, które zostały już wyczyszczone (możesz chcieć zmienić nazwę ro, aby to wyjaśnić) podczas wykonywania tego refaktoryzacji. Następnie powinieneś refaktoryzować b, aby nie zanieczyszczać środowiska globalnego.

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

zmień nazwę b na b_cleaned. Myślę, że będziesz musiał trochę z tym pograć, aby przyzwyczaić się do tego refaktoryzacji. Oczywiście nie każda metoda może zostać zrefaktoryzowana, ale będziesz musiał zacząć od wewnętrznych części. Wypróbuj to za pomocą Eclipse i java (metody wyodrębniania) i „klasy globalnej”, czyli członków klasy, aby uzyskać pomysł.

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

hth.

Pytanie: Mając na uwadze te niebezpieczeństwa, w jaki sposób możemy bezpiecznie refaktoryzować kod w MUMPS lub innym języku z dynamicznym określaniem zakresu?

  • Może ktoś inny może podpowiedzieć.

Pytanie: W jaki sposób ograniczamy ryzyko związane ze zwiększoną niestabilnością kodu o dynamicznym zakresie podczas refaktoryzacji?

  • Napisz program, który wykona dla ciebie bezpieczne refaktoryzacje.
  • Napisz program identyfikujący bezpiecznych kandydatów / pierwszych kandydatów.
pakujący
źródło
Ach, istnieje jedna przeszkoda specyficzna dla MUMPS, która próbuje zautomatyzować proces refaktoryzacji: MUMPS nie ma funkcji pierwszej klasy, nie ma wskaźników funkcji ani żadnego podobnego pojęcia. Co oznacza, że ​​jakakolwiek duża baza kodu MUMPS nieuchronnie będzie miała wiele zastosowań eval (w MUMPS, wywoływany EXECUTE), czasami nawet na oczyszczonych danych wejściowych użytkownika - co oznacza, że ​​niemożliwe będzie statyczne znalezienie i przepisanie wszystkich zastosowań funkcji.
senshin
Ok, uważaj moją odpowiedź za nieodpowiednią. Myślę, że film na youtube refactoring @ google przyniósł bardzo unikalne podejście. Użyli clang do parsowania AST, a następnie użyli własnej wyszukiwarki, aby znaleźć dowolne (nawet ukryte użycie) w celu zmiany kodu. To może być sposób na znalezienie każdego zastosowania. Mam na myśli analizę składni i wyszukiwania kodu świnki.
pakujący
2

Wydaje mi się, że najlepszym rozwiązaniem jest oddanie pełnej kontroli nad kodem i upewnienie się, że masz przegląd modułów i ich zależności.

Przynajmniej masz szansę na wyszukiwanie globalne i możesz dodać testy regresji dla części systemu, w których spodziewany jest wpływ zmiany kodu.

Jeśli nie widzisz szansy na osiągnięcie pierwszego, moja najlepsza rada jest następująca: nie refaktoryzuj modułów, które są ponownie używane przez inne moduły lub dla których nie wiesz, że inni na nich polegają . W każdej bazie kodu o rozsądnych rozmiarach są duże szanse na znalezienie modułów, od których nie zależy żaden inny moduł. Więc jeśli masz mod A zależny od B, ale nie odwrotnie, i żaden inny moduł nie zależy od A, nawet w języku dynamicznie skalowanym, możesz wprowadzić zmiany w A bez rozbijania B lub innych modułów.

Daje to szansę na zastąpienie zależności A do B zależnością A do B2, gdzie B2 to zdezynfekowana, przepisana wersja B. B2 powinien być nowo napisany z uwzględnieniem reguł, o których wspomniałeś powyżej, aby utworzyć kod bardziej ewolucyjny i łatwiejszy do refaktoryzacji.

Doktor Brown
źródło
To dobra rada, chociaż dodam na marginesie, że jest to z natury trudne w MUMPS, ponieważ nie ma pojęcia o specyfikacjach dostępu ani żadnym innym mechanizmie enkapsulacji, co oznacza, że ​​interfejsy API, które określamy w naszej bazie kodu, są w rzeczywistości tylko sugestiami dla konsumentów kod określający, które funkcje powinny wywołać. (Oczywiście ta szczególna trudność nie ma związku z dynamicznym określaniem zakresu; odnotowuję to jako punkt zainteresowania).
senshin 14.09.15
Po przeczytaniu tego artykułu jestem pewien, że nie zazdroszczę ci za twoje zadanie.
Doc Brown,
0

Stwierdzić oczywiste: jak zrobić tutaj refaktoryzację? Postępuj bardzo ostrożnie.

(Jak już to opisałeś, opracowanie i utrzymanie istniejącej bazy kodu powinno być wystarczająco trudne, nie mówiąc już o próbie jej refaktoryzacji).

Wierzę, że zastosowałbym tutaj podejście testowe z mocą wsteczną. Wymagałoby to napisania zestawu testów, aby upewnić się, że bieżąca funkcjonalność będzie działać po rozpoczęciu refaktoryzacji, po pierwsze, aby ułatwić testowanie. (Tak, spodziewam się tutaj problemu z kurczakiem i jajkami, chyba że kod jest już wystarczająco modułowy, aby go przetestować bez zmiany).

Następnie możesz przystąpić do innego refaktoryzacji, sprawdzając, czy nie przełamałeś żadnych testów.

Wreszcie możesz rozpocząć pisanie testów, które oczekują nowej funkcjonalności, a następnie napisać kod, aby testy te działały.

Mark Hurd
źródło