¿Cómo refactorizar de forma segura en un lenguaje con alcance dinámico?

13

Para aquellos de ustedes que tienen la suerte de no trabajar en un idioma con alcance dinámico, permítanme darles un pequeño repaso sobre cómo funciona. Imagine un pseudo-lenguaje, llamado "RUBELLA", que se comporta así:

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"

Es decir, las variables se propagan hacia arriba y hacia abajo en la pila de llamadas libremente: todas las variables definidas en fooson visibles para (y mutables por) su llamador bar, y lo contrario también es cierto. Esto tiene serias implicaciones para la refactorización del código. Imagine que tiene el siguiente código:

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

Ahora, las llamadas a a()se imprimirán qux. Pero luego, algún día, decides que necesitas cambiar bun poco. No conoce todos los contextos de llamadas (algunos de los cuales pueden estar fuera de su base de código), pero eso debería estar bien: sus cambios serán completamente internos b, ¿verdad? Entonces lo reescribes así:

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

Y puede pensar que no ha cambiado nada, ya que acaba de definir una variable local. Pero, de hecho, ¡te has roto a! Ahora, aimprime en oopslugar de qux.


Sacando esto del ámbito de los pseudo-idiomas, así es exactamente como se comporta MUMPS, aunque con una sintaxis diferente.

Las versiones modernas ("modernas") de MUMPS incluyen la llamada NEWdeclaración, que le permite evitar que las variables se filtren de una persona que llama a una persona que llama. Así que en el primer ejemplo anterior, si hubiéramos hecho NEW y = "tetanus"en foo(), a continuación, print(y)en bar()imprimiría nada (en las paperas, todos los nombres apuntan a la cadena vacía a menos que establecer explícitamente a otra cosa). Pero no hay nada que pueda evitar que las variables se filtren de una persona que llama a una persona que llama: si tenemos function p() { NEW x = 3; q(); print(x); }, por lo que sabemos, q()podría mutar x, a pesar de no recibirlo explícitamente xcomo parámetro. Todavía es una mala situación, pero no tan mala como probablemente solía ser.

Con estos peligros en mente, ¿cómo podemos refactorizar de manera segura el código en MUMPS o en cualquier otro idioma con alcance dinámico?

Existen algunas buenas prácticas obvias para facilitar la refactorización, como nunca usar variables en una función que no sean las que usted mismo inicializa ( NEW) o se pasan como un parámetro explícito, y documentar explícitamente cualquier parámetro que se pase implícitamente desde los llamadores de una función. Pero en una base de código de ~ 10 8 -LOC de décadas, estos son lujos que a menudo no se tienen.

Y, por supuesto, esencialmente todas las buenas prácticas para refactorizar en idiomas con alcance léxico también son aplicables en idiomas con alcance dinámico: pruebas de escritura, etc. La pregunta, entonces, es esta: ¿cómo mitigamos los riesgos específicamente asociados con la mayor fragilidad del código de alcance dinámico cuando se refactoriza?

(Tenga en cuenta que si bien ¿Cómo navega y refactoriza el código escrito en un lenguaje dinámico? Tiene un título similar a esta pregunta, no tiene ninguna relación).

senshin
fuente
@gnat No veo cómo esa pregunta / sus respuestas son relevantes para esta pregunta.
senshin
1
@gnat ¿Estás diciendo que la respuesta es "usar diferentes procesos y otras cosas pesadas"? Quiero decir, eso probablemente no esté mal, pero también es demasiado general hasta el punto de no ser particularmente útil.
senshin
2
Honestamente, no creo que haya una respuesta a esto que no sea "cambiar a un lenguaje donde las variables realmente tengan reglas de alcance" o "usar el hijastro bastardo de la notación húngara donde cada variable está prefijada por su archivo y / o nombre del método que tipo o tipo ". El problema que describe es tan terrible que no puedo imaginar una buena solución.
Ixrec
44
Al menos no se puede acusar a MUMPS de publicidad falsa por el nombre de una enfermedad desagradable.
Carson63000

Respuestas:

4

Guau.

No conozco MUMPS como idioma, por lo que no sé si mi comentario se aplica aquí. En términos generales, debe refactorizar de adentro hacia afuera. Los consumidores (lectores) del estado global (variables globales) deben ser refactorizados en métodos / funciones / procedimientos utilizando parámetros. El método c debería verse así después de refactorizar:

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

todos los usos de c deben reescribirse en (que es una tarea mecánica)

c(x)

esto es para aislar el código "interno" del estado global mediante el uso del estado local. Cuando haya terminado con eso, tendrá que volver a escribir b en:

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

la asignación x = "oops" está ahí para mantener los efectos secundarios. Ahora debemos considerar a b como contaminante del estado global. Si solo tiene un elemento contaminado, considere esta refactorización:

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

final reescribe cada uso de b con x = b (). La función b debe usar solo métodos ya limpiados (es posible que desee cambiar el nombre de Ro para que quede claro) al hacer esta refactorización. Después de eso, debe refactorizar b para no contaminar el entorno global.

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

cambie el nombre de b a b_cleaned. Supongo que tendrás que jugar un poco con eso para acostumbrarte a esa refactorización. Claro que no todos los métodos pueden ser refactorizados por esto, pero tendrá que comenzar desde las partes internas. Intente eso con Eclipse y java (métodos de extracción) y los miembros de la clase de "estado global" para obtener una idea.

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

hth.

Pregunta: Con estos peligros en mente, ¿cómo podemos refactorizar de forma segura el código en MUMPS o en cualquier otro lenguaje con alcance dinámico?

  • Quizás alguien más pueda dar una pista.

Pregunta: ¿Cómo mitigamos los riesgos específicamente asociados con la mayor fragilidad del código de ámbito dinámico al refactorizar?

  • Escriba un programa que realice las refactorizaciones seguras por usted.
  • Escriba un programa que identifique candidatos seguros / primeros candidatos.
thepacker
fuente
Ah, hay un obstáculo específico de MUMPS para intentar automatizar el proceso de refactorización: MUMPS no tiene funciones de primera clase, ni tiene punteros de función ni ninguna noción similar. Lo que significa que cualquier base de código MUMPS grande inevitablemente tendrá muchos usos de eval (en MUMPS, llamado EXECUTE), a veces incluso en la entrada de usuario desinfectada, lo que significa que puede ser imposible encontrar y reescribir estáticamente todos los usos de una función.
senshin
Bien, considera mi respuesta como inadecuada. Un video de youtube creo que refactorizar @ google scale hizo un enfoque muy singular. Usaron clang para analizar un AST y luego usaron su propio motor de búsqueda para encontrar cualquier (incluso uso oculto) para refactorizar su código. Esta podría ser una forma de encontrar cada uso. Me refiero a un enfoque de análisis y búsqueda en el código de paperas.
thepacker
2

Supongo que su mejor opción es tener la base de código completa bajo su control y asegurarse de tener una visión general sobre los módulos y sus dependencias.

Por lo tanto, al menos tiene la posibilidad de realizar búsquedas globales y la posibilidad de agregar pruebas de regresión para las partes del sistema en las que espera un impacto por un cambio de código.

Si no ve la oportunidad de lograr lo primero, mi mejor consejo es: no refactorice ningún módulo que sea reutilizado por otros módulos o para el que no sepa que otros confían en ellos . En cualquier base de código de un tamaño razonable, las posibilidades son altas, puede encontrar módulos de los que no depende ningún otro módulo. Entonces, si tiene un mod A que depende de B, pero no viceversa, y ningún otro módulo depende de A, incluso en un lenguaje de alcance dinámico, puede realizar cambios en A sin interrumpir B o cualquier otro módulo.

Esto le da la oportunidad de reemplazar la dependencia de A a B por una dependencia de A a B2, donde B2 es una versión desinfectada y reescrita de B. B2 debe ser una nueva escritura con las reglas en mente que mencionó anteriormente para hacer el código más evolutivo y más fácil de refactorizar.

Doc Brown
fuente
Este es un buen consejo, aunque agregaré aparte que esto es inherentemente difícil en MUMPS ya que no existe una noción de especificadores de acceso ni ningún otro mecanismo de encapsulación, lo que significa que las API que especificamos en nuestra base de código son efectivamente solo sugerencias para los consumidores de código sobre qué funciones deben llamar. (Por supuesto, esta dificultad particular no está relacionada con el alcance dinámico; solo estoy tomando nota de esto como un punto de interés.)
senshin
Después de leer este artículo , estoy seguro de que no te envidio por tu tarea.
Doc Brown
0

Para decir lo obvio: ¿Cómo hacer refactorización aquí? Proceder con mucho cuidado.

(Como lo ha descrito, desarrollar y mantener la base de código existente debería ser bastante difícil, y mucho menos intentar refactorizarlo).

Creo que aplicaría retroactivamente un enfoque basado en pruebas aquí. Esto implicaría escribir un conjunto de pruebas para garantizar que la funcionalidad actual siga funcionando cuando comience a refactorizar, en primer lugar solo para facilitar las pruebas. (Sí, espero un problema de huevo y gallina aquí, a menos que su código ya sea lo suficientemente modular como para probar sin cambiarlo en absoluto).

Luego, puede proceder con otra refactorización, verificando que no haya roto ninguna prueba a medida que avanza.

Finalmente, puede comenzar a escribir pruebas que esperan nuevas funcionalidades y luego escribir el código para que esas pruebas funcionen.

Mark Hurd
fuente