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 foo
son 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 b
un 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, a
imprime en oops
lugar 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 NEW
declaració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 x
como 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).
fuente
Respuestas:
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:
todos los usos de c deben reescribirse en (que es una tarea mecánica)
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:
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:
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.
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.
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?
Pregunta: ¿Cómo mitigamos los riesgos específicamente asociados con la mayor fragilidad del código de ámbito dinámico al refactorizar?
fuente
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.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.
fuente
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.
fuente