Código heredado de refactorización - Parte 10 Disección de métodos largos con extracciones

En la sexta parte de nuestra serie, hablamos sobre atacar métodos largos aprovechando la programación en pares y viendo el código desde diferentes niveles. Continuamos acercándonos y alejándonos, y observamos tanto cosas pequeñas como los nombres como la forma y la sangría..

Hoy, adoptaremos otro enfoque: asumiremos que estamos solos, no tenemos compañeros ni parejas que nos ayuden. Usaremos una técnica llamada "Extraer hasta que caigas" que divide el código en partes muy pequeñas. Haremos todos los esfuerzos posibles para hacer que estas piezas sean lo más fáciles de entender posible para que el futuro, nosotros o cualquier otro programador, pueda entenderlas fácilmente..


Extraer hasta que caigas

La primera vez que escuché este concepto fue de Robert C. Martin. Presentó la idea en uno de sus videos como una manera simple de refactorizar el código que es difícil de entender..

La idea básica es tomar fragmentos de código pequeños y comprensibles y extraerlos. No importa si identifica cuatro líneas o cuatro caracteres que se pueden extraer. Cuando identificas algo que puede encapsularse en un concepto más claro, extraes. Continúa este proceso tanto en el método original como en las piezas recién extraídas hasta que no puedas encontrar ninguna pieza de código que pueda encapsularse como un concepto..

Esta técnica es particularmente útil cuando trabajas solo. Te obliga a pensar tanto en fragmentos de código pequeños como en grandes. Tiene otro efecto agradable: te hace pensar en el código, ¡mucho! Además del método de extracción o refactorización de variables mencionado anteriormente, se encontrará renombrando variables, funciones, clases y más.

Veamos un ejemplo en algún código aleatorio de Internet. Stackoverflow es un buen lugar para encontrar pequeñas piezas de código. Aquí hay uno que determina si un número es primo:

// Compruebe si un número es la función principal isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ i

En este punto, no tengo idea de cómo funciona este código. Acabo de encontrarlo en Internet mientras escribo este artículo, y lo descubriré junto con usted. El proceso que sigue puede no ser el más limpio. En su lugar, reflejará mi razonamiento y refactorización a medida que sucede, sin planificación previa..

Refactorización del verificador de números primos

Según Wikipedia:

Un número primo (o un primo) es un número natural mayor que 1 que no tiene divisores positivos aparte de 1 y sí mismo. 

Como puede ver, este es un método simple para un problema matemático simple. Vuelve cierto o falso, por lo que también debería ser fácil de probar.

la clase IsPrimeTest extiende PHPUnit_Framework_TestCase function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1));  // Comprueba si un número es la función principal isPrime ($ num, $ pf = null) // ... el contenido del método como se ve arriba

Cuando solo estamos jugando con código de ejemplo, la forma más fácil de hacerlo es poner todo en un archivo de prueba. De esta manera, no tenemos que pensar qué archivos crear, a qué directorios pertenecen o cómo incluirlos en el otro. Este es solo un ejemplo simple para usar para familiarizarnos con la técnica antes de aplicarla en uno de los métodos de juego de trivia. Por lo tanto, todo va en un archivo de prueba, puede nombrar como desee. He elegido IsPrimeTest.php.

Esta prueba pasa. Mi siguiente instinto es agregar unos cuantos números primos más y luego escribir otra prueba sin números primos.

function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ this-> assertTrue (isPrime (2)); $ this-> assertTrue (isPrime (3)); $ this-> assertTrue (isPrime (5)); $ this-> assertTrue (isPrime (7)); $ this-> assertTrue (isPrime (11)); 

Eso pasa. Pero que hay de esto?

function testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6)); 

Esto falla inesperadamente: 6 no es un número primo. Esperaba que el método volviera falso. No sé cómo funciona el método, o el propósito de la $ pf parámetro - simplemente esperaba que volviera falso Basado en su nombre y descripción. No tengo ni idea de por qué no funciona ni cómo solucionarlo..

Este es un dilema bastante confuso. ¿Qué debemos hacer? La mejor respuesta es escribir pruebas que pasen para un volumen decente de números. Es posible que tengamos que intentarlo y adivinar, pero al menos tendremos una idea de lo que hace el método. Entonces podemos empezar a refactorizarlo..

function testFirst20NaturalNumbers () for ($ i = 1; $ i<20;$i++)  echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";  

Eso produce algo interesante:

1 - verdadero 2 - verdadero 3 - verdadero 4 - verdadero 5 - verdadero 6 - verdadero 7 - verdadero 8 - verdadero 9 - verdadero 10 - falso 11 - verdadero 12 - falso 13 - verdadero 14 - falso 15 - verdadero 16 - falso 17 - cierto 18 falso 19 verdadero

Aquí comienza a surgir un patrón. Todo verdadero hasta 9, luego alternando hasta 19. ¿Pero este patrón se repite? Intenta ejecutarlo para 100 números e inmediatamente verás que no lo es. En realidad, parece estar funcionando para números entre 40 y 99. Falló una vez entre 30-39 al nominar a 35 como primer. Lo mismo es cierto en el rango de 20-29. 25 es considerado primo.

Este ejercicio que comenzó como un código simple para demostrar una técnica resulta ser mucho más difícil de lo esperado. Sin embargo, decidí mantenerlo porque refleja la vida real de una manera típica..

¿Cuántas veces comenzaste a trabajar en una tarea que parecía fácil solo para descubrir que es extremadamente difícil??

No queremos arreglar el código. Cualquiera que sea el método, debe seguir haciéndolo. Queremos refactorizarlo para que otros lo entiendan mejor..

Como no dice los números primos de manera correcta, usaremos el mismo enfoque de Maestro Dorado que aprendimos en la Lección Uno..

función testGenerateGoldenMaster () para ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);  

Corre esto una vez para generar el Maestro Dorado. Debería correr rápido. Si necesita volver a ejecutarlo, no olvide eliminar el archivo antes de ejecutar la prueba. De lo contrario, la salida se adjuntará al contenido anterior..

function testMatchesGoldenMaster () $ goldenMaster = file (__ DIR__. '/IsPrimeGoldenMaster.txt'); para ($ i = 1; $ i<10000;$i++)  $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'El valor'. $ actualResult. 'no está en el maestro dorado.'); 

Ahora escribe la prueba para el maestro de oro. Es posible que esta solución no sea la más rápida, pero es fácil de entender y nos dirá exactamente qué número no coincide si se rompe algo. Pero hay una pequeña duplicación en los dos métodos de prueba que podríamos extraer en un privado método.

la clase IsPrimeTest extiende PHPUnit_Framework_TestCase function testGenerateGoldenMaster () $ this-> markTestSkipped (); para ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND);  function testMatchesGoldenMaster () $ goldenMaster = file (__ DIR__. '/IsPrimeGoldenMaster.txt'); para ($ i = 1; $ i<10000;$i++)  $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'El valor'. $ actualResult. 'no está en el maestro dorado.');  función privada getPrimeResultAsString ($ i) return $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\norte"; 

Ahora podemos pasar a nuestro código de producción. La prueba se ejecuta en unos dos segundos en mi computadora, por lo que es manejable.

Extraer todo lo que podamos

Primero podemos extraer un isDivisible () Método en la primera parte del código..

if (! is_array ($ pf)) para ($ i = 2; $ i

Eso nos permitirá reutilizar el código en la segunda parte de esta manera:

 else $ pfCount = count ($ pf); para ($ i = 0; $ i<$pfCount;$i++)  if(isDivisible($num, $pf[$i]))  return false;   return true; 

Y tan pronto como comenzamos a trabajar con este código, observamos que está alineado de manera descuidada. Los frenos están a veces al principio de la línea, otras veces al final. 

A veces, las pestañas se utilizan para la sangría, a veces espacios. A veces hay espacios entre el operando y el operador, a veces no. Y no, este no es un código creado especialmente. Esto es la vida real. Código real, no un ejercicio artificial..

// Compruebe si un número es la función principal isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ i < intval(sqrt($num)); $i++)  if (isDivisible($num, $i))  return false;   return true;  else  $pfCount = count($pf); for ($i = 0; $i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;  

Eso se ve mejor. Inmediatamente los dos Si Las declaraciones parecen muy similares. Pero no podemos extraerlos debido a la regreso declaraciones Si no volvemos romperemos la lógica.. 

Si el método extraído devolvería un valor booleano y lo comparamos para decidir si deberíamos o no regresar de isPrime (), Eso no ayudaría en absoluto. Puede haber una manera de extraerlo usando algunos conceptos de programación funcional en PHP, pero tal vez más adelante. Primero podemos hacer algo más simple..

function isPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else $ pfCount = count ($ pf); para ($ i = 0; $ i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;   function checkDivisorsBetween($start, $end, $num)  for ($i = $start; $i < $end; $i++)  if (isDivisible($num, $i))  return false;   return true; 

Extrayendo el para El bucle en su conjunto es un poco más fácil, pero cuando intentamos reutilizar nuestro método extraído en la segunda parte del Si Podemos ver que no funcionará. Hay esta misteriosa $ pf Variable de la que no sabemos casi nada.. 

Parece que comprueba si el número es divisible por un conjunto de divisores específicos en lugar de llevar todos los números hasta el otro valor mágico determinado por intval (sqrt ($ num)). Tal vez podríamos cambiar el nombre $ pf dentro $ divisores.

function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Esta es una manera de hacerlo. Agregamos un parámetro adicional, opcional, a nuestro método de verificación. Si tiene un valor, lo usamos, de lo contrario usamos $ i.

¿Podemos extraer algo más? ¿Qué pasa con este pedazo de código: intval (sqrt ($ num))?

function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  función integerRootOf ($ num) return intval (sqrt ($ num)); 

¿No es eso mejor? Algo. Es mejor que la persona que viene detrás de nosotros no sepa qué. intval () y sqrt () están haciendo, pero no ayuda a hacer que la lógica sea más fácil de entender. ¿Por qué terminamos nuestro para bucle en ese número específico? Tal vez esta es la pregunta que nuestro nombre de función debe responder.

[PHP] // Verifique si un número es una función principal isPrime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  function maximumPossibleFactor ($ num) return intval (sqrt ($ num));  [PHP]

Eso es mejor ya que explica por qué nos detenemos allí. Quizás en el futuro podamos inventar una fórmula diferente para determinar ese número. El nombramiento también introdujo un poco de inconsistencia. Llamamos a los factores numéricos, que es sinónimo de divisores. Tal vez deberíamos elegir uno y usar eso solo. Te dejaré hacer la refactorización de cambio de nombre como un ejercicio..

La pregunta es, ¿podemos extraer algo más? Bueno, hay que intentarlo hasta que nos caemos. Mencioné el lado de programación funcional de PHP unos párrafos arriba. Existen dos características principales de programación funcional que podemos aplicar fácilmente en PHP: funciones de primera clase y recursión. Cada vez que veo un Si declaración con un regreso dentro de una para bucle, como en nuestro checkDivisorsBetween () Método, pienso en aplicar una o ambas técnicas..

function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Pero ¿por qué deberíamos pasar por un proceso de pensamiento tan complejo? La razón más molesta es que este método hace dos cosas distintas: realiza ciclos y decide. Solo quiero que sea un ciclo y dejar la decisión a otro método. Un método siempre debe hacer una sola cosa y hacerlo bien..

function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) $ numberIsNotPrime = function ($ num, $ divisor) if (es divisible ($ num, $ divisor)) return false; ; para ($ i = $ inicio; $ i < $end; $i++)  $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i);  return true; 

Nuestro primer intento fue extraer la condición y la declaración de retorno en una variable. Esto es local, por el momento. Pero el código no funciona. En realidad el para El bucle complica las cosas un poco. Tengo la sensación de que un poco de recursión ayudará.

function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true; 

Cuando pensamos en la recursividad siempre debemos comenzar con los casos excepcionales. Nuestra primera excepción es cuando llegamos al final de nuestra recursión..

function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisor)) return false; 

Nuestro segundo caso excepcional que romperá la recursión es cuando el número es divisible. No queremos continuar. Y eso es sobre todos los casos excepcionales..

ini_set ('xdebug.max_nesting_level', 10000); function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors);  function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false;  checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors); 

Este es otro intento de utilizar la recursión para nuestro problema, pero desafortunadamente, recurrir 10.000 veces en PHP provoca un bloqueo de PHP o PHPUnit en mi sistema. Así que este parece ser otro callejón sin salida. Pero si hubiera estado funcionando, hubiera sido un buen reemplazo de la lógica original.


Reto

Cuando escribí el Golden Master, intencionalmente pasé por alto algo. Digamos que las pruebas no cubren tanto código como deberían. ¿Puedes ver el problema? Si es así, ¿cómo lo abordarías??


Pensamientos finales

"Extraer hasta que caigas" es una buena manera de analizar métodos largos. Te obliga a pensar en pequeñas piezas de código y a darles un propósito al extraerlas en métodos. Me parece sorprendente cómo este simple procedimiento, junto con el cambio de nombre frecuente, puede ayudarme a descubrir que un código hace cosas que nunca creí posibles..

En nuestro siguiente y último tutorial sobre refactorización, aplicaremos esta técnica al juego de preguntas. Espero que les haya gustado este tutorial que resultó ser un poco diferente. En lugar de hablar sobre ejemplos de libros de texto, tomamos un código real y tuvimos que luchar con los problemas reales que enfrentamos todos los días..