Código heredado de refactorización Parte 3 - Condicionales complejos

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

Me gusta pensar en el código tal como pienso en la prosa. Las oraciones largas, anidadas y compuestas con palabras exóticas son difíciles de entender. De vez en cuando necesita uno, pero la mayoría de las veces, puede usar palabras simples y sencillas en oraciones cortas. Esto es muy cierto para el código fuente también. Los condicionales complejos son difíciles de entender. Los métodos largos son como oraciones sin fin.

De la prosa al código

Aquí hay un ejemplo "prosaico" para animarte. Primero, la oración todo en uno. El feo.

Si la temperatura en la sala de servidores es inferior a cinco grados, y la humedad aumenta más del cincuenta por ciento, pero permanece por debajo de los ochenta y la presión del aire es constante, entonces el técnico principal John, que tiene al menos tres años de experiencia laboral en redes y servidores, debería ser notificado, y debe despertarse en medio de la noche, disfrazarse, salir, tomar su auto o llamar a un taxi si no tiene un auto, conducir a la oficina, ingresar al edificio, encender el aire acondicionado y espere hasta que la temperatura aumente más de diez grados y la humedad caiga por debajo del veinte por ciento.

Si puedes entender, comprender y recordar ese párrafo sin releerlo, te doy una medalla (virtual, por supuesto). Los párrafos largos y enredados escritos en una sola oración complicada son difíciles de entender. Desafortunadamente, no conozco suficientes palabras exóticas en inglés para hacer eso aún más difícil de entender.

Simplificación

Vamos a encontrar una manera de simplificarlo un poco. Toda su primera parte, hasta el "entonces" es una condición. Sí, es complicado pero podríamos resumirlo así: Si las condiciones ambientales representan un riesgo ... ... entonces se debe hacer algo. La expresión complicada dice que debemos notificar a alguien que cumple muchas condiciones: a continuación, notifique el nivel tres de soporte técnico. Finalmente, se describe todo un proceso desde que despertamos al técnico hasta que todo está solucionado: y esperar que el entorno sea restaurado dentro de los parámetros normales. Vamos a ponerlo todo junto.

Si las condiciones ambientales representan un riesgo, notifique el soporte técnico de nivel tres y espere que el entorno se restaure dentro de los parámetros normales.

Ahora, eso es solo un 20% de la longitud en comparación con el texto original. No conocemos los detalles y en la gran mayoría de los casos, no nos importa. Y esto es muy cierto para el código fuente también. ¿Cuántas veces te importaron los detalles de implementación de un logInfo ("Algunos mensajes"); ¿método? Probablemente alguna vez, si y cuando lo implementaste. Luego simplemente registra el mensaje en la categoría "información". O cuando un usuario compra uno de sus productos, ¿le importa cómo facturarlo? No. Todo lo que quiere preocuparse es Si el producto fue comprado, deséchelo del inventario y envíelo al comprador.. Los ejemplos podrían ser interminables. Son básicamente como escribimos el software correcto..

Condicionales complejos

En esta sección trataremos de aplicar la filosofía de la prosa a nuestro juego de preguntas. Un paso a la vez. A partir de condicionales complejos. Vamos a empezar con un código fácil. Solo para calentar.

Línea veinte de la GameRunner.php el archivo se lee así:

if (rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId)

¿Cómo sonaría eso en prosa?? Si un número aleatorio entre la ID de respuesta mínima y la ID de respuesta máxima es igual a la ID de la respuesta incorrecta, entonces ...

Esto no es muy complicado, pero aún podemos simplificarlo. Que hay de esto? Si se selecciona una respuesta incorrecta, entonces ... Mejor no lo es?

El método de extracción Refactorización

Necesitamos una forma, un procedimiento, una técnica para mover esa declaración condicional a otro lugar. Ese destino puede ser fácilmente un método. O en nuestro caso, ya que no estamos dentro de una clase aquí, una función. Este movimiento de comportamiento en un nuevo método o función se llama refactorización "Extraer método". A continuación se detallan los pasos, tal como los define Martin Fowler en su excelente libro Refactoring: Mejorando el diseño de un código existente. Si no leyó este libro, debe ponerlo en su lista de "Leer" ahora. Es uno de los libros más esenciales para un programador moderno..

Para nuestro tutorial, he seguido los pasos originales y los he simplificado un poco para que se ajusten mejor a nuestras necesidades y nuestro tipo de tutorial..

  1. Cree un nuevo método y asígnele un nombre después de lo que hace, no cómo lo hace.
  2. Copie el código del lugar extraído, en el método. Tenga en cuenta, esto es dupdo, Aún no elimine el código original..
  3. Escanee el código extraído para cualquier variable que sea local. Deben hacerse parámetros para el método..
  4. Ver si se utilizan variables temporales dentro del método extraído. Si es así, declara dentro y suelta el parámetro extra.
  5. Pase al método de destino las variables como parámetros..
  6. Reemplace el código extraído con la llamada al método de destino.
  7. Ejecuta tus pruebas.

Ahora esto es bastante complicado. Sin embargo, el método de extracción es posiblemente la refactorización más utilizada, a excepción del cambio de nombre tal vez. Así que hay que entender su mecánica..

Afortunadamente para nosotros, los IDE modernos como PHPStorm proporcionan excelentes herramientas de refactorización, como hemos visto en el tutorial PHPStorm: When the IDE Really Matters. Así que usaremos las funciones que tenemos a nuestro alcance, en lugar de hacerlo todo a mano. Esto es menos propenso a errores y mucho, mucho más rápido.

Simplemente seleccione la parte deseada del código y botón derecho del ratón eso.

El IDE entenderá automáticamente que necesitamos tres parámetros para ejecutar nuestro código y propondrá la siguiente solución.

//… // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;  do $ dice = rand (0, 5) + 1; $ aGame-> roll ($ dice); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner);

Si bien este código es sintácticamente correcto, romperá nuestras pruebas. Entre todo el ruido que se nos muestra en colores rojo, azul y negro, podemos detectar la razón por la que:

Error grave: no se puede volver a declarar isCurrentAnswerWrong () (declarado anteriormente en / home / csaba / Personal / Programming / NetTuts / Refactoring Legacy Code - Parte 3: Condicionales complejos y métodos largos /Source/trivia/php/GameRunner.php:16) en / home / csaba / Personal / Programming / NetTuts / Refactoring Legacy Code - Parte 3: Condicionales complejos y métodos largos /Source/trivia/php/GameRunner.php en la línea 18

Básicamente dice que queremos declarar la función dos veces. Pero, ¿cómo puede suceder eso? Lo tenemos solo una vez en nuestro GameRunner.php!

Echa un vistazo a las pruebas. Hay un genera salida () método que hace un exigir() en nuestro GameRunner.php. Se llama al menos dos veces. Aquí está la fuente del error..

Ahora tenemos un dilema. Debido a la siembra del generador aleatorio, necesitamos llamar a este código con valores controlados.

función privada generateOutput ($ semilla) ob_start (); srand ($ semilla); requiere __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); devuelve $ output; 

Pero no hay manera de declarar una función dos veces en PHP, por lo que necesitamos otra solución. Comenzamos a sentir la carga de nuestra prueba de maestro dorado. Ejecutar todo 20000 veces, cada vez que cambiamos un código, puede que no sea una solución a largo plazo. Además del hecho de que lleva años ejecutarse, nos obliga a cambiar nuestro código para adaptarnos a la forma en que lo probamos. Esto suele ser un signo de malas pruebas. El código debería cambiar y seguir haciendo la prueba, pero los cambios deberían tener motivos para cambiar, provenientes únicamente del código fuente..

Pero basta de hablar, necesitamos una solución, incluso una temporal lo hará por ahora. La migración a las pruebas de unidad comenzará con nuestra próxima lección.

Una forma de resolver nuestro problema es tomar todo el resto del código en GameRunner.php y ponerlo en una función. Digamos correr()

include_once __DIR__. '/Game.php'; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;  function run () $ notAWinner; $ aGame = new Game (); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; do $ dice = rand (0, 5) + 1; $ aGame-> roll ($ dice); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner); 

Esto nos permitirá probarlo, pero ten en cuenta que ejecutar el código desde la consola no ejecutará el juego. Hicimos un ligero cambio en el comportamiento. Obtuvimos la capacidad de prueba a costa de un cambio de comportamiento, lo que no queríamos hacer en primer lugar. Si desea ejecutar el código desde la consola, ahora necesitará otro archivo PHP que incluya o requiera al corredor y luego llame explícitamente al método de ejecución. No es un cambio tan grande, pero debe ser recordado, especialmente si tiene terceros que usan su código existente.

Por otro lado, ahora solo podemos incluir el archivo en nuestra prueba..

requiere __DIR__. '/… /Trivia/php/GameRunner.php';

Y luego llamar correr() dentro del método generateOutput ().

función privada generateOutput ($ semilla) ob_start (); srand ($ semilla); correr(); $ output = ob_get_contents (); ob_end_clean (); devuelve $ output; 

Estructura de directorio, archivos y nombres

Tal vez esta sea una buena oportunidad para pensar en la estructura de nuestros directorios y archivos. No hay condicionales más complejos en nuestra GameRunner.php, pero antes seguimos a la Game.php Archivo, no debemos dejar un lío atrás. Nuestro GameRunner.php Ya no está ejecutando nada, y necesitábamos hackear métodos juntos para hacerlo comprobable, lo que rompió nuestra interfaz pública. La razón de esto, es que tal vez estamos probando algo incorrecto.

Nuestras pruebas de prueba correr() en el GameRunner.php archivo, que incluye Game.php, Juega el juego y se genera un nuevo archivo maestro de oro. ¿Y si introducimos otro archivo? Hacemos el GameRunner.php para ejecutar realmente el juego llamando correr() y nada más. Entonces, ¿qué pasa si no hay una lógica allí que podría salir mal y no se necesitan pruebas, y luego movemos nuestro código actual a otro archivo??

Ahora esta es una historia completamente diferente. Ahora nuestras pruebas están accediendo al código justo debajo del corredor. Básicamente, nuestras pruebas son solo corredores. Y por supuesto en nuestra nueva. GameRunner.php Solo habrá una llamada para ejecutar el juego. Este es un verdadero corredor, no hace nada más que llamar al correr() método. Sin lógica significa que no se necesitan pruebas.

require_once __DIR__. '/RunnerFunctions.php'; correr();

Hay otras preguntas que podríamos hacernos en este punto. ¿Realmente necesitamos un RunnerFunctions.php? ¿No podríamos simplemente tomar las funciones desde allí y moverlas a Game.php? Probablemente podríamos, pero con nuestra comprensión actual de, ¿a qué función pertenece dónde? No es suficiente. Encontraremos un lugar para nuestro método en una próxima lección..

También intentamos asignar un nombre a nuestros archivos de acuerdo con lo que hace el código dentro de ellos. Una es solo un conjunto de funciones para el corredor, funciones que, en este punto consideramos que pertenecen juntas, para satisfacer las necesidades del corredor. ¿Se convertirá esto en una clase en un punto en el futuro? Tal vez. Tal vez no. Por ahora, es lo suficientemente bueno..

Limpieza de funciones del corredor

Si echamos un vistazo a la RunnerFunctions.php archivo, hay un poco de un lío que hemos introducido.

Definimos:

$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;

… dentro de correr() método. Tienen una sola razón para existir y un solo lugar donde se usan. ¿Por qué no simplemente definirlos dentro de ese método y deshacerse de los parámetros por completo??

function isCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; 

Ok, las pruebas están pasando y el código es mucho mejor. Pero no lo suficientemente bueno.

Condicionales negativos

Es mucho más fácil, para la mente humana, comprender el razonamiento positivo. Entonces, si puedes evitar los condicionales negativos, siempre debes tomar ese camino. En nuestro ejemplo actual, el método busca una respuesta incorrecta. Sería mucho más fácil entender un método que verifique la validez y lo niegue, cuando sea necesario..

function isCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId)! = $ wrongAnswerId; 

Se utilizó la refactorización del método Rename. Esto es de nuevo, bastante complicado si se usa a mano, pero en cualquier IDE es tan simple como golpear CTRL + r, o seleccionando la opción apropiada en el menú. Para hacer que nuestras pruebas pasen, también necesitamos actualizar nuestra declaración condicional con una negación.

if (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); 

Esto nos acerca un paso más a nuestra comprensión del condicional. Utilizando ! en un Si() declaración, en realidad ayuda. Destaca y resalta el hecho de que algo se niega allí. ¿Pero podemos revertir esto en orden, para evitar la negación por completo? si podemos.

if (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  else $ notAWinner = $ aGame-> wrongAnswer (); 

Ahora no tenemos ninguna negación lógica usando !, ni negación léxica al nombrar y devolver las cosas equivocadas. Todos estos pasos hicieron que nuestro condicional fuera mucho más fácil de comprender..

Condicionales en Game.php

Simplificamos al extremo., RunnerFunctions.php. Atacemos nuestro Game.php archiva ahora. Hay varias maneras de buscar condicionales. Si lo prefiere, puede escanear el código simplemente mirándolo. Esto es más lento, pero tiene el valor agregado de forzarlo a tratar de entenderlo secuencialmente.

La segunda forma obvia de buscar condicionales es hacer una búsqueda de "if" o "if (". Si formateó su código con las funciones integradas de su IDE, puede estar seguro de que todas las declaraciones condicionales tienen la la misma forma específica. En mi caso, hay un espacio entre el "si" y el paréntesis. Además, si utiliza la búsqueda integrada, los resultados encontrados se resaltarán en un color estridente, en mi caso amarillo.

Ahora que los tenemos todos iluminando nuestro código como un árbol de Navidad, podemos tomarlos uno por uno. Conocemos el simulacro, conocemos las técnicas que podemos usar, es hora de aplicarlas.

if ($ this-> inPenaltyBox [$ this-> currentPlayer])

Esto parece bastante razonable. Podríamos extraerlo en un método, pero habría un nombre para ese método para hacer que la condición sea más clara?

si ($ roll% 2! = 0) 

Apuesto que el 90% de todos los programadores pueden entender el problema en lo anterior. Si declaración. Estamos tratando de concentrarnos en lo que hace nuestro método actual. Y nuestro cerebro está conectado al dominio del problema. No queremos "iniciar otro hilo" para calcular esa expresión matemática a fin de comprender que solo verifica si un número es impar. Esta es una de esas pequeñas distracciones que pueden arruinar una deducción lógica difícil. Así que digo que vamos a extraerlo.

if ($ this-> isOdd ($ roll))

Eso es mejor porque se trata del dominio del problema y no requiere capacidad mental adicional.

if ($ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard)

Este parece ser otro buen candidato. No es tan difícil de entender como una expresión matemática, pero, de nuevo, es una expresión que necesita un procesamiento lateral. Me pregunto, ¿qué significa si la posición del jugador actual llegó al final del tablero? ¿No podemos expresar este estado de una manera más concisa? Probablemente podamos.

if ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))

Este es mejor. Pero lo que realmente sucede dentro de la Si? El jugador es reposicionado al comienzo del tablero. El jugador comienza una nueva "vuelta" en la carrera. ¿Y si en el futuro, tendremos una razón diferente para comenzar una nueva vuelta? Si nuestro Si cambio de declaración cuando cambiamos la lógica subyacente en el método privado? ¡Absolutamente no! Por lo tanto, vamos a cambiar el nombre de este método en lo que el Si representa, en lo que sucede, no lo que estamos comprobando.

if ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))

Cuando intente nombrar métodos y variables, siempre piense qué debe hacer el código y no qué estado o condición representa. Una vez que obtenga esto correctamente, el cambio de nombre de las acciones en su código disminuirá significativamente. Pero aún así, incluso un programador experimentado debe cambiar el nombre de un método al menos de tres a cinco veces antes de encontrar su nombre correcto. Así que no tengas miedo de golpear CTRL + r y renombrarlo frecuentemente. Nunca confirme sus cambios en el VCS del proyecto si no exploró los nombres de los métodos recién agregados y su código no se lee como una prosa bien escrita. El cambio de nombre es tan barato en nuestros días, que puede cambiar el nombre de las cosas solo para probar varias versiones y revertir con solo presionar un botón.

los Si El estado de cuenta en la línea 90 es el mismo que el anterior. Solo podemos reutilizar nuestro método extraído. ¡Voila, la duplicación eliminada! Y no se olvide de ejecutar sus pruebas ahora y luego, incluso cuando se refactoriza utilizando la magia de su IDE. Lo que nos lleva a nuestra siguiente observación. La magia, a veces, falla. Echa un vistazo a la línea 65.

$ lastPositionOnTheBoard = 11;

Declaramos una variable y la usamos solo en un solo lugar, como un parámetro de nuestro nuevo método extraído. Esto sugiere fuertemente que la variable debería estar dentro del método..

función privada playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; devuelve $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard; 

Y no olvides llamar al método sin ningún parámetro en tu Si declaraciones.

if ($ this-> playerShouldStartANewLap ())

los Si declaraciones en el pregunta() El método parece estar bien, así como los de currentCategory ().

if ($ this-> inPenaltyBox [$ this-> currentPlayer])

Esto es un poco más complicado, pero en el dominio y lo suficientemente expresivo..

if ($ this-> currentPlayer == count ($ this-> players))

Podemos trabajar en esto. Es obvio que los medios de comparación, si el jugador actual está fuera de límite. Pero como hemos aprendido anteriormente, queremos intención no estado.

if ($ this-> shouldResetCurrentPlayer ())

Eso es mucho mejor, y lo reutilizaremos en las líneas 172, 189 y 203. Duplicación, quiero decir triplicado, quiero decir cuadruplicación, eliminado!

Las pruebas están pasando y todo Si Las afirmaciones fueron evaluadas por su complejidad..

Pensamientos finales

Hay varias lecciones que se pueden aprender a partir de condicionales de refactorización. En primer lugar, ayudan a comprender mejor la intención del código. Luego, si nombra el método extraído para representar la intención correctamente, evitará futuros cambios de nombre. Encontrar la duplicación en la lógica es más difícil que encontrar líneas duplicadas de código simple. Puede haber pensado que deberíamos hacer una duplicación consciente, pero prefiero tratar con la duplicación cuando tengo pruebas de unidad en las que puedo confiar en mi vida. El Maestro Dorado es bueno, pero a lo más es una red de seguridad, no un paracaídas..

Gracias por leer y estar atentos para nuestro próximo tutorial cuando presentaremos nuestras primeras pruebas de unidad.