¿Desde dónde comunicar o tratar situaciones de error? Parte 2

En el anterior artículo vimos como poder resolver ciertas situaciones de error mediante programación orientada a objetos más tradicional evitando el uso de null y utilizando excepciones.

En esta segundo artículo vamos a ver como podemos enfocar los mismos escenarios donde se producen situaciones excepcionales con un enfoque más funcional.

Option

Es una mónada que representa un valor opcional. Las instancias de Option son una instancia de Some o el objeto None.

En lenguajes exclusivamente funcionales o donde se puede realizar programación funcional pura este tipo viene de serie, como sucede en Scala con el tipo Option o en Haskell con el tipo Maybe.

Este tipo de objeto se suele utilizar para representar la presencia o ausencia de valor.

Se suele utilizar en aquellos escenarios donde es posible que no exista un resultado o se produzca un caso excepcional.

En Kotlin este tipo no viene de serie con la librería estándar, una implementación simple pero suficiente para nuestro ejemplo podría ser la siguiente:

sealed class Option<out A> {
    object None : Option<Nothing>()
    data class Some<out A>(val t: A) : Option<A>()

    val isEmpty get() = this is None

    fun <A> some(t: A): Option<A> = Some(t)
    fun none(): Option<A> = None
}

fun <A,B> Option<A>.fold(ifEmpty: () -> B, ifSome: (A) -> B): B =
        when (this) {
            is Option.None -> ifEmpty()
            is Option.Some -> ifSome(t)
        }

fun <A,B> Option<A>.flatMap(f: (A) -> Option<B>): Option<B> =
        fold({ this as Option.None }, f)

fun <A,B> Option<A>.map(f: (A) -> B):Option<B> =
        flatMap { a -> Option.Some(f(a)) }

Utilizando Option

Si recordáis en el anterior artículo teníamos un escenario donde en caso de no existir el recurso solicitado devolvíamos una excepción.

class MovieRepository (val context: Application): MovieRepository {

    val baseAddress = context.getString(R.string.base_address)

    var allMovies:MutableList<Movie> = mutableListOf<Movie>()

    ...

   override fun getById (id: Long) = getAll().firstOrNull{it.id == id} ?: throw MovieNotFoundException()
    }

    ...
}

class GetMovieByIdUseCase (private val movieRepository: MovieRepository,  
executor: Executor): UseCase(executor) {

    ...

    fun run() {
        try {
            val movie = movieRepository.getById(id)

            uiExecute {onMovieLoaded(movie)}
        } catch (ex: MovieNotFoundException) {
            uiExecute {onMovieNotFoundError()}
        } catch (ex: Exception) {
            uiExecute {onConnectionError()}
        }
    }
}

Utilizando el tipo Option, el repositorio debería devolver un tipo Option, donde en caso de no existir el recurso devolvería None o Some(movie) si el recurso si existe.

class FakeMovieRepository (val context: Application): MovieRepository {

    override fun getAll (): Option<List<Movie> {
       ...
    }

    override fun getById(id: Long): Option<Movie> {
        return getAll().flatMap {
           if (it.any { movie -> movie.id == id }){
              Option.Some(
                 it.first { it.id == id })
            } else {
                Option.None
            }
        }
    }
    ...
}

El tipo Option devuelto en el repositorio se lo comunicamos a través de la función onResult al llamador del caso de uso.

class GetMovieByIdUseCase(private val movieRepository: MovieRepository,
                          private val executor: Executor) : UseCase(executor) {

    private var id: Long = 0

    fun execute(id: Long, onResult: (Option<Movie>) -> Unit) {
        this.id = id

        asyncExecute {
            val movieResult = movieRepository.getById(id)

            uiExecute { onResult(movieResult) }
        }
    }
}

Y posteriormente en el presenter se tomarían las decisiones de que hacer en cada situación.

class MoviePresenter(private val getMovieByIdUseCase: GetMovieByIdUseCase) {

    ...

    private fun loadMovie(id: Long) {
        loadingMovie()

        getMovieByIdUseCase.execute(id,
                onResult = { result ->
                    result.fold({showMovieNotFoundError()}, {movie -> showMovie(movie)})
                })
    }

    private fun showMovie(movie: Movie) {
        view?.hideLoading()
        view?.showMovie(movie)
    }

    private fun showMovieNotFoundError() {
        view?.hideLoading()
        view?.showMovieNotFoundError()
    }
    ...
}

Limitaciones de Option

Option tiene sentido cuando queremos modelar la presencia o ausencia de valor, pero ¿qué ocurre si en caso de ausencia de valor necesito mas información?.

Por ejemplo es habitual en aplicaciones móviles que el repositorio se comunique con dos orígenes de datos el remoto y el local.

Me puede interesar distinguir cuando no existe el recurso o cuando no tengo connectividad porque el mensaje de error a mostrar al usuario, por poner el ejemplo más simple, quiero que sea distinto.

Either

En situaciones donde Option se queda corto puede ser más interesante utillizar Either.

Es una mónada que representa un valor de uno de dos tipos posibles (es una unión disjunta).

Una instancia de Either es una instancia de Left o Right. Un uso común, aunque no exclusivo, de Either es para la gestión de errores.

Por convención Left se usa para el caso de error y Right se usa para el éxito.

En lenguajes exclusivamente funcionales o donde se puede realizar programación funcional pura este tipo viene de serie, como sucede en Scala o Haskell.

Un implementación simple, escrita en Kotlin, pero suficiente para nuestro ejemplo podría ser la siguiente:

sealed class Either<out L, out R> {
    //Failure
    data class Left<out L>(val value: L) : Either<L, Nothing>()

    //Success
    data class Right<out R>(val value: R) : Either<Nothing, R>()

    val isRight get() = this is Right<R>
    val isLeft get() = this is Left<L>

    fun <L> left(a: L) = Left(a)
    fun <R> right(b: R) = Right(b)
}

 fun <L, R, T> Either<L, R>.fold(left: (L) -> T, right: (R) -> T): T     
         =
        when (this) {
            is Either.Left  -> left(value)
            is Either.Right -> right(value)
        }
 fun <L, R, T> Either<L, R>.flatMap(f: (R) -> Either<L, T>): 
        Either<L, T> =
        fold({ this as Either.Left }, f)

 fun <L, R, T> Either<L, R>.map(f: (R) -> T): Either<L, T> =
        flatMap { Either.Right(f(it)) }

Utilizando Either

Lo primero es definir en el dominio previamante los casos excepcionales que queremos modelar.

sealed class GetMovieFailure{
    class NetworkConnection: GetMovieFailure()
    class MovieNotFound: GetMovieFailure()
}

El repositorio debería devolver un tipo Either, donde en caso de error devolvería uno de los tipos de GetMovieFailure como Left o el Movie resultante como Right:

class FakeMovieRepository (val context: Application): MovieRepository {

    override fun getAll (): Either<GetMoviesFailure,List<Movie>> {
       ...
    }

   override fun getById (id: Long): Either<GetMovieFailure,Movie> {
      return getAll().fold(
{Either.Left(GetMovieFailure.NetworkConnection)},
      {
        if (it.any { movie -> movie.id == id }){
            Either.Right(it.first{it.id == id})
        } else {
Either.Left(GetMovieFailure.MovieNotFound())
        }
     })
  }
    ...
}

El tipo Either devuelto en el repositorio se lo comunicamos a través de la función onResult al llamador del caso de uso.

class GetMovieByIdUseCase(private val movieRepository: MovieRepository, private val executor: Executor) : UseCase(executor) {
    private var id: Long = 0

    fun execute(id: Long, onResult: (Either<GetMovieFailure, Movie>) -> Unit) {
        this.id = id

        asyncExecute {
            val movieResult = movieRepository.getById(id)

            uiExecute { onResult(movieResult) }
        }
    }
}

Y posteriormente en el presenter se tomarian las decisiones de que hacer en cada situación.

class MoviePresenter(private val getMovieByIdUseCase: GetMovieByIdUseCase) {

    ...

    private fun loadMovie(id: Long) {
        loadingMovie()

        getMovieByIdUseCase.execute(id,
                onResult = { result ->
                    result.fold({failure -> showError(failure)},   
                                {movie -> showMovie(movie)})
                })
    }

    private fun showError(failure: GetMovieFailure) 
   {
        when(failure){
           is GetMovieFailure.NetworkConnection ->  
              showConnectionError()
           is GetMovieFailure.MovieNotFound ->  
              showMovieNotFoundError()
        }
    }
    private fun showMovie(movie: Movie) {
        view?.hideLoading()
        view?.showMovie(movie)
    }

    private fun showMovieNotFoundError() {
        view?.hideLoading()
        view?.showMovieNotFoundError()
    }

    private fun showConnectionError() {
        view?.hideLoading()
        view?.showConnectionError()
    }

    ...
}

Conclusiones

Hemos visto como tratar y comunicar situaciones excepcionales utilizando Kotlin pero con un enfoque más estilo programción funcional sin el uso de excepciones.

Hay varios factores que pueden afectar para decidirnos a utilizar programación más tradicional como en el anterior artículo o programación más estilo funcional como en este como pueden ser el conocimiento del equipo, tiempos de entrega, tipo de proyecto etc…

Así que dependiendo del contexto en el que te encuentres puede ser más recomendable o práctico decidirse por una u otra opción.

Como siempre conocer bien el contexto en el que te encuentras es clave para poder decidir.

El código fuente del código lo puedes ver aquí.