Objeto Maybe para evitar nulos

2015-09-11

Esta semana nuestro mentor Alfredo nos ha enseñado la importancia de evitar hacer comparaciones con nulos y más aún la importancia de devolver nulls. Principalmente no es nada intuitivo que un método cuya firma dice que devolverá un objeto del tipo X de repente devuelva un null. Al no ser intuitivo como desarrollador lo normal es que no hagas esta comparación e intentes hacer directamente una operación sobre ese objeto. Hasta que sucede lo que tenía que suceder: NullRefence. Entonces vas a ese punto en el cuál tienes un nulo y haces la comprobación:

1
2
3
4
5
6
7
8
public List GetTripsByUser(User.User user)
{
var loggedUser = sessionService.GetLoggedUser();
if (loggedUser == null) throw new UserNotLoggedInException();
return user.HasFriend(loggedUser) ?
tripRepository.FindTripsByUser(user) : new List<Trip>();
}

Este sería el resultado. No hace falta mencionar lo feo que queda estar preguntando por ese null. Lo que realmente chirría es: ¿Por qué un objeto al que le pido un LoggedUser me está devolviendo null? ¿Por qué comparo un objeto User con un null? ¿No se supone que es un User?

La solución que Alfredo nos propuso es hacer una aproximación al ejemplo de Nat Pryce usando The Maybe Type. Evidentemente para el contexto de esta kata necesitaba algo más sencillo. Pero me encantó el concepto de devolver un objeto que puede ser o no un usuario (Maybe). El potencial de esto es hacerlo genérico y obtener cualquier tipo de objeto mediante un Maybe que puede o no tener un valor. En la kata no lo resolví con un genérico porque no me hizo falta. Además en lugar de llamarlo MaybeUser lo he llamado UserSearchResult. Aquí pongo el ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserSearchResult
{
private readonly User possibleUser;
public UserSearchResult(User possibleUser)
{
this.possibleUser = possibleUser;
}
public bool HasNotUser()
{
return possibleUser == null;
}
public User User()
{
if (possibleUser == null) throw new UserValueDoesNotExist();
return possibleUser;
}
}

Como he dicho, esta clase es muy concreta para User. Pero en caso de tener más casos como este sería sencillo hacerla genérica y reutilizarla. Esta clase desde el punto de vista del TripService ahora quedaría de la siguiente manera (donde antes estaba la comparación con null):

1
2
3
4
5
6
7
8
public List GetTripsByUser(User.User user)
{
var userSearchResult = sessionService.GetLoggedUser();
if (userSearchResult.HasNotUser) throw new UserNotLoggedInException();
return user.HasFriend(userSearchResult.User) ?
tripRepository.FindTripsByUser(user) : new List<Trip>();
}

Evidentemente una de las claras ventajas me parecía que el método GetLoggedUser devolvía un objeto UserSearchResult según su firma. Lo cual hace evidente para quien lo use que podría ser que no devuelva un User concreto sino que podría venir sin valor (a fin de cuentas es un resultado). Esto hace más intuitivo que tengas que preguntar si lo tiene. Otra ventaja es que la comparación con null te la has llevado a un punto mucho más concreto, lo has encapsulado. De esta manera siempre trabajas con resultados con o sin valor en lugar de preguntar por nulos lo cual hace que tu dominio quede mejor representado.

Como conclusión diré que esta técnica me parece más razonable que devolver nulos cada vez que quieres devolver un valor vacío. Cuando tenemos listas devolvemos una lista vacía, cuando tenemos strings una cadena vacía, cuando tenemos números un 0, entonces ¿Por qué devolvemos nulos cuando tenemos un objeto vacío? Gracias a este tipo de objetos estamos diciendo explicitamente que nuestro método puede no devolver un valor. Esto hace que su cliente entienda mejor el funcionamiento de ese método.