Abordagem funcional para tratamento de erros no Dart

Na transição para uma nova tecnologia, perdemos as ferramentas usuais para o desenvolvimento. Em alguns casos, somos forçados a aturar sua ausência devido a algumas limitações técnicas, mas, se possível, carregamos as ferramentas conosco. Ao desenvolver aplicativos para Android, tomei o exemplo de uma arquitetura limpa proposta por Fernando Cejas como base. Compreendendo os padrões de design usados ​​no Flutter, decidi abandonar essa arquitetura em favor do BLoC. Eu me acostumei rapidamente a esse modelo, ele é muito semelhante ao MVVM com o qual trabalhei anteriormente, mas não queria aturar um detalhe. Ao chamar métodos de repositório, tive que capturar exceções, convertê-las em algum tipo e, de acordo com o tipo, criar o estado necessário. Na minha opinião, isso atrapalha bastante o bloco e eu portado o tipo Either usado anteriormente em projetos Android baseados em Fernando.


Ou vieram de linguagens de programação funcionais. Ele fornece o valor de um dos tipos possíveis:


  • Esquerda (em caso de falha);
  • Certo (se for bem-sucedido).

Implementação básica do Either
/// Signature of callbacks that have no arguments and return right or left value. typedef Callback<T> = void Function(T value); /// Represents a value of one of two possible types (a disjoint union). /// Instances of [Either] are either an instance of [Left] or [Right]. /// FP Convention dictates that: /// [Left] is used for "failure". /// [Right] is used for "success". abstract class Either<L, R> { Either() { if (!isLeft && !isRight) throw Exception('The ether should be heir Left or Right.'); } /// Represents the left side of [Either] class which by convention is a "Failure". bool get isLeft => this is Left<L, R>; /// Represents the right side of [Either] class which by convention is a "Success" bool get isRight => this is Right<L, R>; L get left { if (this is Left<L, R>) return (this as Left<L, R>).value; else throw Exception('Illegal use. You should check isLeft() before calling '); } R get right { if (this is Right<L, R>) return (this as Right<L, R>).value; else throw Exception('Illegal use. You should check isRight() before calling'); } void either(Callback<L> fnL, Callback<R> fnR) { if (isLeft) { final left = this as Left<L, R>; fnL(left.value); } if (isRight) { final right = this as Right<L, R>; fnR(right.value); } } } class Left<L, R> extends Either<L, R> { final L value; Left(this.value); } class Right<L, R> extends Either<L, R> { final R value; Right(this.value); } 

A implementação é muito básica, inferior às soluções em outros idiomas, mas lida com sua tarefa. Eu uso esse tipo como resultado de todos os métodos do repositório e o tratamento de exceções é transferido para a camada de dados. Isso elimina a necessidade de construções try / catch, o que torna o código mais legível.


Exemplo de tentativa / captura
 class ContactBloc { final ContactRepository contactRepository; ContactBloc(this.contactRepository); @override Stream<ContactState> mapEventToState(ContactEvent event) async* { if (event is GetContactEvent) { yield LoadContactState(); try { var contact = contactRepository.getById(event.id); yield ContactIsShowingState(contact); } on NetworkConnectionException catch (e) { yield NetworkExceptionState(e); } catch (e) { yield UnknownExceptionState(e); } } } } abstract class ContactRepository { Future<Contact>getById(int id); } 

Exemplo com qualquer
 class ContactBloc { final ContactRepository contactRepository; ContactBloc(this.contactRepository); @override Stream<ContactState> mapEventToState(ContactEvent event) async* { if (event is GetContactEvent) { yield LoadContactState(); final either = contactRepository.getById(event.id); if (either.isRight) { final contact = either.right; yield ContactIsShowingState(contact); } else { final failure = either.left; if (failure is NetworkFailure) yield NetworkFailureState(failure); if (failure is UnknownFailure) yield UnknownFailureState(failure); } } } } abstract class ContactRepository { Future<Either<Failure, Contact>>getById(int id); } 

Quanto à legibilidade, alguém pode argumentar. Talvez alguém esteja acostumado a tentar / pegar e tenha razão à sua maneira, na maioria das vezes isso é saboroso. Uma vantagem adicional é que nós mesmos podemos definir a hierarquia de falhas e retornar no lado esquerdo. Vamos fazer uma falha abstrata, tornar comum todos os recursos ServerFailure, NetworkFailure e alguns recursos específicos do ContactFailure para o recurso atual, com herdeiros. No bloco, saberemos exatamente qual das falhas esperar.


A desvantagem na implementação de Failure on Dart é a falta de classes seladas como no kotlin; caso contrário, não haveria ifs com o casting. A linguagem é jovem, se desenvolve ativamente e espero que chegue a hora e teremos ferramentas para escrever manipuladores de forma mais sucinta.


Alguém pode não gostar desta implementação, pode achar inútil, mas eu só queria apresentar a possibilidade de uma abordagem funcional para o tratamento de erros no Dart, embora o uso não tenha sido tão elegante quanto em outros idiomas.


Recursos:


Código fonte

Source: https://habr.com/ru/post/pt459757/


All Articles