Banner funciones PHP

Vi esta pregunta hace poco en un foro de programación y me respuesta fue que en teoría una función debería contener unas 5 líneas de código o menos. Incluso he visto funciones muy útiles que contienen una sola línea de código. Si esta regla te parece exagerada o quieres saber cómo puedes escribir procedimientos completos en funciones de tan pocas líneas, por favor acompáñame en el resto del artículo:

Esta semana he estado escribiendo una nueva versión del componente Styde\Html que NO va a extender del paquete LaravelCollective\Html. Por ende he estado diseñando clases de más bajo nivel que serán capaces de generar código HTML desde cero.

Mi primera meta fue crear un método como el siguiente:

<?php

Html::tag('span', 'This is a span', ['id' => 'my-span'])

Que genere el código:

<span id="my-span">This is a span</span>

Para poder probar que la función me devuelve el código HTML esperado sin tener que abrir y revisar el resultado en el navegador a cada rato -y poder refactorizar más adelante- escribí la siguiente prueba unitaria con PHPUnit:

/** @test */
function it_generates_html_tags()
{
    $this->assertEquals(
        '<span id="my-span">This is a span</span>',
        $this->htmlBuilder->tag('span', 'This is a span', ['id' => 'my-span'])->render()
    );

    $this->assertEquals(
        '<input readonly="readonly" type="text">',
        $this->htmlBuilder->tag('input', false, ['type' => 'text', 'readonly'])->render()
    );
}

Aprende más sobre pruebas en Introducción al diseño de clases con pruebas unitarias en PHPUnit.

Escribir la lógica de un método puede parecerte una tarea difícil o no, dependiendo de la complejidad del método y de tu experiencia como programador. Una técnica que yo uso para simplificar el trabajo es dividir una tarea grande en varias tareas pequeñas. Por ejemplo, para generar una etiqueta HTML debemos:

  • Imprimir el código para abrir una etiqueta.
    • Imprimir los atributos HTML dentro de la etiqueta de apertura.
      • Algunos atributos serán pares (como type="text") y otros impares como readonly.
  • Imprimir el contenido de la etiqueta (puede ser texto y/o otras etiquetas HTML).
  • Imprimir el cierre de la etiqueta.
  • Algunas etiquetas no tendrán contenido ni cierre, como por ejemplo la etiqueta input

Esto nos puede llevar al siguiente resultado:

public function render()
{
    // Comenzamos a concatenar la etiqueta de apertura
    $result = '<'.$this->tag;

    // Concatenamos los atributos
    foreach ($this->attributes as $name => $value) {
        // Si el nombre del atributo es numerico quiere decir que se trata de un atributo sin valor como disabled o readonly
        if (is_numeric($name)) {
            $result .= " $value";
        } else {
            $result .= ' '.$name.'="'.htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false).'"';
        }
    }

    // IMPORTANTE: Cerramos la etiqueta de apertura
    $result .= '>';

    // Si el contenido esta marcado como falso entonces quiere decir que esta etiqueta no tiene contenido ni cierre
    if ($this->content === false) {
        return $result;
    }

    // Vamos a concatenar el contenido de la etiqueta (puede ser un array de etiquetas o una cadena)
    foreach (Arr::wrap($this->content) as $content) {
        $result .= e($content);
    }

    // Por ultimo concatenamos el cierre de la etiqueta
    $result .= '</'.$this->tag.'>';

    // Retornamos la etiqueta
    return $result;
}

Esta función, excluyendo su declaración, espacios en blanco y comentarios, tiene 17 líneas de código. Si tienes un muy buen monitor con muy buena resolución quizás seas capaz de visualizar toda la función sin tener que hacer scroll:

Vista del código monitor con resolución de 2048 x 1152

Por supuesto podría comprimir más el código, eliminando comentarios y espacios en blanco:

Una función larga sin líneas en blanco ni comentarios ¿Es más difícil o fácil de leer?

Como puedes ver no es un método que sea agradable a la vista ni fácil de leer ni entender.

Pero volvamos a la parte del artículo donde yo proponía dividir la tarea de generar una etiqueta HTML en pequeñas tareas más sencillas:

  • Abrir una etiqueta.
    • Generar atributos HTML.
  • Imprimir contenido de la etiqueta (si aplica).
  • Cerrar la etiqueta (si aplica).

Divide y vencerás

¿Qué tal si dividimos estas pequeñas tareas también en pequeños métodos y hacemos que el método render se encargue simplemente de llamar a estos nuevos métodos?

Rediseñemos el método render:

/**
 * Render the HTML element.
 *
 * @return string
 */
public function render()
{
    // Render a single tag.
    if ($this->content === false) {
        return $this->renderOpenTag();
    }

    // Render a paired tag.
    return $this->renderOpenTag().$this->renderContent().$this->renderCloseTag();
}

¡Ahora tenemos un pequeño método de tan sólo 4 líneas de código! Este método se puede leer sin scroll aunque por alguna razón estés trabajando en un monitor CRT de 13» como el que yo tenía cuando estaba aprendiendo a programar.

Por supuesto aún tenemos que definir y escribir el resto de los métodos:

Para imprimir la apertura de una etiqueta:

protected function renderOpenTag()
{
    return '<'.$this->tag.$this->renderAttributes().'>';
}

Nota que aquí a su vez estoy delegando al método renderAttributes():

public function renderAttributes()
{
    $result = '';

    foreach ($this->attributes as $name => $value)
    {
        if (is_numeric($name)) {
            $result .= ' '.$value;
        } else {
            $result .= ' '.$name.'="'.htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false).'"';
        }
    }

    return $result;
}

Este método renderAttributes por si solo ocupa 10 líneas de código y nota que el código dentro de los condicionales tiene bastantes niveles de sangrado*.

*Según la Real Academia Española deberíamos decir «sangrado» en vez de «indentado» lo cuál para mí suena más correcto puesto que siento que mis ojos comienzan a sangrar cuanto más sangrado está un código a la derecha).

¿Cómo podemos evitar tener varios niveles de sangrado indentación en nuestro código?

El método renderAttributes debe encargarse de varias tareas: recorrer el listado de atributos por un lado para generarlos, concatenarlos y finalmente retornar el resultado, pero como puedes ver generar atributos incluye algo de lógica extra ¿Podríamos pasar esta lógica a un nuevo método, quizás renderAttribute (en singular)?

public function renderAttributes()
{
    $result = '';

    foreach ($this->attributes as $name => $value)
    {
        $result .= ' '.$this->renderAttribute($name);
    }

    return $result;
}

protected function renderAttribute($name)
{
    if (is_numeric($this->attributes[$name])) {
        return $value;
    }

    return $name.'="'.htmlspecialchars($this->attributes[$name], ENT_QUOTES, 'UTF-8', false).'"';
}

Ahora el método renderAttributes tiene tan sólo 6 líneas de código, aunque siendo creativos podríamos reducirlo aún más:

public function renderAttributes()
{
    return array_reduce(array_keys($this->attributes), function ($result, $name) {
        return $result.' '.$this->renderAttribute($name);
    }, '');
}

En este caso estoy usando la función array_reduce de PHP para «reducir» el array de atributos a simplemente una cadena de texto y con esto ¡Ahora el método renderAttributes técnicamente contiene una sólo línea de código! Aunque podemos ver 3 líneas en realidad hay una única línea que es el llamado a la función array_reduce con el callback y el resto de los argumentos. Incluso dentro del callback tenemos también una sola línea de código.

Finalmente necesitamos el código para imprimir el contenido y el cierre de una etiqueta, intenta diseñarlos por ti mismo. Yo te mostraré el código del resultado final:

public function render()
{
    // Render a single tag.
    if ($this->content === false) {
        return $this->renderOpenTag();
    }

    // Render a paired tag.
    return $this->renderOpenTag().$this->renderContent().$this->renderCloseTag();
}

protected function renderOpenTag()
{
    return '<'.$this->tag.$this->renderAttributes().'>';
}

public function renderAttributes()
{
    return array_reduce(array_keys($this->attributes), function ($result, $name) {
        return $result.' '.$this->renderAttribute($name);
    }, '');
}

protected function renderAttribute($name)
{
    if (is_numeric($name)) {
        return $this->attributes[$name];
    }

    return $name.'="'.htmlspecialchars($this->attributes[$name], ENT_QUOTES, 'UTF-8', false).'"';
}

public function renderContent()
{
    return implode('', array_map('e', (array) $this->content));
}

protected function renderCloseTag()
{
    return '</'.$this->tag.'>';
}

Como puedes ver a pesar de que ahora tenemos 6 métodos en vez de 1 solo, todos nuestros métodos tienen 4 líneas o menos, incluso refactorizando logré hacer que algunos métodos ocupen incluso una sola linea de código. Es importante notar que en todos los pasos que he hecho a lo largo del tutorial he mantenido las pruebas unitarias en verde:

Sin las pruebas unitarias que agregué al inicio, hacer esta refactorización sería riesgosa, puesto que podría introducir bugs que no estaban antes*.

*Aún con pruebas unitarias o manuales, siempre hay posibilidades de introducir bugs por cada mínimo cambio que hagamos al código, con pruebas por supuesto el riesgo es mucho menor, por lo tanto vale la pena escribirlas.

Hablando de pruebas unitarias, sería muy fácil ahora escribir pruebas para cada paso de la cadena:

/** @test */
function it_closes_html_tags()
{
    $htmlElement = new HtmlElement('span');

    $this->assertEquals('', (string) $htmlElement->close());
}

En este caso te recomendaría escribir pruebas para los métodos que son públicos en la clase.

Además es importante notar que aunque ahora tenemos 6 métodos en vez de 1, nosotros podemos utilizar estos métodos por separado, para imprimir únicamente la etiqueta de apertura / cierre de un elemento HTML o generar únicamente los atributos, esto podemos ahora lograrlo sin tener que repetir el código por lo tanto aplicamos otro principio llamado DRY (don’t repeat yourself).

Por último además fíjate que los nombres de los métodos pueden actuar como una buena documentación de lo que está sucediendo dentro del código del método y esto es especialmente efectivo en métodos cortos. Ahora ya no necesitamos un comentario como el siguiente:

// Vamos a concatenar el contenido de la etiqueta (puede ser un array de etiquetas o una cadena)

Puesto que el nombre del método renderContent habla por sí solo, o en todo caso podríamos agregar más comentarios en el encabezado del método, como un DocBlock:

/**
 * Render the content of the tag. It can include other HTML elements or just simple text. The text will be escaped by default.
 * 
 * @return string
 */
public function renderContent()
{
    return implode('', array_map('e', (array) $this->content));
}

¿Realmente la refactorización nos ayuda a mejorar nuestro código?

Podrías argumentar -y con toda razón- que la versiones anteriores de renderAttributes y sobretodo de renderContent, eran más fáciles de leer que el resultado actual, a pesar de que me parece innegable que la refactorización como un todo ha hecho que nuestro método render quede mucho mejor, la mejora del método renderContent me parece debatible, en este caso no hay reglas fijas, sino consejos que aplican a cada situación. Muchas veces puedes empeorar tu código intentando mejorarlo, así que debes tener mucho cuidado. Te recomiendo primero que nada que escribas pruebas unitarias y segundo que no temas presionar CONTROL + Z si en algún momento consideras que estás empeorando el resulta y sobretodo que guardes tu código en un control de versiones (como Git) para que puedas resguardar tu progreso, deshacer algunos cambios con mayor facilidad y evitar pérdida de código.

Ejercicio

Como puedes ver en el método render, estoy utilizando la propiedad content como una bandera para determinar cuando se debe o no imprimir la etiqueta de cierre, esto puede considerarse una mala práctica por varias razones, una de ellas es que es difícil entender este comportamiento cuando declaremos un nuevo elemento con new HtmlElement('input', false). A simple vista cuesta entender qué significa que pasemos un valor falso como segundo argumento. ¿Cómo podrías mejorar esto? Puedes compartir tus ideas y soluciones en los comentarios o, si tienes cuenta en Styde, usando el canal #php de nuestro Slack.

Continua aprendiendo en styde.net

También puedes continuar aprendiendo con nosotros con los siguientes cursos: Curso de programación orientada a objetos con PHP, Curso de Refactorización con PHP y Curso de Git y suscribirte a nuestro boletín para recibir contenido para mejorar tus habilidades de programación:

Suscríbete a nuestro boletín

Te enviaremos publicaciones con consejos útiles y múltiples recursos para que sigas aprendiendo.

Además si ya eres parte de Styde, puedes unirte ya mismo al canal #styde_html de nuestro canal de Slack para participar en el desarrollo de la nueva versión de este componente y aprender mucho más sobre refactorización y pruebas. Eres bienvenido, no importa tu nivel actual de programación:

Únete a nuestra comunidad en Discord y comparte con los usuarios y autores de Styde, 100% gratis.

Únete hoy

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.