Código de legado de refactorización Parte 1 - El maestro dorado

Código antiguo. Código feo Código complicado. Código de espagueti. Tonterías jibberish. En dos palabras, Código Legado. Esta es una serie que te ayudará a trabajar y lidiar con ella..

En un mundo ideal, solo escribirías código nuevo. Lo escribirías hermoso y perfecto. Nunca tendría que volver a visitar su código y nunca tendrá que mantener proyectos que tengan diez años. En un mundo ideal…

Desafortunadamente, vivimos en una realidad que no es ideal. Tenemos que entender, modificar y mejorar el código antiguo. Tenemos que trabajar con código legado. ¿Entonces, Qué esperas? Entremos en este primer tutorial, obtengamos el código, lo entendamos un poco y creemos una red de seguridad para nuestras futuras modificaciones..

Definición de Código Legado

El código heredado se definió de tantas maneras que es imposible encontrar una definición única y comúnmente aceptada para él. Los pocos ejemplos al comienzo de este tutorial son solo la punta del iceberg. Así que no te daré ninguna definición oficial. En su lugar, te citaré mi favorito..

A mi, código legado Es simplemente el código sin pruebas. ~ Michael Plumas

Bueno, esa es la primera definición formal de la expresión. código legado, publicado por Michael Feathers en su libro Working Effectively with Legacy Code. Por supuesto, la industria usó la expresión durante años, básicamente para cualquier código que sea difícil de cambiar. Sin embargo esta definición tiene algo diferente que contar. Explica el problema muy claramente, para que la solución sea obvia. "Difícil de cambiar" es tan vago. ¿Qué debemos hacer para facilitar el cambio? ¡No tenemos idea! "Código sin pruebas" en cambio es muy concreto. Y la respuesta a nuestra pregunta anterior es simple: haga que el código sea verificable y pruébelo. Entonces empecemos.

Obteniendo nuestro código legado

Esta serie se basará en el excepcional juego de preguntas de J.B. Rainsberger, diseñado para eventos de Retiro de código heredado. Está hecho para ser como un código legado real y también para ofrecer oportunidades para una amplia variedad de refactorización, en un nivel de dificultad decente.

Revisa el código fuente

El juego Trivia está alojado en GitHub y tiene licencia GPLv3, por lo que puedes jugar con él libremente. Comenzaremos esta serie revisando el repositorio oficial. El código también se adjunta a este tutorial con todas las modificaciones que haremos, por lo que si se confunde en algún momento, puede echar un vistazo al resultado final..

 $ git clone https://github.com/jbrains/trivia.git Clonación en 'trivia' ... remoto: contando objetos: 429, listo. remoto: Compresión de objetos: 100% (262/262), hecho. remoto: Total 429 (delta 100), reutilizado 419 (delta 93) Objetos de recepción: 100% (429/429), 848.33 KiB | 305.00 KiB / s, hecho. Resolución de deltas: 100% (100/100), hecho. Comprobando la conectividad ... hecho.

Cuando abres el trivialidades Directorio verá nuestro código en varios lenguajes de programación. Trabajaremos en PHP, pero usted es libre de elegir su favorito y aplicar las técnicas presentadas aquí..

Entendiendo el Código

Por definición, el código heredado es difícil de entender, especialmente si ni siquiera sabemos qué se supone que debe hacer. Entonces, el primer paso es ejecutar el código y hacer algún tipo de razonamiento, de qué se trata..

Tenemos dos archivos en nuestro directorio..

$ cd php / $ ls -al total 20 drwxr-xr-x 2 csaba csaba 4096 10 de marzo 21:05. drwxr-xr-x 26 csaba csaba 4096 10 de marzo 21: 05… -rw-r - r-- 1 csaba csaba 5568 10 de marzo 21:05 Game.php -rw-r - r-- 1 csaba csaba 410 mar 10 21:05 GameRunner.php

GameRunner.php Parece ser un buen candidato para nuestro intento de ejecutar el código.

$ php ./GameRunner.php Chet se agregó. Son el número de jugador 1 Pat se agregó. El número de jugador es 2. Sue se agregó. Son el número 3 de Chet es el jugador actual. Lanzaron un 4. Pregunta Pop 0 La respuesta fue correcta !!!! Chet ahora tiene 1 monedas de oro. Pat es el jugador actual. Han sacado un 2. La nueva ubicación de Pat es 2. La categoría es Deportes. Pregunta deportiva 0 ¡La respuesta fue correcta! Pat ahora tiene 1 monedas de oro. Sue es el jugador actual. Han lanzado un 1. La nueva ubicación de Sue es 1. La categoría es Ciencia Pregunta de Ciencia 0 ¡La respuesta fue correcta! Sue ahora tiene 1 monedas de oro. Chet es el jugador actual. Han sacado un 4 ## Algunas líneas eliminadas para mantener ## el tutorial en un tamaño razonable. ¡La respuesta fue correcta! Sue ahora tiene 5 monedas de oro. Chet es el jugador actual. Han sacado un 3. Chet está saliendo del área de penalización. La nueva ubicación de Chet es 11. La categoría es Rock Rock Pregunta 5 ¡La respuesta fue correcta! Chet ahora tiene 5 monedas de oro. Pat es el jugador actual. Han sacado un 1. La nueva ubicación de Pat es 10 La categoría es Deportes Deportes Pregunta 1 ¡La respuesta fue correcta! Pat ahora tiene 6 monedas de oro.

DE ACUERDO. Nuestra suposición era correcta. Nuestro código corrió y produjo algunos resultados. Analizar esta salida nos ayudará a deducir una idea básica sobre lo que hace el código.

  1. Sabemos que es un juego de Trivia. Lo sabíamos cuando comprobamos el código fuente.
  2. Nuestro ejemplo tiene tres jugadores: Chet, Pat y Sue..
  3. Hay algún tipo de rodar de un dado o un concepto similar..
  4. Hay una ubicación actual para un jugador. Posiblemente en algún tipo de tabla.?
  5. Hay varias categorías desde las cuales se hacen preguntas.
  6. Los usuarios responden preguntas.
  7. Las respuestas correctas dan oro a los jugadores..
  8. Las respuestas incorrectas envían a los jugadores al cuadro de penalización..
  9. Los jugadores pueden salir de la casilla de penalización, según una lógica no muy clara.
  10. Parece que el usuario que primero llega a seis monedas de oro gana..

Ahora eso es mucho conocimiento. Podríamos averiguar la mayor parte del comportamiento básico de la aplicación con solo mirar la salida. En aplicaciones de la vida real, la salida puede no ser texto en la pantalla, pero puede ser una página web, un registro de errores, una base de datos, una comunicación de red, un archivo de volcado, etc. En otros casos, el módulo que necesita modificar no se puede ejecutar aislado. Si es así, deberá ejecutarlo a través de otros módulos de la aplicación más grande. Solo intente agregar el mínimo para obtener un resultado razonable de su código heredado.

Escaneando el Código

Ahora que tenemos una idea de lo que produce el código, podemos empezar a verlo. Empezaremos con el corredor..

El corredor del juego

Me gusta comenzar con la ejecución de todo el código a través del formateador de mi IDE. Esto mejora enormemente la legibilidad al familiarizar la forma del código con lo que estoy acostumbrado. Así que esto:

... se convertirá en esto:

... que es algo mejor. Puede que no sea una gran diferencia con esta pequeña cantidad de código, pero estará en nuestro próximo archivo.

Mirando a nuestro GameRunner.php archivo, podemos identificar fácilmente algunos aspectos clave que observamos en la salida. Podemos ver las líneas que agregan a los usuarios (9-11), que se llama a un método roll () y se selecciona un ganador. Por supuesto, estos están lejos de los secretos internos de la lógica del juego, pero al menos podríamos comenzar por identificar los métodos clave que nos ayudarán a descubrir el resto del código..

El archivo de juego

Deberíamos hacer el mismo formateo en el Game.php archivo también.

Este archivo es mucho más grande; Cerca de 200 líneas de código. La mayoría de los métodos tienen el tamaño adecuado, pero algunos de ellos son bastante grandes y después del formateo, podemos ver que en dos lugares la sangría del código va más allá de cuatro niveles. Los altos niveles de sangrado generalmente implican muchas decisiones complejas, por lo que, por ahora, podemos suponer que esos puntos en nuestro código serán más complejos y más sensibles al cambio..

El maestro de oro

Y el pensamiento de cambio nos lleva a nuestra falta de pruebas. Los métodos que vimos en Game.php Son bastante complejos. No te preocupes si no los entiendes. En este punto, también son un misterio para mí. El código heredado es un misterio que debemos resolver y comprender. Hicimos nuestro primer paso para entenderlo y ahora es el momento de nuestro segundo paso..

Entonces, ¿qué es este maestro de oro?

Cuando se trabaja con código heredado, es casi imposible entenderlo y escribir código que seguramente ejerza todas las rutas lógicas a través del código. Para ese tipo de pruebas, deberíamos entender el código, pero aún no lo hemos hecho. Así que tenemos que tomar otro enfoque..

En lugar de tratar de averiguar qué probar, podemos probarlo todo, muchas veces, por lo que terminamos con una gran cantidad de resultados, sobre los cuales casi podemos asumir que se produjo ejercitando todas las partes de nuestro legado código. Se recomienda ejecutar el código al menos 10,000 (diez mil) veces. Escribiremos una prueba para ejecutarla el doble y guardaremos la salida..

Escribiendo el Golden Master Generator

Podemos pensar en el futuro y comenzar creando un generador y una prueba como archivos separados para futuras pruebas, pero ¿es realmente necesario? Aún no lo sabemos con certeza. Entonces, ¿por qué no comenzar con un archivo de prueba básico que ejecutará nuestro código una vez y construirá nuestra lógica a partir de ahí?.

Lo encontrarás en el archivo de código adjunto, dentro del fuente carpeta pero fuera de la trivialidades carpeta nuestra Prueba carpeta. En esta carpeta, creamos un archivo: GoldenMasterTest.php.

la clase GoldenMasterTest extiende PHPUnit_Framework_TestCase function testGenerateOutput () ob_start (); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Podríamos hacer esto de muchas maneras. Podríamos, por ejemplo, ejecutar nuestro código desde la consola y redirigir su salida a un archivo. Sin embargo, tenerlo en una prueba que se ejecute fácilmente dentro de nuestro IDE es una ventaja que no debemos ignorar..

El código es bastante simple, amortigua la salida y la coloca en el $ salida variable. los requerir una vez() También se ejecutará todo el código dentro del archivo incluido. En nuestro var dump veremos una salida ya familiar..

Sin embargo, en una segunda ejecución, podemos observar algo extraño:

... las salidas difieren. Aunque ejecutamos el mismo código, la salida es diferente. Los números rodados son diferentes, las posiciones de los jugadores son diferentes..

Sembrando el generador aleatorio

hacer $ aGame-> roll (rand (0, 5) + 1); if (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner);

Al analizar el código esencial del corredor, podemos ver que utiliza la función rand () Para generar números aleatorios. Nuestra siguiente parada es la documentación oficial de PHP para investigar esto. rand () función.

El generador de números aleatorios se siembra automáticamente.

La documentación nos dice que la siembra ocurre automáticamente. Ahora tenemos otra tarea. Necesitamos encontrar una manera de controlar la semilla. los srand () La función puede ayudar con eso. Aquí está su definición de la documentación..

Siembre el generador de números aleatorios con semilla o con un valor aleatorio si no se da una semilla.

Nos dice, que si ejecutamos esto antes de cualquier llamada a rand (), Siempre debemos terminar con los mismos resultados..

function testGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Nosotros ponemos srand (1) antes de nuestro requerir una vez(). Ahora la salida es siempre la misma..

Poner la salida en un archivo

la clase GoldenMasterTest extiende PHPUnit_Framework_TestCase function testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ file_content, $ this-> generateOutput ());  private function generateOutput () ob_start (); srand (1); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); devuelve $ output; 

Este cambio parece razonable. ¿Derecha? Extrajimos la generación de código en un método, lo ejecutamos dos veces y esperamos que la salida sea igual. Sin embargo no serán.

La razon es que requerir una vez() No requerirá el mismo archivo dos veces. La segunda llamada a la genera salida () método producirá una cadena vacía. ¿Así que, Qué podríamos hacer? ¿Qué pasa si simplemente exigir()? Eso debería ser ejecutado cada vez..

Bueno, eso lleva a otro problema: "No se puede volver a declarar la sentencia de Echoln ()". ¿Pero de dónde viene eso? Está justo al principio de la Game.php expediente. La razón por la que este error está ocurriendo es porque en GameRunner.php tenemos incluir __DIR__. '/Game.php';, que trata de incluir el archivo del Juego dos veces, cada vez que llamamos al genera salida () método.

include_once __DIR__. '/Game.php';

Utilizando include_once en GameRunner.php Resolveremos nuestro problema. Sí, necesitábamos modificar GameRunner.php Sin tener pruebas para ello, todavía! Sin embargo, podemos estar 99% seguros de que nuestro cambio no romperá el código en sí. Es un cambio lo suficientemente pequeño y simple para no asustarnos mucho. Y lo más importante, hace pasar las pruebas..

Ejecutarlo varias veces

Ahora que tenemos código que podemos ejecutar muchas veces, es hora de generar algo de salida.

function testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (20, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2);  private function generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ()); $ primero = falso;  else file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  $ veces--; 

Aquí extrajimos otro método: generar muchos (). Tiene dos parámetros. Una para la cantidad de veces que queremos ejecutar nuestro generador, la otra es un archivo de destino. Pondrá la salida generada en los archivos. En la primera ejecución, vacía los archivos y, para el resto de las iteraciones, agrega los datos. Puedes mirar el archivo para ver la salida generada 20 veces.

¡Pero espera! ¿El mismo jugador gana cada vez? Es eso posible?

cat /tmp/gm.txt | grep "tiene 6 monedas de oro". Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro..

¡Sí! ¡Es posible! Es más que posible. Es una cosa segura. Tenemos la misma semilla para nuestra función aleatoria. Jugamos el mismo juego una y otra vez.

Ejecutarlo de manera diferente cada vez

Necesitamos jugar juegos diferentes, de lo contrario es casi seguro que solo una pequeña parte de nuestro código heredado se ejerce una y otra vez. El alcance del maestro de oro es ejercitar tanto como sea posible. Necesitamos volver a sembrar el generador aleatorio cada vez, pero de una manera controlada. Una opción es usar nuestro contador como valor semilla..

la función privada generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ primero = falso;  else file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND);  $ veces--;  función privada generateOutput ($ semilla) ob_start (); srand ($ semilla); requiere __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); devuelve $ output; 

Esto sigue manteniendo nuestra prueba, por lo que estamos seguros de que generamos la misma salida completa cada vez, mientras que la salida juega un juego diferente para cada iteración..

cat /tmp/gm.txt | grep "tiene 6 monedas de oro". Sue ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Pat ahora tiene 6 monedas de oro. Pat ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Sue ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Sue ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Sue ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Pat ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro. Chet ahora tiene 6 monedas de oro..

Hay varios ganadores para el juego de una manera aleatoria. Esto luce bien.

Llegando a 20,000

Lo primero que puedes intentar es ejecutar nuestro código para 20,000 iteraciones de juegos.

function testGenerateOutput () $ times = 20000; $ this-> generateMany ($ times, '/tmp/gm.txt'); $ this-> generateMany ($ times, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2); 

Esto casi funcionará. Se generarán dos archivos de 55MB..

ls -alh / tmp / gm * -rw-r - r-- 1 csaba csaba 55M 14 de marzo 20:38 /tmp/gm2.txt -rw-r - r-- 1 csaba csaba 55M 14 de marzo 20:38 /tmp/gm.txt

Por otro lado, la prueba fallará con un error de memoria insuficiente. No importa cuánta RAM tengas, esto fallará. Tengo 8GB más un swap de 4GB y falla. Las dos cadenas son demasiado grandes para ser comparadas en nuestra afirmación.

En otras palabras, generamos buenos archivos, pero PHPUnit no puede compararlos. Necesitamos una solución.

$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');

Ese parece ser un buen candidato, pero todavía falla. Qué lástima. Necesitamos investigar más a fondo la situación..

$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);

Esto sin embargo, está funcionando..

Puede comparar las dos cadenas y fallar si son diferentes. Tiene sin embargo, un pequeño precio. No podrá decir exactamente qué está mal cuando las cadenas difieren. Simplemente dirá "Falló afirmando que lo falso es verdadero".. Pero vamos a tratar con eso en un próximo tutorial.

Pensamientos finales

Hemos terminado para este tutorial. Hemos aprendido mucho para nuestra primera lección y estamos en un buen comienzo para nuestro trabajo futuro. Conocimos el código, lo analizamos de diferentes maneras y, en su mayor parte, comprendimos su lógica esencial. Luego creamos un conjunto de pruebas para asegurarnos de que se ejerce lo más posible. Sí. Las pruebas son muy lentas. Les toma 24 segundos en mi CPU Core i7 generar la salida dos veces. Afortunadamente en nuestro desarrollo futuro, mantendremos la gm.txt Archivo intacto y generar otro solo una vez por ejecución. Pero 12 segundos es todavía una gran cantidad de tiempo para una base de código tan pequeña.

Para cuando terminemos esta serie, nuestras pruebas deberían ejecutarse en menos de un segundo y probar todo el código correctamente. Por lo tanto, estad atentos para nuestro próximo tutorial cuando abordemos problemas como constantes mágicas, cadenas mágicas y condicionales complejos. Gracias por leer.