Código limpio: Gestión de errores

Código limpio: Gestión de errores

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.

Esta es la novena entrega de una serie en la que analizamos el libro clean code de Robert C Martin publicado en el año 2008. Si quieres puedes ir a ver el índice de la serie. También puedes ver la versión en video de esta entrada en nuestro canal de youtube.

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.