Código limpio: Gestión de errores

Código limpio: Gestión de errores

Esta entrada está disponible en video

Puedes encontrar una versión en video de esta entrada en nuestro canal de youtube.

Código Limpio

El libro Clean Code o Código Limpo es considerado por muchos como "la biblia" del desarrollo de software. Probablemente esta es una afirmación exagerada, sin embargo no se puede negar que es un gran libro que merece la pena leer. En esta serie de entradas analizamos los principales capítulos del libro clean code publicado por Robert C Martin en el año 2008.

Se han escrito mil entradas sobre el que se considera uno de los más importantes libros sobre filosofía de desarrollo de software, pero nosotros en esta serie de entradas queremos profundizar un poco más en cada uno de los capítulos. Si te dedicas a programar y te interesa mejorar en tu profesión quedate. A lo largo de esta serie de entradas vamos a aprender un montón de cosas.


Si lo deseas puedes ver el resto de entradas de la serie: Código Limpio

En la última entrada vimos cual es la ley de demeter, para que sirve y cómo resolver el problema del acoplamiento a una estructura de clases a través de la inyección de dependencias. En esta entrada vamos a ver que deberías tener en cuenta para mejorar la gestión de errores, como por ejemplo evitar el uso de códigos de error, separar la lógica de la gestión de errores y por supuesto evitar conocer los detalles de implementación de las clases que hay por debajo. Cuando programamos además de que nuestro código sea limpio, vamos a tener que enfrentarnos a la sucia tarea de la gestión de errores.

Veamos un ejemplo con la clase responsable de notificar a los usuarios de la academia que hay un nuevo contenido disponible. En el método send recibiremos un usuario y la entrada que debe serle notificada.

Partiremos de este primer ejemplo donde la gestión de errores ensucia el código para ver cuales son las diferentes mejoras que podemos ir realizando.

class SendCommand extends AbstractCommand
{
    private function send(User $user, Post $post): void
    {
        $code = $this->mailer->sendTemplate(
            [$user->getEmail() => $user->getName()],
            $post->getName(),
            '@App/Command/Academy/Mailing/Post/post.html.twig',
            [
                'user' => $user,
                'post' => $post,
            ]
        );

        if (Code::TIMEOUT == $code) {
            sleep(20);
            $this->send($user, $post);
        } elseif (Code::ADDITIONAL_DATA_REQUIRED == $code) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because was unavailable',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        } elseif (Code::ADDITIONAL_DATA_REQUIRED == $code) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because additional space was needed',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        } elseif (Code::INBOX_IS_FULL == $code) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because the inbox was full',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        }
    }
}

Como se puede ver este ejemplo inicial tiene bastante que mejorar.

Evita utilizar códigos de error

Los códigos de error son difíciles de manejar. En general van a requerir un montón de estructuras if / else para poder lidiar con los diferentes códigos de error.

Además siempre cabe la posibilidad de que se añada un nuevo código de error que nosotros no tengamos capturado. En lugar de códigos de error sería conveniente utilizar excepciones.

A continuación vemos exactamente el mismo código pero utilizando excepciones:

class SendCommand extends AbstractCommand
{
    private function send(User $user, Post $post): void
    {
        try {
            $this->mailer->sendTemplate(
                [$user->getEmail() => $user->getName()],
                $post->getName(),
                '@App/Command/Academy/Mailing/Post/post.html.twig',
                [
                    'user' => $user,
                    'post' => $post,
                ]
            );
        } catch (TimeoutException $e) {
            sleep(20);
            $this->send($user, $post);
        } catch (AdditionalDataRequiredException $e) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because was unavailable',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        } catch (ServiceUnavailableException $e) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because additional space was needed',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        } catch (InboxIsFullException $e) {
            $this->log(
                sprintf(
                    'message [%06d] for user "%s" could not be sent because the inbox was full',
                    $post->getId(),
                    $user->getEmail()
                )
            );
        }
    }
}

De momento no hemos mejorado mucho y nuestro código sigue teniendo una pinta terrible, pero estamos un paso más cerca de tener nuestro código bastante más elegante.

Separar lógica y gestión de errores

Lo que vamos a hacer ahora es separar la responsabilidad en dos métodos. Uno de ellos contendrá la lógica de negocio y el otro contendrá la gestión de errores. Este sencillo cambio hace nuestro código mucho más legible y fácil de mantener.

class SendCommand extends AbstractCommand
{
    private function handleSendException(User $user, Post $post, \Exception $exception): void
    {
        try {
            throw $exception;
        } catch (TimeoutException $e) {
            // ..
        } catch (AdditionalDataRequiredException $e) {
            // ..
        } catch (ServiceUnavailableException $e) {
            // ..
        } catch (InboxIsFullException $e) {
            // ..
        }
    }

    private function send(User $user, Post $post): void
    {
        try {
            $this->mailer->sendTemplate(
                [$user->getEmail() => $user->getName()],
                $post->getName(),
                '@App/Command/Academy/Mailing/Post/post.html.twig',
                [
                    'user' => $user,
                    'post' => $post,
                ]
            );
        } catch (\Exception$exception) {
            $this->handleSendException(user, $post, $exception);
        }
    }
}

Evita los detalles de implementación

Nuestro código ha mejorado bastante, pero todavía tiene un problema. Nuestro comando conoce los detalles de implementación del sistema de envío de correo. Esto supone un problema, si en el futuro quisiéramos enviar el correo o las notificaciones con otra librería o a través de otro medio, nuestro comando dejaría de funcionar.

Para ello es conveniente evitar conocer los detalles de implementación ( las excepciones ) que contiene el sistema de envío, con lo que nos debería quedar algo así.

class SendCommand extends AbstractCommand
{
    private function handleSendException(User $user, Post $post, \Exception $exception): void
    {
        $this->log(
            sprintf(
                'message [%06d] for user "%s" with message "%s"',
                $post->getId(),
                $user->getEmail(),
                $exception->getMessage()
            )
        );
    }

    private function send(User $user, Post $post): void
    {
        try {
            $this->mailer->sendTemplate(

                [$user->getEmail() => $user->getName()],
                $post->getName(),
                '@App/Command/Academy/Mailing/Post/post.html.twig',
                [
                    'user' => $user,
                    'post' => $post,
                ]
            );
        } catch (\Exception$exception) {
            $this->handleSendException($user, $post, $exception);
        }
    }
}
¿Quieres ser una bestia del desarrollo de software?
¡Continúa con nosotros en YouTube!

Todas las semanas un nuevo vídeo sobre desarrollo de software en tu bandeja de entrada.

Tranquilo, no te vamos a enviar spam.