Detectar bugs en código

Con cada línea de código que agregamos a nuestros proyectos aumentamos su complejidad y la posibilidad de que los bugs comiencen a acecharnos cuando menos lo esperamos: unos minutos antes de una reunión con el cliente o un domingo cuando estamos en el cine lejos de nuestro computador. Para prevenir estas situaciones aterradoras te brindaré 7 consejos para escribir código más seguro:

1. Asigna nombres descriptivos a tus variables, funciones, parámetros, clases y métodos.

El código se escribe una sola vez pero debe leerse e interpretarse muchas veces, tanto por otros programadores como por ti. Por lo tanto vale la pena que le dediques unos segundos extras a pensar cómo nombrar esa nueva clase o método para que refleje su verdadera intención o contenido.

¿Qué código se entiende mejor?

<?php
$evento->add($req->q);
<?php

$event->addTickets($request->quantity);

La primera línea combina inglés y español, el método add no es claro sobre qué se está agregando. La variable $req no es lo suficientemente clara a simple vista ni tampoco es fácil ver que q significa cantidad.

El segundo ejemplo lo podría entender incluso alguien que no programe.

2. Usa un estándar como PSR-2 si trabajas con PHP

No subestimes la importancia de escribir código de forma ordenada y consistente, puesto que te permitirá detectar problemas con mayor facilidad:

<?php
//...
public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i)
    {
            $code = Code::generate(); }
            $this->tickets()->create(
                [
                'code' => $code,
            ]);
    }
//...
public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i) {
        $code = Code::generate();
    }

    $this->tickets()->create([
        'code' => $code,
    ]);
}

Los dos bloques de código anterior tienen el mismo bug: ambos crearán un solo ticket en vez de 2 o más. ¿En cuál bloque notaste el problema a simple vista? Imagina las posibles consecuencias de tener un código más complejo y mal indentado.

3. Reduce el uso de variables locales

Uno de los primeros conceptos que aprendemos en algoritmos es a declarar y usar variables temporales, sin embargo su abuso puede hacer un código mucho más difícil de leer. Considera estos 2 ejemplos:

<?php

$contact                           = array();
$contact['firstname']              = $user->first_name;
$contact['surname']                = $user->last_name;
$contact['id']                     = $user->id;
$contact_emails                    = array();
$contact_email                     = array();
$contact_email['email']            = $user->email;
$contact_emails[]                  = $contact_email;
$contact['emails']                 = $contact_emails;

$this->create('contact', $contact);
<?php

$contact = [
    'id' => $user->id,
    'firstname' => $user->first_name,
    'surname' => $user->last_name,
    'emails' => [
        [
            'email' => $user->email,
        ],
    ],
];

$this->create('contact', $contact);

¿En cuál fragmento resulta más fácil ver o cambiar la estructura final que se va a enviar?

Además es una mala práctica alinear los signos de igualdad en la asignación de variables, no solo contradice PSR-2 sino que puede además dificultar la lectura del código.

Volviendo al ejemplo de agregar tickets, el código se puede mejorar si eliminamos la variable $code y la colocamos en línea:

<?php

public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i) {
        $this->tickets()->create([
            'code' => Code::generate(6),
        ]);
    }
}

Sin embargo, en algunos casos puntuales podemos usar una variable local para mejorar la claridad del código:

<?php
//...
public function calculateTotal($price, $quantity, $deliveryCost)
{
    $subtotal = $price * $quantity;

    if ($subtotal < 30) {
        $subtotal += $deliveryCost;
    }

    return $subtotal;
}

Esto, en mi opinión, se lee un poco mejor que:

<?php

function calculateTotal($price, $quantity, $deliveryCost)
{
    if ($price * $quantity < 30) {
        return ($price * $quantity) + $deliveryCost;
    }

    return $price * $quantity;
}

4. Elimina el uso de números mágicos

Si tus órdenes que superen los 30 dólares no incurrirán en costo de entrega, deberías colocar el valor como una propiedad, constante o variable de configuración:

<?php

if ($subtotal < self::DELIVERY_COST_THRESHOLD) {
    $subtotal += $deliveryCost;
}

5. Divide y vencerás

Muchos ejemplos y escenarios se pueden mejorar separando el código en pequeños métodos, cada uno con una responsabilidad diferente. Por ejemplo:

En el método getContactInfo retornaremos el arreglo con la información de contacto directamente desde el modelo de usuarios:

<?php

$this->create('contact', $user->getContactInfo());

La Programación orientada a objetos nos dice que agrupemos datos y funcionalidad en un solo lugar (clases), en este caso armamos el arreglo con la información de contacto en el lugar donde tenemos los datos del usuario (el modelo User).

Veamos otro ejemplo:

<?php

$subtotal = $item->price * $quantity;
$subtotal = $this->addDeliveryCost($subtotal);

El método addDeliveryCost retornará el subtotal con el costo de la entrega solo si el subtotal no excede de cierta cantidad, sino retornará el subtotal original:

Con este nuevo método podemos colocar el código en línea una vez más y nuestro código queda simple y legible:

<?php
//...
return $this->addDeliveryCost($item->price * $quantity);

6. Aplica soluciones simples

Muchos tutoriales que prometen enseñarte a escribir mejor código solamente te enseñan a complicar tu código.

Por ejemplo, si necesitas crear un usuario con Laravel y Eloquent, el fantasma del SRP te dice que está mal que coloques esto en tu controlador:

<?php

User::create([
    'name' => $request->name,
    'email' => $request->email,
    'password' => bcrypt($request->password),
]);

Debes escribir esto, que es “mucho mejor”:

<?php

$this->commandTransport->handleCommand(
    new UserCreationCommand(
        new UserNameField($request->name),
        new UserEmailField($request->email),
        new UserPasswordField(bcrypt($request->password)),
    )
);

Dentro de la clase UserCommandHandler no puedes crear el usuario con Eloquent, nope, tienes que pasar la información un repositorio:

<?php

class UserCreationCommandHandler
{
    //...

    public function handle(UserCreationCommand $command)
    {
        $this->userRepository->create(
           $command->name,
           $command->email,
           $command->password,
        );
    }
}

Eventualmente… dentro de UserEloquentRepository vas, por supuesto, a llamar a User::create solo que 3 capas después:

<?php

class UserEloquentRepository implements UserRepository
{
    //...

    public function create(
        UserNameField $name,
        UserEmailField $email,
        UserPasswordField $password
    ) {
        return User::create([
            'name' => $name->getValue(),
            'email' => $email->getValue(),
            'password' => bcrypt($password->getValue()),
        ]);
    }

}

Si luego de 1 hora de trabajo el cliente te llama y te pide que separes el nombre en 2 campos (first_name y last_name) ¿Cuál ejemplo crees que te llevará más tiempo de arreglar?

¿En cuál ejemplo es más posible que cometas un error sin querer (por ejemplo que olvides pasar un campo de un lado a otro)?

¡Por cierto en el ejemplo con comandos y repositorios llamé a la función bcrypt 2 veces sin que te dieras cuenta! Muahahaha…

Usar una docena de interfaces y clases no va a prevenir que cometas un error, en cualquiera de los ejemplos necesitas probar tu código y hablando del tema:

7. Prueba tu código de forma automatizada (usa pruebas unitarias)

Los contadores tienen una práctica llamada “Contabilidad de partida doble” que les permite reducir errores mediante la introducción de todas las operaciones 2 veces.

Escribir pruebas unitarias muchas veces requiere que escribamos el código 2 veces, una vez para definir cada prueba:

<?php

function test_order_without_delivery_cost()
{
    $order = new Order;
    $order->addItem(new Item(['price' => 20]), 5);

    $expectedTotal = 20 * 5;
    $this->assertSame($expectedTotal, $order->getTotal());
}

function test_order_with_delivery_cost()
{
    $order = new Order;
    $order->addItem(new Item(['price' => 20]), 1);

    $expectedTotal = 20 + DELIVERY_COST;
    $this->assertSame(expectedTotal, $order->getTotal());
}

Y una segunda vez para definir el código de producción (que lo dejo de tu parte como un ejercicio).

Muchos programadores se quejan de esta práctica porque en teoría nos hace “trabajar doble”, pero al «escribir doble» reducimos dramáticamente la posibilidad de equivocarnos dos veces de la misma manera (si nos equivocamos de 2 formas diferentes la prueba va a fallar), es por esto que proyectos que usan pruebas unitarias tienden a tener menos bugs y en consecuencia requerirán de muchas menos horas de terror depuración tratando de encontrar esos bugs que nos atormentan y aparecen en nuestras pesadillas donde clientes furiosos nos persiguen sin descanso.

Así que todos estos consejos (entre muchos otros que no podría cubrir en un solo post) te permitirán escribir código menos propenso a errores, más fácil de mantener, extender e incluso re-usar en proyectos similares.

Si quieres aprender más sobre estas técnicas y muchas otras, te recomiendo que me acompañes en los siguientes cursos:

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