Für diejenigen von Ihnen, die das Glück haben, nicht in einer Sprache mit dynamischem Umfang zu arbeiten, möchte ich Ihnen eine kleine Auffrischung darüber geben, wie das funktioniert. Stellen Sie sich eine Pseudosprache namens "RUBELLA" vor, die sich wie folgt verhält:
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"
Das heißt, Variablen breiten sich frei im Aufrufstapel auf und ab aus - alle in definierten Variablen foo
sind für ihren Aufrufer sichtbar (und für ihn veränderbar) bar
, und das Gegenteil ist auch der Fall. Dies hat schwerwiegende Auswirkungen auf die Code-Refactorability. Stellen Sie sich vor, Sie haben folgenden Code:
function a() { // defined in file A
x = "qux";
b();
}
function b() { // defined in file B
c();
}
function c() { // defined in file C
print(x);
}
Jetzt werden Anrufe an a()
gedruckt qux
. Aber eines Tages entscheiden Sie, dass Sie b
ein wenig ändern müssen . Sie kennen nicht alle aufrufenden Kontexte (von denen einige tatsächlich außerhalb Ihrer Codebasis liegen können), aber das sollte in Ordnung sein - Ihre Änderungen werden vollständig intern sein b
, oder? Also schreiben Sie es wie folgt um:
function b() {
x = "oops";
c();
}
Und Sie denken vielleicht, dass Sie nichts geändert haben, da Sie gerade eine lokale Variable definiert haben. Aber in der Tat, du bist kaputt a
! Jetzt a
druckt oops
eher als qux
.
Dies wird aus dem Bereich der Pseudosprachen zurückgebracht, und genau so verhält sich MUMPS, wenn auch mit unterschiedlicher Syntax.
Moderne ("moderne") Versionen von MUMPS enthalten die sogenannte NEW
Anweisung, mit der Sie verhindern können, dass Variablen von einem Angerufenen zu einem Aufrufer gelangen. So im ersten Beispiel oben, wenn wir getan hatten NEW y = "tetanus"
in foo()
, dann print(y)
in bar()
würde nichts drucken (in Mumps, alle Namen auf die leere Zeichenfolge verweisen , sofern nicht ausdrücklich auf etwas anderes). Es gibt jedoch nichts, was verhindern könnte, dass Variablen von einem Aufrufer zu einem Angerufenen gelangen: Wenn wir function p() { NEW x = 3; q(); print(x); }
, soweit wir wissen, q()
mutieren könnten x
, obwohl wir nicht explizit x
als Parameter empfangen . Dies ist immer noch eine schlechte Situation, aber nicht so schlimm, wie es wahrscheinlich früher war.
Wie können wir unter Berücksichtigung dieser Gefahren Code in MUMPS oder einer anderen Sprache mit dynamischem Gültigkeitsbereich sicher umgestalten?
Es gibt einige gute Methoden, um das Refactoring zu vereinfachen, z. B. die Verwendung von Variablen in einer Funktion, die nicht von Ihnen selbst initialisiert ( NEW
) oder als expliziter Parameter übergeben wurde, und die explizite Dokumentation von Parametern, die implizit von Funktionsaufrufern übergeben wurden. Aber in einer jahrzehntealten Codebasis von ~ 10 8 -LOC handelt es sich um Luxus, den man oft nicht hat.
Und natürlich sind im Wesentlichen alle bewährten Methoden für die Umgestaltung in Sprachen mit lexikalischem Geltungsbereich auch in Sprachen mit dynamischem Geltungsbereich anwendbar - Schreibtests und so weiter. Die Frage lautet also: Wie verringern wir die Risiken, die speziell mit der erhöhten Fragilität von Code mit dynamischem Gültigkeitsbereich beim Refactoring verbunden sind?
(Beachten Sie, dass in einer dynamischen Sprache verfasster Code zum Navigieren und Umgestalten zwar einen ähnlichen Titel wie diese Frage hat, aber in keiner Beziehung steht.)
quelle
Antworten:
Beeindruckend.
Ich kenne MUMPS nicht als Sprache, daher weiß ich nicht, ob mein Kommentar hier zutrifft. Im Allgemeinen - Sie müssen von innen nach außen umgestalten. Diese Verbraucher (Leser) des globalen Zustands (globale Variablen) müssen mithilfe von Parametern in Methoden / Funktionen / Prozeduren umgestaltet werden. Die Methode c sollte nach dem Refactoring so aussehen:
alle Verwendungen von c müssen neu geschrieben werden (was eine mechanische Aufgabe ist)
Dies dient dazu, den "inneren" Code mithilfe des lokalen Status vom globalen Status zu isolieren. Wenn Sie damit fertig sind, müssen Sie b neu schreiben in:
Die Zuweisung x = "oops" dient dazu, die Nebenwirkungen zu vermeiden. Jetzt müssen wir b als Verschmutzung des globalen Staates betrachten. Wenn Sie nur ein verschmutztes Element haben, berücksichtigen Sie dieses Refactoring:
end Schreibe jede Verwendung von b mit x = b () um. Funktion b darf bei diesem Refactoring nur Methoden verwenden, die bereits bereinigt wurden (möglicherweise möchten Sie sie umbenennen, um dies zu verdeutlichen). Danach sollten Sie b umgestalten, um die globale Umwelt nicht zu verschmutzen.
benenne b in b_cleaned um. Ich nehme an, Sie müssen ein bisschen damit spielen, um sich an dieses Refactoring zu gewöhnen. Natürlich kann nicht jede Methode dadurch überarbeitet werden, aber Sie müssen von den inneren Teilen ausgehen. Versuchen Sie das mit Eclipse und Java (Extraktionsmethoden) und "global state" aka Klassenmitgliedern, um sich ein Bild zu machen.
hth.
Frage: Wie können wir unter Berücksichtigung dieser Gefahren Code in MUMPS oder einer anderen Sprache mit dynamischem Gültigkeitsbereich sicher umgestalten?
Frage: Wie verringern wir die Risiken, die speziell mit der erhöhten Fragilität von Code mit dynamischem Gültigkeitsbereich beim Refactoring verbunden sind?
quelle
EXECUTE
), manchmal sogar für bereinigte Benutzereingaben - was bedeutet, dass es unmöglich sein kann, alle Verwendungen einer Funktion statisch zu finden und neu zu schreiben.Ich schätze, Sie sollten die vollständige Codebasis unter Ihre Kontrolle bringen und sich einen Überblick über die Module und ihre Abhängigkeiten verschaffen.
Sie haben also zumindest die Möglichkeit, globale Suchen durchzuführen und Regressionstests für die Teile des Systems hinzuzufügen, bei denen Sie eine Auswirkung einer Codeänderung erwarten.
Wenn Sie keine Chance sehen, das erste zu erreichen, ist mein bester Rat: Refaktorieren Sie keine Module, die von anderen Modulen wiederverwendet werden oder von denen Sie nicht wissen, dass andere sich auf sie verlassen . In jeder Codebasis einer vernünftigen Größe sind die Chancen hoch, dass Sie Module finden, von denen kein anderes Modul abhängt. Wenn Sie also einen Mod A haben, der von B abhängt, aber nicht umgekehrt, und kein anderes Modul von A abhängt, auch nicht in einer Sprache mit dynamischem Gültigkeitsbereich, können Sie Änderungen an A vornehmen, ohne B oder andere Module zu unterbrechen.
Dies gibt Ihnen die Möglichkeit, die Abhängigkeit von A zu B durch eine Abhängigkeit von A zu B2 zu ersetzen, wobei B2 eine bereinigte, umgeschriebene Version von B ist. B2 sollte unter Berücksichtigung der oben genannten Regeln neu geschrieben werden, um den Code zu erstellen entwicklungsfähiger und leichter umzugestalten.
quelle
Um das Offensichtliche zu formulieren : Wie kann man hier umgestalten? Gehen Sie sehr vorsichtig vor.
(Wie Sie beschrieben haben, sollte die Entwicklung und Pflege der vorhandenen Codebasis schwierig genug sein, geschweige denn der Versuch, sie zu überarbeiten.)
Ich glaube, ich würde hier nachträglich einen testgetriebenen Ansatz anwenden. Dazu müsste eine Reihe von Tests geschrieben werden, um sicherzustellen, dass die aktuelle Funktionalität beim Beginn des Refactorings weiterhin funktioniert. Dies dient lediglich der Vereinfachung der Tests. (Ja, ich erwarte hier ein Henne-Ei-Problem, es sei denn, Ihr Code ist bereits modular genug, um zu testen, ohne ihn überhaupt zu ändern.)
Anschließend können Sie mit anderen Umgestaltungen fortfahren und dabei sicherstellen, dass Sie keine Tests unterbrochen haben.
Schließlich können Sie Tests schreiben, die neue Funktionen erwarten, und dann den Code schreiben, damit diese Tests funktionieren.
quelle