¿Cómo puede refactorizar con seguridad en un lenguaje con alcance dinámico?

13

Para aquellos de ustedes que tienen la suerte de no trabajar en un lenguaje con alcance dinámico, permítanme informarles un poco sobre cómo funciona esto. Imagina 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 foo son visibles (y se pueden mata) de su interlocutor bar , y lo contrario también es cierto. Esto tiene serias implicaciones para la refactorabilidad del código. Imagina que tienes 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() imprimirán qux . Pero entonces, algún día, decides que necesitas cambiar un poco de b . No conoces todos los contextos de llamada (algunos de los cuales pueden estar fuera de tu base de código), pero eso debería estar bien: tus cambios serán completamente internos a b , ¿verdad? Así que lo reescribes así:

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

Y podría pensar que no ha cambiado nada, ya que acaba de definir una variable local. Pero, de hecho, has roto a ! Ahora, a imprime oops en lugar de qux .

Sacando esto de nuevo del reino de los pseudo-lenguajes, esto es exactamente cómo se comporta MUMPS, aunque con una sintaxis diferente.

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

Teniendo en cuenta estos peligros, ¿cómo podemos refactorizar de forma segura el código en MUMPS o en cualquier otro idioma con un 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 inicializa usted mismo ( NEW ) o se pasan como un parámetro explícito, y documentar explícitamente cualquier parámetro que sea pasó implícitamente de los llamadores de una función. Pero en una base de códigos de ~ 10 8 -LOC de hace décadas, estos son lujos que a menudo no se tienen.

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

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

    
pregunta senshin 12.09.2015 - 15:38

3 respuestas

4

Wow.

No sé MUMPS como idioma, por lo que no sé si mi comentario se aplica aquí. En general, debe refactorizar de adentro hacia afuera. Esos consumidores (lectores) de estado global (variables globales) deben ser refactorizados en métodos / funciones / procedimientos usando parámetros. El método c debería tener este aspecto después de la refactorización:

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

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

c(x)

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

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

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

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

termina de reescribir 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 para dejarlo claro) al realizar 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;
}

cambia 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 refactorizarse con esto, pero tendrá que comenzar desde las partes internas. Intenta eso con Eclipse y java (métodos de extracción) y "estado global" a.k.a. miembros de la clase para obtener una idea.

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

hth.

Pregunta: Teniendo en cuenta estos peligros, ¿cómo podemos refactorizar de manera segura el código en MUMPS o en cualquier otro idioma con un alcance dinámico?

  • Tal vez alguien más pueda dar una pista.

Pregunta: ¿Cómo mitigamos los riesgos específicamente asociados con el aumento de la fragilidad del código de alcance dinámico cuando se refactoriza?

  • Escriba un programa que haga las refactorizaciones seguras por usted.
  • Escriba un programa que identifique a los candidatos seguros / primeros candidatos.
respondido por el thepacker 12.09.2015 - 22:29
2

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

Entonces, 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 el primero, mi mejor consejo es: no refactorice ningún módulo que haya sido reutilizado por otros módulos, o para el que no sabe 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 dependiendo de B, pero no al revés, y ningún otro módulo depende de A, incluso en un lenguaje de alcance dinámico, puede realizar cambios en A sin romper B ni ningún 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 saneada y reescrita de B. B2 debe ser un nuevo escrito con las reglas en mente que mencionó anteriormente para Hacer que el código sea más evolutivo y más fácil de refactorizar.

    
respondido por el Doc Brown 13.09.2015 - 08:56
0

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

(Como lo describió, desarrollar y mantener la base de código existente debería ser lo suficientemente 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 al comenzar la refactorización, en primer lugar para facilitar las pruebas. (Sí, espero un problema de gallina y huevo aquí, a menos que su código ya sea lo suficientemente modular como para probarlo sin cambiarlo en absoluto).

Luego puedes continuar con otra refactorización, verificando que no hayas superado ninguna prueba a medida que avanzas.

Finalmente, puedes comenzar a escribir pruebas que esperan nuevas funciones y luego escribir el código para hacer que esas pruebas funcionen.

    
respondido por el Mark Hurd 16.09.2015 - 08:22

Lea otras preguntas en las etiquetas