Comment refactoriser en toute sécurité dans une langue à portée dynamique?

13

Pour ceux d'entre vous qui ont la chance de ne pas travailler dans une langue à portée dynamique, permettez-moi de vous donner un petit rappel sur la façon dont cela fonctionne. Imaginez un pseudo-langage, appelé "RUBELLA", qui se comporte comme ceci:

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"

Autrement dit, les variables se propagent librement de haut en bas dans la pile des appels - toutes les variables définies dans foosont visibles (et modifiables par) son appelant bar, et l'inverse est également vrai. Cela a de sérieuses implications pour la refactorisation du code. Imaginez que vous disposez du code suivant:

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

Maintenant, les appels à a()seront imprimés qux. Mais un jour, vous décidez que vous devez changer bun peu. Vous ne connaissez pas tous les contextes d'appel (dont certains peuvent en fait être en dehors de votre base de code), mais cela devrait être correct - vos modifications vont être complètement internes à b, non? Donc vous le réécrivez comme ceci:

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

Et vous pourriez penser que vous n'avez rien changé, puisque vous venez de définir une variable locale. Mais en fait, vous avez cassé a! Maintenant, aimprime oopsplutôt que qux.


En ramenant cela hors du domaine des pseudo-langages, c'est exactement comment MUMPS se comporte, bien qu'avec une syntaxe différente.

Les versions modernes ("modernes") de MUMPS incluent la soi-disant NEWinstruction, qui vous permet d'empêcher les variables de fuir d'un appelé vers un appelant. Donc, dans le premier exemple ci-dessus, si nous avions fait NEW y = "tetanus"in foo(), alors print(y)in bar()n'imprimerait rien (dans MUMPS, tous les noms pointent vers la chaîne vide sauf s'ils sont explicitement définis sur autre chose). Mais rien ne peut empêcher les variables de fuir d'un appelant à un appelé: si nous en avons function p() { NEW x = 3; q(); print(x); }, pour autant que nous le sachions, elles q()pourraient muter x, bien qu'elles ne reçoivent pas explicitement xcomme paramètre. C'est toujours une mauvaise situation, mais pas aussi mauvaise qu'auparavant.

Compte tenu de ces dangers, comment pouvons-nous refactoriser le code en toute sécurité dans MUMPS ou dans tout autre langage avec une portée dynamique?

Il existe des bonnes pratiques évidentes pour faciliter le refactoring, comme ne jamais utiliser de variables dans une fonction autre que celles que vous initialisez ( NEW) vous-même ou qui sont passées en tant que paramètre explicite, et documenter explicitement tous les paramètres qui sont implicitement transmis par les appelants d'une fonction. Mais dans une base de code vieille de 10 décennies ~ 10 8 -LOC, ce sont des luxes que l'on n'a souvent pas.

Et, bien sûr, pratiquement toutes les bonnes pratiques de refactorisation dans les langues à portée lexicale sont également applicables dans les langues à portée dynamique - tests d'écriture, etc. La question est donc la suivante: comment atténuer les risques spécifiquement associés à la fragilité accrue du code à portée dynamique lors de la refactorisation?

(Notez que même si Comment naviguer et refactoriser du code écrit dans un langage dynamique? A un titre similaire à cette question, il n'a aucun rapport.)

senshin
la source
liés (éventuellement un doublon): Existe
moucher
@gnat Je ne vois pas en quoi cette question / ses réponses sont pertinentes pour cette question.
senshin
1
@gnat Êtes-vous en train de dire que la réponse est "utiliser différents processus et autres trucs lourds"? Je veux dire, ce n'est probablement pas faux, mais c'est aussi trop général au point de ne pas être particulièrement utile.
senshin
2
Honnêtement, je ne pense pas qu'il y ait une réponse à cela autre que "passer à un langage où les variables ont réellement des règles de portée" ou "utiliser le beau-fils bâtard de la notation hongroise où chaque variable est préfixée par son nom de fichier et / ou de méthode plutôt que le type ou le type ". Le problème que vous décrivez est tellement terrible que je ne peux pas imaginer une bonne solution.
Ixrec
4
Au moins, vous ne pouvez pas accuser MUMPS de publicité mensongère pour avoir été nommé d'après une maladie désagréable.
Carson63000

Réponses:

4

Sensationnel.

Je ne connais pas MUMPS comme langue, donc je ne sais pas si mon commentaire s'applique ici. De manière générale - Vous devez refactoriser de l'intérieur vers l'extérieur. Ces consommateurs (lecteurs) d'état global (variables globales) doivent être refactorisés en méthodes / fonctions / procédures utilisant des paramètres. La méthode c devrait ressembler à ceci après refactoring:

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

tous les usages de c doivent être réécrits (ce qui est une tâche mécanique)

c(x)

il s'agit d'isoler le code "interne" de l'état global en utilisant l'état local. Lorsque vous aurez terminé, vous devrez réécrire b en:

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

l'affectation x = "oops" est là pour garder les effets secondaires. Nous devons maintenant considérer b comme polluant l'état global. Si vous n'avez qu'un seul élément pollué, pensez à ce refactoring:

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

réécrire chaque utilisation de b avec x = b (). La fonction b doit utiliser uniquement des méthodes déjà nettoyées (vous pouvez vouloir que ro rename co le précise) lors de cette refactorisation. Après cela, vous devez refactoriser b pour ne pas polluer l'environnement mondial.

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

renommez b en b_cleaned. Je suppose que vous devrez jouer un peu avec cela pour vous familiariser avec ce refactoring. Bien sûr, toutes les méthodes ne peuvent pas être refactorisées par cela, mais vous devrez commencer par les parties internes. Essayez cela avec Eclipse et java (méthodes d'extraction) et «état global», alias les membres de la classe, pour vous faire une idée.

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

hth.

Question: Compte tenu de ces dangers, comment pouvons-nous refactoriser en toute sécurité le code dans MUMPS ou dans tout autre langage avec une portée dynamique?

  • Peut-être que quelqu'un d'autre peut donner un indice.

Question: Comment atténuer les risques spécifiquement associés à la fragilité accrue du code à portée dynamique lors de la refactorisation?

  • Écrivez un programme qui effectue les refactorisations sécuritaires pour vous.
  • Écrivez un programme qui identifie les candidats / premiers candidats sûrs.
thepacker
la source
Ah, il y a un obstacle spécifique à MUMPS pour essayer d'automatiser le processus de refactoring: MUMPS n'a pas de fonctions de première classe, ni de pointeurs de fonction ni aucune notion similaire. Ce qui signifie que toute grande base de code MUMPS aura inévitablement beaucoup d'utilisations d'eval (dans MUMPS, appelé EXECUTE), parfois même sur une entrée utilisateur aseptisée - ce qui signifie qu'il peut être impossible de trouver et de réécrire statiquement toutes les utilisations d'une fonction.
senshin
D'accord, ma réponse n'est pas adéquate. Une vidéo youtube, je pense que refactoring @ google scale a fait une approche très unique. Ils ont utilisé clang pour analyser un AST, puis ont utilisé leur propre moteur de recherche pour trouver tout (même un usage caché) pour refactoriser leur code. Cela pourrait être un moyen de trouver chaque utilisation. Je veux dire une approche d'analyse et de recherche sur le code des oreillons.
thepacker
2

Je suppose que votre meilleur coup est de mettre la base de code complète sous votre contrôle et de vous assurer d'avoir une vue d'ensemble des modules et de leurs dépendances.

Vous avez donc au moins la possibilité de faire des recherches globales et d'ajouter des tests de régression pour les parties du système où vous vous attendez à un impact par un changement de code.

Si vous ne voyez aucune chance d'accomplir le premier, mon meilleur conseil est: ne refactorisez pas les modules qui sont réutilisés par d'autres modules, ou pour lesquels vous ne savez pas que d'autres comptent sur eux . Dans toute base de code d'une taille raisonnable, les chances sont élevées que vous puissiez trouver des modules dont aucun autre module ne dépend. Donc, si vous avez un mod A dépendant de B, mais pas l'inverse, et qu'aucun autre module ne dépend de A, même dans un langage à portée dynamique, vous pouvez apporter des modifications à A sans interrompre B ou tout autre module.

Cela vous donne une chance de remplacer la dépendance de A à B par une dépendance de A à B2, où B2 est une version réécrite et nettoyée de B. B2 devrait être une nouvelle écriture avec les règles à l'esprit que vous avez mentionnées ci-dessus pour créer le code plus évolutif et plus facile à refactoriser.

Doc Brown
la source
C'est un bon conseil, bien que j'ajouterai en passant que c'est intrinsèquement difficile dans MUMPS car il n'y a aucune notion de spécificateurs d'accès ni aucun autre mécanisme d'encapsulation, ce qui signifie que les API que nous spécifions dans notre base de code ne sont en fait que des suggestions aux consommateurs de la code sur les fonctions qu'ils doivent appeler. (Bien sûr, cette difficulté particulière n'est pas liée à la portée dynamique; j'en prends simplement note comme point d'intérêt.)
senshin
Après avoir lu cet article , je suis sûr que je ne vous envie pas pour votre tâche.
Doc Brown
0

Pour énoncer l'évidence: comment faire du refactoring ici? Procédez très soigneusement.

(Comme vous l'avez décrit, développer et maintenir la base de code existante devrait être assez difficile, et encore moins tenter de la refactoriser.)

Je crois que j'appliquerais rétroactivement ici une approche axée sur les tests. Cela impliquerait d'écrire une suite de tests pour vous assurer que la fonctionnalité actuelle continue de fonctionner pendant que vous commencez la refactorisation, tout d'abord pour faciliter les tests. (Oui, je m'attends à un problème de poulet et d'oeuf ici, à moins que votre code soit déjà suffisamment modulaire pour le tester sans le changer du tout.)

Ensuite, vous pouvez procéder à d'autres refactorisations, en vérifiant que vous n'avez pas réussi les tests au fur et à mesure.

Enfin, vous pouvez commencer à écrire des tests qui attendent de nouvelles fonctionnalités, puis écrire le code pour faire fonctionner ces tests.

Mark Hurd
la source