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 foo
są 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 b
się 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 a
drukuje oops
zamiast 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ą NEW
instrukcję, 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 x
jako 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 są 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).