Código sin nulls

Hace algunos días Juan Quijano escribió un post en GenBetaDev con este mismo título donde comentaba lo poco que le gustaba que la funciones devolviesen null y lo que hacía para evitar errores en ese caso.

Este post es mi respuesta a su post, ya que personalmente no me gusta la solución que presenta. En general termina con una solución como la siguiente:

  1. public class Modelo
  2. {
  3.     public Persona GetPersonaByName(string nombre)
  4.     {
  5.         Persona persona = new Persona();
  6.         if (nombre == "pepe")
  7.         { persona = new Persona { Nombre = nombre, Edad = 14 }; }
  8.         return persona;
  9.     }
  10. }
  11. public class Persona
  12. {
  13.     public string Nombre { get; set; }
  14.     public int Edad { get; set; }
  15.     public Persona()
  16.     {
  17.         Nombre = string.Empty;
  18.         Edad = 0;
  19.     }
  20. }

No me gusta por varias razones, la principal es que “oculta” la causa, devolviendo una Persona “sin datos” cuando no se encuentra la persona. Está estableciendo una convención que nadie más sabe: que una persona con el nombre vacío “no existe” en realidad. El código va a terminar llenándose de ifs para validar si el nombre es o no vacío para hacer algo o no. A diferencia de un null, donde olvidarte del if genera un error en ejecución (y por lo tanto es visible), dejarte un if en este caso hará que tu código se comporte mal… y a veces esto puede ser mucho, pero que mucho, más difícil que detectar el null reference, que al menos viene con stack trace. Otro problema es que no siempre existe en todo el rango de valores posibles un valor que pueda ser usado como “indicador de que no hay datos”.

Hay varios patrones para tratar esos caso, el más conocido el NullObject. De hecho un NullObject “mal hecho” es lo que propone Juan en su post. No soy muy amante del NullObject, aunque lo he usado a veces (la última hace poco en un refactoring, donde se tuvo que “desactivar” toda una funcionalidad. En este caso lo hicimos creando un NullObject del objeto que se estaba usando, de forma que el impacto en el resto del código (unos 90 proyectos de VS) fue nulo).

No quiero hablar del NullObject, si no presentar otra alternativa. En este caso una clase que contenga el valor más un indicador de si el valor existe o no. Vamos lo equivalente a Nullable<T> pero para cualquier tipo (sí… incluso los que pueden ser null). A priori parece que no ganamos nada pero dejadme un rato y veréis las ventajas que aporta.

La versión inicial de nuestra clase sería:

  1. public struct Maybe<T>
  2. {
  3.  
  4.     private readonly T _value;
  5.     private readonly bool _isEmpty;
  6.     private readonly bool _initialized;
  7.  
  8.     public T Value
  9.     {
  10.         get { return _value; }
  11.     }
  12.  
  13.     public bool IsEmpty
  14.     {
  15.         get { return (!_initialized) || _isEmpty; }
  16.     }
  17.  
  18.     public Maybe(T value)
  19.     {
  20.         _value = value;
  21.         _isEmpty = ((object)value) == null;
  22.         _initialized = true;
  23.     }
  24.  
  25.     public static Maybe<T> Empty()
  26.     {
  27.         return new Maybe<T>();
  28.     }
  29. }

Un punto importante es que no es una clase, es una estructura. Eso es para evitar que alguien que declare que devuelve un Maybe<T> termine devolviendo un null (recordad que queremos evitar los null).

Vale, esta estrcutura, tal cual está no nos aporta casi nada útil. El método del Modelo que presentaba Juan quedaría ahora como:

  1. public Maybe<Persona> GetPersonaByName(string nombre)
  2. {
  3.     Persona persona = new Persona();
  4.     if (nombre == "pepe")
  5.     {
  6.         persona = new Persona { Nombre = nombre, Edad = 14 };
  7.         return new Maybe<Persona>(persona);
  8.     }
  9.     return Maybe<Persona>.Empty();
  10. }

O devolvemos un Maybe relleno con la persona o devolvemos un Maybe vacío.  El test que usaba Juan quedaría como sigue:

  1. [TestMethod]
  2. public void GetPersonaByName_con_null_devuelve_string_empty()
  3. {
  4.     var modelo = new Modelo();
  5.     var persona = modelo.GetPersonaByName(null);
  6.     Assert.AreEqual(string.Empty, persona.Value.Nombre);
  7. }

Este test falla y la razón es obvia: persona.Value es null por lo que persona.Value.Nombre da un NullReferenceException. Podría añadir un if en el código para validar si person.IsEmpty es true, y en este caso no hacer nada. Personalmente prefiero mil veces un if (person.IsEmpty) que un if (person.Nombre ==””) ya que el primer if deja mucho claro que se pretende. Pero está claro, que no hemos ganado mucho. Como digo, dicha estructura apenas aporta nada.

Lo bueno es preparar dicha estructura para que pueda ser usada como un monad. Lo siento, soy incapaz de encontrar palabras sencillas para definir que es un monad porque el concepto es muy profundo, así que os dejo con el enlace de la wikipedia: http://en.wikipedia.org/wiki/Monad_(functional_programming)

Ahora vamos a preparar nuestra estructura para que pueda ser usada como un monad:

  1. public struct Maybe<T>
  2. {
  3.  
  4.     private readonly T _value;
  5.     private readonly bool _isEmpty;
  6.     private readonly bool _initialized;
  7.  
  8.     public T Value
  9.     {
  10.         get { return _value; }
  11.     }
  12.  
  13.     public bool IsEmpty
  14.     {
  15.         get { return (!_initialized) || _isEmpty; }
  16.     }
  17.  
  18.     public Maybe(T value)
  19.     {
  20.         _value = value;
  21.         _isEmpty = ((object)value) == null;
  22.         _initialized = true;
  23.     }
  24.  
  25.     public static Maybe<T> Empty()
  26.     {
  27.         return new Maybe<T>();
  28.     }
  29.  
  30.     public void Do(Action<T> action)
  31.     {
  32.         if (!IsEmpty) action(Value);
  33.     }
  34.  
  35.     public void Do(Action<T> action, Action elseAction)
  36.     {
  37.         if (IsEmpty)
  38.         {
  39.             action(Value);
  40.         }
  41.         else
  42.         {
  43.             elseAction();
  44.         }
  45.     }
  46.  
  47.     public TR Do<TR>(Func<T, TR> action)
  48.     {
  49.         return Do(action, default(TR));
  50.     }
  51.  
  52.     public TR Do<TR>(Func<T, TR> action, TR defaultValue)
  53.     {
  54.         return IsEmpty ? defaultValue : action(Value);
  55.     }
  56.  
  57.  
  58.     public Maybe<TR> Apply<TR>(Func<T, TR> action)
  59.     {
  60.         return IsEmpty ? Maybe<TR>.Empty() : new Maybe<TR>(action(Value));
  61.     }
  62. }

He añadido dos familias de métodos:

  1. Método Do para hacer algo solo si Maybe tiene valor
  2. Método Apply para encadenar Maybes. Este es el más potente y lo veremos luego.

Empecemos por los métodos Do. Dichos métodos básicamente nos permiten evitar el if(). Son poco más que una pequeña ayuda que nos proporciona la estructura. Mi test quedaría de la siguiente manera:

  1. [TestMethod]
  2. public void GetPersonaByName_con_null_devuelve_string_empty()
  3. {
  4.     var modelo = new Modelo();
  5.     var persona = modelo.GetPersonaByName(null);
  6.     var name = string.Empty;
  7.     persona.Do(p => name = p.Nombre);
  8.     Assert.AreEqual(string.Empty, name);
  9. }

El código del Do se ejecuta solo si hay valor, es decir si se ha devuelto una persona.

Podríamos reescribir el test usando otra de las variantes de Do:

  1. [TestMethod]
  2. public void GetPersonaByName_con_null_devuelve_string_empty()
  3. {
  4.     var modelo = new Modelo();
  5.     var persona = modelo.GetPersonaByName(null);
  6.     var name = persona.Do(p => p.Nombre, "no_name");
  7.     Assert.AreEqual("no_name", name );
  8. }

No hay mucho más que decir sobre los métodos Do… porque el método más interesante es Apply 😉

El método Apply me permite encadenar Maybes. Para ver su potencial, cambiaré el método del Modelo:

  1. public Maybe<Persona> GetPersonaByName(string nombre)
  2. {
  3.     Persona persona = new Persona();
  4.     if (nombre == "pepe")
  5.     {
  6.         persona = new Persona {Nombre = null, Edad = 42};
  7.         return new Maybe<Persona>(persona);
  8.     }
  9.     return Maybe<Persona>.Empty();
  10. }

Ahora si le paso “pepe” me da a devolver una Persona pero con el Nombre a null. Tratar esos casos con ifs se vuelve muy complejo y costoso. Apply viene en nuestra ayuda:

  1. [TestMethod]
  2. public void Acceder_a_nombre_null_no_da_probleamas()
  3. {
  4.     var modelo = new Modelo();
  5.     var persona = modelo.GetPersonaByName("pepe");
  6.     // En este punto tenemos un Maybe relleno pero value.Nombre es null
  7.     var nombreToUpper = string.Empty;
  8.     nombreToUpper = persona.Apply(p => p.Nombre).Do(s =>s.ToUpper(), "NO_NAME");
  9.     Assert.AreEqual("NO_NAME", nombreToUpper);
  10. }

La variable persona es un Maybe<Persona> con un valor. El método Apply lo que hace es básicamente ejecutar una transformación sobre el valor (el objeto Persona) y devolver un Maybe con el resultado. En este caso transformamos el objeto persona a p.Nombre, por lo que el valor devuelto por Apply es un Maybe<string>. Y como el valor de p.Nombre era null, el Maybe está vacío.

La combinación de Apply y Do permite tratar con valores nulos de forma muy sencilla y elegante.

Si os pregunto que capacidades funcionales tiene C# seguro que muchos responderéis LINQ… Porque no hacemos que nuestra clase Maybe<T> pueda participar del juego de LINQ? Por suerte eso es muy sencillo. Para ello basta con que Maybe<T> implemente IEnumerable<T> añadiendo esas dos funciones:

  1. public IEnumerator<T> GetEnumerator()
  2. {
  3.     if (IsEmpty) yield break;
  4.     yield return _value;
  5. }
  6.  
  7. IEnumerator IEnumerable.GetEnumerator()
  8. {
  9.     return GetEnumerator();
  10. }

Básicamente un Maybe<T> lleno se comporta como una colección de un elemento de tipo T, mientras que un Maybe<T> vacío se comporta como una colección vacía. A partir de aquí… tenemos todo el poder de LINQ para realizar transformaciones, consultas, uniones, etc… con nuestros Maybe<T> con otros Maybe<T> o cualquier otra colección. P. ej. podríamos tener el siguiente código:

  1. [TestMethod]
  2. public void Comprobar_Que_Nombre_es_Null()
  3. {
  4.     var modelo = new Modelo();
  5.     var persona = modelo.GetPersonaByName("pepe");
  6.     var tiene_nombre= persona.Apply(p => p.Nombre).Any();
  7.     Assert.IsFalse(tiene_nombre);
  8. }

Y por supuesto podemos iterar con foreach sobre los elementos de un Maybe<T> :)

Ya para finalizar vamos a añadir un poco más de infrastructura a la clase Maybe<T>. En concreto soporte para la comparación:

  1. public static bool operator ==(Maybe<T> one, Maybe<T> two)
  2. {
  3.     if (one.IsEmpty && two.IsEmpty) return true;
  4.     return typeof(T).IsValueType ?
  5.         EqualityComparer<T>.Default.Equals(one._value, two._value) :
  6.     object.ReferenceEquals(one.Value, two.Value);
  7. }
  8.  
  9. public bool Equals(Maybe<T> other)
  10. {
  11.     return _isEmpty.Equals(other._isEmpty) && EqualityComparer<T>.Default.Equals(_value, other._value);
  12. }
  13.  
  14. public override bool Equals(object obj)
  15. {
  16.     if (ReferenceEquals(null, obj)) return false;
  17.     return obj is Maybe<T> && Equals((Maybe<T>)obj);
  18. }
  19.  
  20. public static bool operator !=(Maybe<T> one, Maybe<T> two)
  21. {
  22.     return !(one == two);
  23. }
  24.  
  25. public override int GetHashCode()
  26. {
  27.     unchecked
  28.     {
  29.         return (_isEmpty.GetHashCode() * 397) ^ EqualityComparer<T>.Default.GetHashCode(_value);
  30.     }
  31. }

Maybe<T> intenta replicar el comportamiento de comparación de T. Es decir:

  • Dos Maybe<T> son “Equals” si los dos Ts de cada Maybe son “Equals”
  • Un Maybe<T> == otro Maybe<T> si:
    • Ambos Ts son el mismo objeto (en el caso de tipo por referencia)
    • Ambos Ts son “Equals” en el caso de tipos por valor

P. ej. el siguiente test valida el comportamiento de ==:

  1. var i = 10;
  2. var i2 = 10;
  3. var p = new Persona();
  4. var p2 = new Persona();
  5. Assert.IsTrue(new Maybe<int>(i) == new Maybe<int>(i2));
  6. Assert.IsFalse(new Maybe<Persona>(p) == new Maybe<Persona>(p2));

Y finalmente añadimos soporte para la conversión implícita de Maybe<T> a T:

  1. public static implicit operator Maybe<T>(T from)
  2. {
  3.     return new Maybe<T>(from);
  4. }

Dicha conversión nos permite simplificar las funciones que deben devolver un Maybe<T>. Así ahora la función del modelo puede ser:

  1. public Maybe<Persona> GetPersonaByName(string nombre)
  2. {
  3.     return nombre == "pepe" ?
  4.         new Persona {Nombre = null, Edad = 42} :
  5.         null;
  6. }

Fíjate que la función GetPersonaByName sigue devolviendo un Maybe<Persona> pero para el código es como si devolviese un Persona. El return null se traduce a devolver un Maybe<Persona> vacío.

Bueno… con eso termino el post. Espero que os haya resultado interesante y que hayáis visto otras posibles maneras de lidiar con las dichosas referencias null.

Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *