Esta es la tercera parte de la traducción del manual de buenas prácticas de Testing en Etsy donde se aborda el tema del Legacy Code.
Legacy code
La mayoría del código fuente que ha sobrevivido normalmente no fué escrito con un diseño limpio o pensando en la usabilidad/reutilización. Puede tener estado global, o no tener ninguna abstracción ( de modo que, por ejemplo, todas las operaciones de base están en una clase o en un método aislado que no está relacionado con la base de datos).
Vamos a tener que utilizar la imaginación cuando nos encontramos con código como este. Al final, nuestro trabajo está por hacer, tenemos que diseñar el código de acuerdo a unas normas, independientemente de como interactue ahora.
Pongamos por ejemplo que tenemos el código legacy que busca una dirección de email de un usuario en la base de datos:
<?php
class LegacyCode {
private static function databaseLookUpOfEmailAddress($user) {
// Do MySQL stuff...
}
public static function getEmailAddress($user) {
return self::databaseLookUpOfEmailAddress($user);
}
}
?>
y nuestro código hace algo así:
<?php
class MyNewClass {
public function sendWelcomeEmail($user) {
$this->sendEmail($this->welcomeText, LegacyCode::getEmailAddress($user));
}
}
?>
Ahora nuestro código, a través de LegacyCode tiene una dependencia insalvable con una base de datos MySQL real. En otras palabras, no hay una abstracción de código para la parte de la base de datos. Es cierto, que es difícil de testear, ¿Por qué tenemos que configrar una instancia de MySQL solo para probar la funcion sendWelcomeEmail?
Un enfoque para intentar romper esas dependencias, es recubrir el código Legacy.
<?php
class MyNewClass {
public function __construct($emailLookerUpper) {
$this->emailLookerUpper = $emailLookerUpper;
}
public function sendWelcomeEmail($user) {
$this->sendEmail(
$welcomeText,
$this->emailLookerUpper->lookUpEmail($user));
}
}
class ProdEmailLookerUpper implements EmailLookerUpper {
public function lookUpEmail($user) {
return LegacyCode::getEmailAddress($user);
}
}
?>
Esta solución enmascara las entradas a código Legacy de la manera que más nos convenga. La desventaja aquí es que corremos el riesgo de crear muchas abstracciones a los largo del código.
Una alternativa podría ser tratar de mejorar el código Legacy. Empezando por escribir tests para cubrir el código Legacy. Este paso nos prepara para refactorizar el código de manera más segura. Si el código Legacy es difícil de probar, tendremos que sopesar el hecho de escribir complicados tests si lo que planeamos es refactorizar de todos modos.
En cuanto a la mejora real, podríamos mantener la abstracción dentro de la clase Legacy — aunque esto puede introducir más problemas de diseños de los que resuelve:
<?php
class LegacyCode {
private static $emailLookerUpper;
// Introduce the EmailLookerUpper abstraction into all this static code
// by using a static setter.
public static function setEmailLookerUpper($emailLookerUpper) {
self::$emailLookerUpper = $emailLookerUpper;
}
public static function getEmailAddress($user) {
return self::$emailLookerUpper->lookUpEmail($user);
}
}
class ProdEmailLookerUpper implements EmailLookerUpper {
public function lookUpEmail($user) {
// Do MySQL stuff from the old databaseLookUpOfEmailAddress() function...
}
}
?>
Utilizar variables globales/estáticas es horrible ya que añaden una gran cantidad de complejidad en nuestro código, además si las utilizamos en las entradas y/o salidas hacemos que sea muy fácil introducir enredos extraños entre partes no relacionadas de su código. Pero en este ejemplo podemos utilizar static de manera temporal para introducir una abstracción. Tenemos que tener cuidad, esta medida «temporal» puede ser por mucho tiempo si no hacemos un esfuerzo por realizar la migración. Al menos podemos probarlo.
Mientras estamos refactorizando, se deben añadir o adaptar los tests para la viaja funcionalidad que se ha rediseñado. Si no tenemos pruebas, estamos reescribiendo el código, en lugar de invertir en el proceso de mejora de su diseño.
Otra alternativa es simplemente refactorizar toda la clase LegacyCode con un mejor diseño orientado a objetos- solo si podemos hacerlo correctamente y si me merece la pena el esfuerzo. Pero cada vez que estemos tentados a destripar un pedazo de código heredado y reescribirlo, básicamente refactorizarlo, debemos hacer un análisis de coste-beneficio:
- ¿Cuánto tiempo tardamos en volver a escribir?
- ¿Qué características tendría que tener? ¿Cuánto tiempo tendremos que mantener esas carácteristicas sin tener esta reescritura?
- ¿De cuántos dolores de cabeza en mantenimiento nos libera? Consideremos el valor total de este beneficio a través de todo el equipo.
- ¿Cuántas clases necesitaríamos para cambiar la API reescrita? ¿Cuantas clases estane n un desarrollo activo y se beneficiarían de un uso más fácil?
Podría ser mejor solo realizar pequeñas mejoras para la tarea en la que estemos trabajando actualmente ¡y añadamos tests!.
Conclusiones finales
- En resumen, los tests nos ayudan a pensar sobre nuestro código en términos de entradas, salidas y partes
- La elección de la abstracción «correcta» puede ser difícil de decidir, así que tenemos que pensar como se verían nuestros tests se verían bajo un determinado conjunto de abstracciones.
- Experimente, tomemos el tiempo que necesitemos. Disfrutemos del proceso.
Intentemos el CodeLab
En el repositorio de Testing101 tenemos una aplicación práctica de estas ideas en un código real. Es altamente recomendable que trabajemos sobre ella