De onde se comunicar ou lidar com situações de erro? Método funcional

Tempo de leitura: 5 minutes

No artigo anterior, vimos como resolver certas situações de erro por meio da programação orientada a objetos mais tradicional, evitando o uso de null e usando exceções.

Neste segundo artigo, veremos como podemos abordar os mesmos cenários em que ocorrem situações excepcionais com uma abordagem mais funcional.

 

Opção

É uma mônada que representa um valor opcional. Instâncias de Option são uma instância de Some or the None objeto.

Em linguagens exclusivamente funcionais ou onde a programação funcional pura pode ser realizada, este tipo é padrão, como em Scala com o tipo Option ou em Haskell com o tipo Maybe.

Este tipo de objeto é freqüentemente usado para representar a presença ou ausência de valor.

Geralmente é usado nos cenários em que é possível que não haja resultado ou que ocorra um caso excepcional.

Em Kotlin, esse tipo não vem com a biblioteca padrão, uma implementação simples, mas suficiente para nosso exemplo, poderia ser a seguinte:

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)) }

Usando Opção

Se você se lembrar no artigo anterior, tivemos um cenário em que, se o recurso solicitado não existisse, retornaríamos uma exceção.

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()}
        }
    }
}

Usando o tipo Opção, o repositório deve retornar um tipo Opção, onde se o recurso não existir, ele retornará Nenhum ou Alguns (filme) se o recurso existir.

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
            }
        }
    }
    ...
}

O tipo de opção retornado no repositório é comunicado por meio da função onResult ao chamador do 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) }
        }
    }
}

E mais tarde, no apresentador, seriam tomadas as decisões sobre o que fazer em cada situação.

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()
    }
    ...
}

Limitações de opções

A opção faz sentido quando queremos modelar a presença ou ausência de valor, mas e se em caso de ausência de valor eu precisar de mais informações?

Por exemplo, é comum em aplicativos móveis que o repositório se comunique com duas fontes de dados, a remota e a local.

Posso ter interesse em distinguir quando o recurso não existe ou quando não tenho conectividade porque a mensagem de erro a ser mostrada ao usuário, para colocar o exemplo mais simples, quero que seja diferente.

 

Either

Em situações em que Option é insuficiente, pode ser mais interessante usar Either.

É uma mônada que representa um valor de um dos dois tipos possíveis (é uma união disjunta).

Uma instância de Either é uma instância de Left ou Right. Um uso comum, mas não exclusivo, de Either é para tratamento de erros.

Por convenção, Left é usado para falha e Right é usado para sucesso.

Em linguagens exclusivamente funcionais ou onde a programação funcional pura pode ser feita, esse tipo vem padrão, como em Scala ou Haskell.

Uma implementação simples, escrita em Kotlin, mas suficiente para nosso exemplo, poderia ser a seguinte:

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

A primeira coisa é definir no domínio previamente os casos excepcionais que queremos modelar.

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

O repositório deve retornar um tipo Either, em que, em caso de erro, ele retornaria um dos tipos GetMovieFailure como Left ou o 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())
        }
     })
  }
    ...
}

O tipo Either retornado no repositório é comunicado por meio da função onResult ao chamador do 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) }
        }
    }
}

E mais tarde, no apresentador, seriam tomadas as decisões sobre o que fazer em cada situação.

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()
    }

    ...
}

 

Conclusões

Vimos como lidar e relatar situações excepcionais usando Kotlin, mas com uma abordagem de estilo de programação mais funcional sem o uso de exceções.

Vários são os fatores que podem nos afetar na hora de decidir por usar uma programação mais tradicional como no artigo anterior ou uma programação mais funcional como esta, como conhecimento da equipe, prazos de entrega, tipo de projeto, etc.

Portanto, dependendo do contexto em que você se encontra, pode ser mais aconselhável ou prático decidir por uma ou outra opção.

Como sempre, conhecer bem o contexto em que você se encontra é a chave para decidir.

Você pode ver o código-fonte do código aqui. (Do autor)