Estrategias para refactorización de código PHP inestable

Prueba de unidad y el código de refactorización legado PHP para facilitar las pruebas y mejorar la calidad del código

Con el crecimiento de PHP desde un lenguaje de scripting simple hasta ser un lenguaje de programación integral, ha habido un crecimiento paralelo de la complejidad de las bases de código de una aplicación PHP típica. Para controlar el soporte y el mantenimiento de estas aplicaciones, varias herramientas de prueba ayudan a automatizar este proceso. Un método, la prueba de unidades, le permite comprobar si código que usted escribe directamente está correcto. No obstante, con frecuencia las bases del código legado no se pueden adaptar a este tipo de pruebas. Este artículo trata estrategias para refactorizar código PHP común problemático para facilitar las pruebas usando herramientas populares para prueba de unidades, al tiempo que reduce las dependencias que mejoran su base de código.

John Mertic, Software Engineer, SugarCRM

author photoJohn Mertic es ingeniero de software en SugarCRM y cuenta con varios años de experiencia con aplicaciones web PHP. En SugarCRM, él se ha especializado en integración de datos y en arquitectura móvil y de interfaz de usuario. Es un ávido escritor que ha publicado en php|architect, IBM developerWorks y en el Apple Developer Connector, y es el autor del libro "The Definitive Guide to SugarCRM: Better Business Applications". También ha contribuido a varios proyectos de fuente abierta, más notablemente en el proyecto PHP, donde es el creador y quien mantiene el PHP Windows Installer



29-08-2011

Introducción

Volviendo 15 años al pasado del PHP, vemos que este ha crecido pasando de ser una alternativa simple y dinámica de lenguaje de scripting, a scripts CGI que fueron populares por algún tiempo, y al lenguaje de programación integral que es actualmente. A medida que el código base crece, las pruebas manuales se convierten en una tarea imposible, y cada cambio de código hecho, grande o pequeño, puede afectar toda la aplicación. Los efectos pueden ser tan simples como que una página no cargue o que un formulario no se guarde, o pueden ser también algo difícil de detectar o que sólo aparezca bajo ciertas circunstancias. Podría incluso hacer que un problema anterior de la aplicación reaparezca. Se han desarrollado varias herramientas de prueba para resolver estos problemas.

Un método popular se conoce como prueba funcional o de aceptación, el cual comprueba la aplicación mediante la interacción típica de usuario de la aplicación. Esta es una buena técnica para comprobar los diferentes procesos de la aplicación, pero puede ser un proceso muy lento y generalmente no hace un trabajo tan bueno como lo hace la comprobación de las clases y funciones de nivel inferior, para asegurar que están funcionando como se supone. Aquí es donde otro método de pruebas, la prueba por unidades, entra en juego. La meta es comprobar la funcionalidad del código subyacente de la aplicación para asegurar que tras la ejecución se den los resultados correctos. Con frecuencia, estas aplicaciones web "maduras" acumulan bastante código legado que con el tiempo puede ser difícil de comprobar, lo cual reduce la capacidad para que los equipos de desarrollo proporcionen un buen cubrimiento en pruebas para una aplicación. A esto se le conoce comúnmente como "código inestable". Vamos a ver cómo identificar esto en su aplicación y cómo arreglarlo.


Identificando el código inestable

Las áreas problema de su código base que son inestables a menudo no son notorias cuando se escribe el código. Cuando usted escribe código para una aplicación PHP, hay una tendencia a adaptarlo para la forma como fluye la solicitud web, tomando a menudo un enfoque más procedimental al diseño de aplicaciones. La urgencia para terminar el proyecto o las correcciones a la aplicación en una urgencia, puede hacer que los desarrolladores "corten las esquinas" para completar el código rápidamente. El código previo pobremente escrito o confuso puede agravar los problemas de inestabilidad de una aplicación, porque los desarrolladores a menudo tratarán de hacer la corrección menos riesgosa posible, incluso si esta agrava los problemas de soporte durante la marcha. Estas áreas de problema son todas muy inestables mediante herramientas de prueba por unidad sin tomar grandes medidas.


Funciones que dependen del estado global

Las variables globales son convenientes en las aplicaciones PHP. Estas le permiten a usted tener variables u objetos que se puedan inicializar temprano en su aplicación, y luego ser aprovechadas en cualquier parte en la aplicación. No obstante, esa flexibilidad tiene un precio, pues el uso pesado de variables globales es un problema común que se ve en código inestable. Esto lo podemos ver en el Listado 1.

Listado 1. Función que depende del estado global
<?php 
function formatNumber($number) 
{ 
    global $decimal_precision, $decimal_separator, $thousands_separator; 
     
    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 
     
    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator); 
}

Debido al uso de estas variables locales se presentan dos problemas diferentes. El primer problema es que usted necesita tener a cada una en cuenta en su prueba, asegurándose de establecerles valores válidos que la función espere. El segundo y mayor problema es que usted no puede alterar el estado de los testes subsecuentes e invalidar los resultados necesarios para assegurarse de que el estado global fue reinicializado para el modo que estaba antes de la ejecución del teste. PHPUnit tiene instalaciones que pueden hacer copia de seguridad de variables locales y restaurarlas después de que se ejecute su prueba, lo cual puede ayudar a aliviar el problema. No obstante, un mejor enfoque es proporcionar una forma para que la clase de prueba pase directamente valores para estas globales que el método pueda usar. El listado 2 muestra un ejemplo de cómo hacer esto.

Listado 2. Función corregida para permitir la alteración temporal de las variables globales
<?php 
function formatNumber($number, $decimal_precision = null, $decimal_separator = null, 
$thousands_separator = null) 
{ 
    if ( is_null($decimal_precision) ) global $decimal_precision; 
    if ( is_null($decimal_separator) ) global $decimal_separator; 
    if ( is_null($thousands_separator) ) global $thousands_separator; 
     
    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 
     
    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator);
}

Hacer esto no sólo ha permitido que el código se pueda comprobar más, sino que también ha hecho que no dependa de las variables globales del método. Esto puede abrir la posibilidad para refactorizar este código que resta para que no utilice las variables globales en absoluto.


Singletons que no se pueden restablecer

Las Singletons son clases que están diseñadas para tener solo una instancia existente al mismo tiempo en una aplicación. Estas son un patrón común utilizado para objetos globales de una aplicación, como conexiones de una base de datos y opciones de configuración. A menudo son consideradas tabú en una aplicación, lo cual muchos desarrolladores consideran es una distinción inmerecida, debido a la utilidad de tener un objeto para uso disponible siempre. Gran parte de esto proviene del uso excesivo de singletons, donde muchos de estos así llamados objetos de dios pueden ser imposibles de extenderse. Pero desde una perspectiva de pruebas, un gran problema es que a menudo son inmutables. Observemos el Listado 3 como un ejemplo.

Listado 3. Objeto Singleton que buscamos comprobar
<?php 
class Singleton 
{ 
    private static $instance; 
     
    protected function __construct() { } 
    private final function __clone() {} 
     
     
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 
         
        return self::$instance; 
    } 
}

Así que es posible ver que después de que se crea una instancia para el singleton por primera vez, cada llamado hecho al método getInstance() retornará el mismo objeto y no uno nuevo, lo cual puede convertirse en un gran problema si efectuamos cambios a ese objeto. La solución más fácil es añadir un método al objeto que pueda reiniciarlo. El Listado 4 muestra dicho ejemplo.

Listado 4. Objeto Singleton con un método de reinicio añadido
<?php 
class Singleton 
{ 
    private static $instance; 
     
    protected function __construct() { } 
    private final function __clone() {} 
     
     
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 
         
        return self::$instance; 
    } 
     
    public static function reset() 
    { 
        self::$instance = null; 
    } 
}

Ahora, llamamos al método de reinicio para que inicie cada ejecución de prueba, para asegurar que vamos a través del código de iniciación para el objeto singleton en cada ejecución de prueba. Tener este método disponible puede ser útil en la aplicación en general ahora que el singleton se torna mutable fácilmente.


Trabajando en el constructor de clase

Una buena práctica durante la prueba de unidad es comprobar solo y exactamente lo que usted desea, y evitar tener que configurar más objetos y variables que los absolutamente necesarios. Cada objeto y variable que usted establece es también uno que usted necesita remover después del hecho. Esto se convierte en un problema para elementos más molestos como archivos y tablas de bases de datos, donde si usted necesita modificar el estado debe ser cuidadoso en extremo para limpiar sus huellas después de que se complete la prueba. La mayor barrera para mantener esa regla intacta es el constructor del objeto en sí, el cual efectúa todo tipo de cosas que no son pertinentes para su prueba. Considere el Listado 5 a continuación como un ejemplo.

Listado 5. Clase con un método singleton extenso
<?php 
class MyClass 
{ 
    protected $results; 
     
    public function __construct() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable'); 
    } 
     
    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

Aquí, con el fin de probar el método fdfdfd en el objeto, terminamos necesitando configurar una conexión de base de datos, tener registros en la tabla y luego limpiar todos estos recursos después del hecho. Esto puede parecer una exageración cuando ninguno de estos necesita comprobar el método fdfdfd. Por lo tanto, vamos a modificar el constructor como se muestra en el Listado 6.

Listado 6. Clase modificada para saltase opcionalmente toda la lógica de inicialización innecesaria
<?php 
class MyClass 
{ 
    protected $results; 
     
    public function __construct($init = true) 
    { 
        if ( $init ) $this->init(); 
    } 
     
    public function init() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable');
    } 
     
    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

Hemos refactorizado una gran cantidad de código en el constructor para ponerlo en un método init(), que todavía será llamado predeterminadamente en el constructor para evitar romper cualquier código existente. No obstante, ahora podemos simplemente pasar una variable booleana falsa al constructor durante nuestras pruebas para evitar llamar al método init() y a toda la lógica de inicialización innecesaria. Esta refactorización de la clase también mejora el código de forma que hemos separado el código de inicialización del código de construcción del objeto.


Al tener dependencias de clase pesadamente codificadas

Como vimos en la sección previa, un gran problema del diseño de clases que dificulta la comprobación es tener que inicializar todo tipo de objetos que no son necesarios para su prueba. Previamente, vimos cómo la lógica de inicialización pesada puede añadir todo tipo de sobrecarga a la escritura de una prueba (especialmente cuando la prueba no necesita nada de ello para funcionar), pero otro problema puede ocurrir cuando creamos directamente nuevos objetos dentro de métodos de la clase que podemos estar probando. Observemos elListado 7 que contiene un ejemplo de dicho código problemático.

Listado 7. Clase con un método que inicializa directamente otro objeto
<?php 
class MyUserClass 
{ 
    public function getUserList() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $results = $dbconn->query('select name from user'); 
         
        sort($results); 
         
        return $results; 
    } 
}

Digamos que estamos probando el método getUserList de arriba, pero el enfoque de nuestra prueba es asegurarnos que la lista de usuarios retornada sea ordenada alfabéticamente. En este caso, el hecho de que podamos capturar los registros de la base de datos realmente no importa, pues lo que podríamos estar comprobando es nuestra capacidad para ordenar los registros que retornan. El problema es que como creamos una instancia directamente para un objeto de conexión de base de datos dentro del método, necesitamos hacer todo este trabajo de andamiaje para comprobar el método adecuadamente. Por lo tanto, hagamos un cambio para permitir que un objeto sea interpuesto, como se ve en el Listado 8.

Listado 8. Clase que tiene un método que inicializa a otro objeto directamente, pero que también ofrece una forma de evadir esto
<?php 
class MyUserClass 
{ 
    public function getUserList($dbconn = null) 
    { 
        if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) { 
            $dbconn = new DatabaseConnection('localhost','user','password'); 
        } 
        $results = $dbconn->query('select name from user'); 
         
        sort($results); 
         
        return $results; 
    } 
}

Ahora es posible pasar un objeto directamente que sea compatible con el objeto de conexión de base de datos esperado, y usará ese objeto en lugar de crear uno nuevo. El objeto que usted pasó simplemente puede ser un objeto simulado, es decir, uno donde codificáramos pesadamente varios de los valores de retorno de los métodos llamados para regresar directamente los datos que deseamos usar. En este caso, podríamos simular el método de consulta del objeto de conexión de base de datos de manera que pudiéramos retornar simplemente los resultados en lugar de llamar a la base de datos por ellos. Hacer este tipo de refactorización también puede mejorar el método, permitiendo a su aplicación interponer diferentes conexiones de base de datos en lugar de estar atados únicamente al predeterminado especificado.


Beneficios del código que se puede comprobar

Ciertamente, la escritura de código más estable tiene el beneficio obvio de facilitar la escritura de pruebas de unidad para su aplicación PHP (lo cual se ha visto en los ejemplos presentados en este artículo), pero también crea durante el proceso una aplicación mejor diseñada, más modular y más estable. Todos hemos visto diferentes niveles de código "espagueti" que entrelazan estrechamente lógica empresarial y de presentación en un gran desastre procedimental en aplicaciones PHP, lo cual indudablemente puede causar pesadillas de soporte a cualquier persona que profundice en ello. En el proceso de hacer al código comprobable, hemos refactorizado código problemático anterior; no sólo problemático en su diseño sino también en su función. Hemos abierto opciones para mejor reutilización de código al hacer que las funciones y clases sean menos de un solo propósito y más fáciles de usar por otras áreas de la aplicación al remover las dependencias fuertemente codificadas. Adicionalmente, hemos facilitado el soporte futuro del código base al remover el código de calidad pobre y reemplazándolo con código de mayor calidad.


Conclusión

En este artículo, buscamos hacer que el código PHP se pudiese comprobar mejor mediante varios ejemplos de código inestable que se encuentra típicamente en las aplicaciones PHP. Exploramos cómo las situaciones se presentan en la aplicación, y luego vimos cómo solucionar mejor el código problemático para posibilitar las pruebas. También vimos cómo hacer estos cambios a su código no sólo hacen que el código de pueda comprobar mejor, sino también la calidad mejorada del código en general y la reutilización del código potenciado en las secciones de código refactorizadas.


Descargar

DescripciónNombretamaño
Article source codecode_examples.zip3KB

Recursos

Aprender

Obtener los productos y tecnologías

  • Innove en su próximo proyecto de desarrollo de fuente abierta con software de prueba IBM, disponible para descarga o en DVD.

Comentar

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=Lotus
ArticleID=751806
ArticleTitle=Estrategias para refactorización de código PHP inestable
publish-date=08292011