Code Tip: No devolver null

jueves, 3 de septiembre de 2015

Este blog no esta mantenido, suscribite a la newsletter del nuevo


Devolver null en una función es una mala práctica, vamos a ver en este artículo porque es una mala práctica y como podemos evitarlo.

Devolver null es una mala práctica porque estamos obligando al llamador a verificar si el objeto devuelto es null antes de realizar cualquier acción. Vamos a ver casos concretos.

Cuando se devuelve una colección

Fijaros en el siguiente código, tenemos una función que devuelve una lista, si no encuentra datos a devolver el valor devuelto es null.

    public List<User> GetUsers()
    {
        if (...no hay usuarios...)
            return null;
    }


Esta situación obliga al llamador a verificar si es null la lista antes de hacer nada.

    List<User> users = GetUsers()

    if (users != null)
    {
        foreach (User user in users)
        {
            ....hace lo que sea....
        }
    }


Ensuciando el código, imagina if anidados con llamadas a funciones que pueden devolver null, el código quedaria bastante terrible.

Una mejor solución es en caso de no tener datos que devolver, crear una lista vacía como valor de retorno

    public List<User> GetUsers()
    {
        if (...no hay usuarios...)
            return new List<User>();
    }


Ahora el código del llamador se simplifica

    List<User> users = GetUsers()

    foreach (User user in users)
    {
        ....hace lo que sea....
    }


Quedando más sencillo.

Cuandose devuelve un objeto

Otro escenario es cuando tenemos una función que devuelve un objeto pero en función de cada caso la solución puede ser diferente.

Lanzar una excepción

Por ejemplo el siguiente código, tenemos una función que devuelve una objeto, si no encuentra el objeto a devolver, el valor devuelto es null.

    public User GetUserById(int id)
    {
        if (...no existe ese usuario...)
            return null;
    }


Volvemos a tener que verificar si es null el resultado de la función antes de hacer nada.

    User user = repository.GetUserById(id)

    if (user != null)
    {
        ....hace lo que sea....
    }


Volvemos a ensuciar el código y además si nos olvidamos de validar algún null, es facil caer en un NullReferenceException.

En este escenario lo más apropiado es lanzar una excepción si no existe ningún usuario con ese identificador

    public User GetUserById(int id)
    {
        if (...no existe ese usuario...)
            throw new Exception("User not found with id "+ id);
    }


Ahora el código del llamador se simplifica

    User user = repository.GetUserById(id)

    ....hace lo que sea....


En la pila de llamadas debería existir en las capas más altas alguien manejando esta posible excepción, pero en las partes intermedias el código se simplifica al no tener que validar nulos.

Utilizar Null Object Pattern

Hay otros escenariós ante una situación parecida, donde la solución más apropiada pueda ser utilizar el patrón Null Object.

Imaginemos que tenemos una factoría de logger

    public Logger GetLogger(string type)
    {
        if (...no existe ese tipo...)
            return null;
    }


Mismo problema en el llamador, hay que verificar que el logger no sea nulo antes de hacer nada.

    Logger logger = factory.GetLogger(type)

    .
    .
    .

    if (logger != null)
    {
        logger.Debug("escribimos traza");
    }


Volvemos a ensuciar el código, en este caso podemos mejorar el código creando una función Debug que verifica si logger es null antes de invocar el método Debug del logger, pero una mejor solución es utilizar Null Object Pattern.

Abstraemos el logger con una interfaz, y tenemos la implementación real y otra vacía.

    public ILogger
    {
        void Debug(string message);
    }

    public RealLogger:ILogger
        public void Debug(string message)
        {
            //hace lo que sea
        }
    }

    public NullLogger:ILogger
    {
        public void Debug(string message)
        {
            //No hace nada
        }
    }


Ahora la factoría en lugar de devolver null devuelve NullLogger si no existe el tipo.

    public Logger GetLogger(string type)
    {
        if (...no existe ese tipo...)
            return new NullLogger();
    }


Ahora cada vez que utilicemos logger no es necesario verificar si es null.

    Logger logger = factory.GetLogger(type)

    .
    .
    .

    logger.Debug("escribimos traza");


Un poco de historia: Sir Charles Antony Richard Hoare (aka C.A.R. Hoare) fue el primero que introdujo la referencia nula, os dejo unas palabras suyas donde reconoce que fue un error.

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. In recent years, a number of program analysers like PREfix and PREfast in Microsoft have been used to check references, and give warnings if there is a risk they may be non-null. More recent programming languages like Spec# have introduced declarations for non-null references. This is the solution, which I rejected in 1965.

Y para terminar un poco de actualidad: los lenguajes funcionles como pueden ser F# y Kotlin por defecto no permiten asignar null para evitar este tipo de situaciones.

Conclusiones

Devolver null es una mala práctica que nos puede llevar a muchas situaciones de error y tenemos a nuestro alcance soluciones en lenguajes como java o c#. Es cierto que si llamamos a librerías externas o apis tendremos que protegernos de excepciones provocadas porque pueden devolver null, pero si en nuestro código evitamos ponernos piedras en el camino, mucho mejor.

3 comentarios:

  1. Interesante el articulo y la parte de Un poco e historia.
    Saludos

    ResponderEliminar
    Respuestas
    1. Muchas gracias por el comentario Victor.
      Saludos para ti también.

      Eliminar
  2. El problema no solo es el devolver null, sino el cómo representas el resultado de la operación. Por ejemplo, en el caso de la lista, si ocurrió un fallo en la llamada a la BDD ¿qué regresas? no puedes regresar una lista vacía, puesto que una lista vacía significa que no encontraste datos, pero no que ocurrió un error. Lo mismo ocurre con un elemento simple, si regresas null quiere decir que no se encontró en la BDD, pero si ocurrió un error en la llamada, ¿regresas null también?.

    Una solución es devolver una tupla, uno de los valores sería bool y el otro la lista, el bool sería verdadero si no ocurrió ningún error, y falso en caso de error, y la lista estaría vacía si ocurrió un error o si no ocurrió ningún error pero no encontró resultados.

    Pero desde mi punto de vista es mejor crear una clase que pueda representar los dos estados (error/no error) y también el resultado, y definir métodos para acceder fácilmente al resultado de la búsqueda.

    ResponderEliminar