leanmind logo leanmind text logo

Blog

BDD

Behaviour-driven Development es una técnica para tomar mejores requisitos de producto.

Null, un viejo enemigo del lado oscuro

Por Carlos Blé Jurado

Limita lo máximo posible el uso del valor null en tu código, porque tiene múltiples consecuencias negativas en la mantenibilidad del mismo. Por lo tanto, limita también al máximo la cantidad de objetos nullables, es decir, que pueden tener un valor nulo. El lenguaje Kotlin quiere ayudarte haciendo explícito el problema de los nulos, pero ¿entiendes lo que te está proponiendo?, ¿o le das el “sí de los locos”?

Problemas: incertidumbre, mezcla de responsabilidades, acoplamiento

Cuando un objeto es null e intentamos acceder a uno de sus campos, se produce una excepción en tiempo de ejecución, ya que no es posible hacerlo y el programa no tiene ninguna opción para continuar. Ante la incertidumbre de que el valor del objeto sea nulo, nos volvemos defensivos añadiendo comprobaciones condicionales para evitar estas excepciones no controladas. Esta programación defensiva tiene un efecto dominó o burbuja, que hace que en las demás capas del código volvamos a añadir comprobaciones para evitar acceso a nulos. Es fácil terminar llenando el código de comprobaciones sobre los nulos por todas partes, repitiendo la misma lógica de control en multitud de artefactos del sistema. Lo peor es que cuando cualquier otra persona se encuentra un código así, tiende a mantener el mismo estilo, ya que se enfrenta a un nivel de incertidumbre incómodo, "¿por qué será que esto puede ser nulo?, ¿por qué hay que comprobarlo en todos sitios?, ¿el valor nulo refleja algún estado especial del sistema?, ¿debo seguir prograpango el nulo?"

La responsabilidad de gestionar nulos se esparce por todas partes, en lugar de estar encapsulada en un solo punto del sistema, provocando un acoplamiento indeseado entre ellas.

Algnos lenguajes como C# y Kotlin, implementan el operador Elvis para hacer menos tedioso el código defensivo contra los valores nulos, pero esto no evita los problemas descritos arriba, simplemente hace el código más corto:

val expression : String? = null
val size : Int? = expression?.length

La variable size tendrá el valor nulo, porque expression tiene valor nulo y, por tanto, no se llega a acceder a la propiedad length. El operador evita el uso de bloque if/else, pero no soluciona el problema de propagación de los nulos.

Lo mejor para minimizar los nulos es evitar que se generen y sino al menos evitar su propagación. Es decir:

Kotlin está diseñado para que los objetos por defecto no puedan ser nulos y así estar seguros en tiempo de compilación de que no van a producirse excepciones por problemas con nulos. El compilador se puede asegurar de que no se va a dar el caso. Los programadores diseñan lenguajes de programación para ayudar a los programadores a evitar el error humano. Se busca que los compiladores sean cada vez más inteligentes y evitar caer en los mismos errores una y otra vez. Pero ningún diseño es infalible ante la ignorancia humana. Por más que los diseñadores de lenguajes intentan fomentar las buenas prácticas y reducir los errores, siempre nos las ingeniamos para seguirlos cometiendo. Algunas de esas decisiones de diseño, fruto de la buena intención, acaban fracasando en su propósito y causando incluso más molestias a los programadores. Es el caso, por ejemplo, de las Checked Exceptions de Java, una buena intención que no sirvió más que para molestar. Está claro que las herramientas no suplen la carencia de conocimiento ni la falta de curiosidad por conocer cómo funcionan tales herramientas y para qué fueron diseñadas.

Por defecto, en muchos lenguajes de programación como Java o JavaScript, un objeto no inicializado tiene el valor nulo y el compilador o el intérprete no fuerzan su inicialización explícita. En Kotlin, no existe ese valor por defecto, así que al definir el tipo tenemos que asignarle un valor explícitamente o bien indicar explícitamente que no lo queremos inicializar todavía (lateinit).

var text: String? = null

¿Defines text como nullable (?) solo porque el compilador te obliga a asignarle un valor por defecto? No abras la puerta a los nullables solo porque se te pide un valor de inicialización en un momento en el que todavía desconoces ese valor. Puedes conseguirlo siguiendo estos dos principios:

Si no puedes cumplir estos dos principios, entonces puedes decirle a Kotlin que más adelante inicializarás la variable:

lateinit var text: String

Ahora bien, si necesitas que un objeto pueda ser nulo, entonces adelante, márcalo como nullable usando el símbolo de interrogación (String?). Pero no uses los nulos para codificar estados del sistema con un significado especial.

Que nulo solo signifique “nada”

Por desgracia, es común encontrar código donde los programadores decidieron otorgar un significado especial al valor nulo o a números mágicos (0,1,2), a cualquier otro primitivo o al orden de los elementos dentro de un array… un significado que no es obvio, que no es explícito y que representa un estado de la aplicación en concreto. Cosas del estilo de… “Si delivery es nulo, significa que el envío no se pudo realizar”. El problema es que ese conocimiento queda cifrado, no es explícito, no es evidente, no lo entiende una persona que no sea quien escribió ese código. Hace un uso inadecuado de los elementos del lenguaje. La mejor forma de escribir código es siendo muy explícitos y claros sobre nuestra intención al programar. Crear tipos, clases, objetos, funciones que expresen los conceptos que necesitamos, con nombres adecuados. Los primitivos y los nulos formarán parte del interior de esas construcciones nuestras, pero no estarán regados por todo el código. Estarán encapsulados como elementos de implementación indispensables para comunicarnos con la máquina, no con las personas.

En ocasiones, es inevitable lidiar con el concepto de “vacío” o “inexistente”, un punto en el que no podemos quitarnos de encima el valor nulo. Afortunadamente, existe un patrón al que podemos recurrir que se llama Null Object Pattern que nos puede ayudar en estos casos.

Patrón Null Object

Una implementación típica del patrón es crear una clase que hereda de otra y que implementa sus métodos de forma inocua, para que cuando se usen el sistema se comporte como si este objeto fuera coceptualmente vacío, inexistente o nulo.

public interface Animal {
    void makeSound() ;
}

public class NullAnimal implements Animal {
    public void makeSound() {
        // silence...
    }
}

El punto del código que instancia la clase, sabe que se trata de un nulo, pero en lugar de retornar null, retorna una instancia de NullAnimal, de forma que los que consumen ese objeto reciben una instancia no nula con métodos a los que pueden invocar, pero el resultado de invocar a esos métodos es inocuo. Así no hay que programar de forma defensiva. Como con todos los patrones, es importante no abusar de él. Personalmente, lo he usado en algunas ocasiones y el resultado ha quedado muy bien, pero no es un patrón que aparezca en todos mis proyectos ni mucho menos. A veces, la mejor forma de no propagar un nulo es lanzar una excepción, típicamente cuando somos incapaces de procesar una situación determinada. En general, prefiero código resiliente que intenta resolver las situaciones con sentido sin lanzar excepciones en tiempo de ejecución a la primera de cambio, pero hay veces que lo que más sentido tiene es tirar esa excepción.

El valor nulo es un viejo enemigo, nos lleva dando guerra toda la vida, ¡todos contra null!

Publicado el 26/07/2019 por

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers