Wie können Sie eine Sprache mit dynamischem Umfang sicher umgestalten?

13

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 foosind 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 bein 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 adruckt oopseher 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 NEWAnweisung, 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 xals 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.)

Senshin
quelle
@gnat Ich sehe nicht, wie diese Frage / ihre Antworten für diese Frage relevant sind.
Senshin
1
@gnat Wollen Sie damit sagen, dass die Antwort "andere Prozesse und andere Schwergewichte verwenden" lautet? Ich meine, das ist wahrscheinlich nicht falsch, aber es ist auch zu allgemein, um nicht besonders nützlich zu sein.
Senshin
2
Ehrlich gesagt, ich glaube nicht, dass es eine andere Antwort gibt, als "zu einer Sprache zu wechseln, in der Variablen tatsächlich Gültigkeitsregeln haben" oder "das Bastard-Stiefkind der ungarischen Notation zu verwenden, bei der jeder Variablen der Datei- und / oder Methodenname vorangestellt wird als Typ oder Art ". Das Problem, das Sie beschreiben, ist so schrecklich, dass ich mir keine gute Lösung vorstellen kann .
Ixrec
4
Zumindest kann man MUMPS keine falsche Werbung vorwerfen, weil sie nach einer schlimmen Krankheit benannt wurde.
Carson63000

Antworten:

4

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:

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

alle Verwendungen von c müssen neu geschrieben werden (was eine mechanische Aufgabe ist)

c(x)

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:

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

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:

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

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.

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

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.

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

hth.

Frage: Wie können wir unter Berücksichtigung dieser Gefahren Code in MUMPS oder einer anderen Sprache mit dynamischem Gültigkeitsbereich sicher umgestalten?

  • Vielleicht kann jemand anderes einen Hinweis geben.

Frage: Wie verringern wir die Risiken, die speziell mit der erhöhten Fragilität von Code mit dynamischem Gültigkeitsbereich beim Refactoring verbunden sind?

  • Schreiben Sie ein Programm, das die sicheren Refactorings für Sie erledigt.
  • Schreiben Sie ein Programm, das sichere Kandidaten / Erstkandidaten identifiziert.
thepacker
quelle
Ah, es gibt ein MUMPS-spezifisches Hindernis für den Versuch, den Refactoring-Prozess zu automatisieren: MUMPS verfügt weder über erstklassige Funktionen noch über Funktionszeiger oder ähnliche Begriffe. Dies bedeutet, dass jede große MUMPS-Codebasis unweigerlich eine Vielzahl von Möglichkeiten zur Auswertung bietet (in MUMPS genannt 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.
Senshin
Okay, halte meine Antwort für nicht angemessen. Ein Youtube-Video, von dem ich glaube, dass es ein einzigartiger Ansatz war, @ google scale umzugestalten. Sie verwendeten clang, um einen AST zu analysieren, und verwendeten dann ihre eigene Suchmaschine, um eine (sogar verborgene) Verwendung zu finden, um ihren Code umzugestalten. Dies könnte ein Weg sein, um jede Verwendung zu finden. Ich meine einen Analyse- und Suchansatz für Mumps-Code.
Thepacker
2

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.

Doc Brown
quelle
Dies ist ein guter Rat, auch wenn ich hinzufügen möchte, dass dies in MUMPS von Natur aus schwierig ist, da weder Zugriffsspezifizierer noch andere Verkapselungsmechanismen bekannt sind. Dies bedeutet, dass die APIs, die wir in unserer Codebasis angeben, im Grunde genommen nur Vorschläge für Verbraucher der sind Code , um die Funktionen , die sie sollten nennen. (Natürlich hat diese besondere Schwierigkeit nichts mit dynamischem Scoping zu tun. Ich mache mir nur eine Notiz darüber, um dies zu erwähnen.)
Senshin,
Nach dem Lesen dieses Artikels bin ich sicher, dass ich Sie nicht um Ihre Aufgabe beneide.
Doc Brown
0

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.

Mark Hurd
quelle