Enfoque funcional para el manejo de errores en Dart

En la transición a una nueva tecnología, perdemos las herramientas habituales para el desarrollo. En algunos casos, nos vemos obligados a soportar su ausencia debido a algunas limitaciones técnicas, pero si es posible, llevamos las herramientas con nosotros. Al desarrollar aplicaciones de Android, tomé como ejemplo la arquitectura limpia propuesta por Fernando Cejas. Al comprender los patrones de diseño utilizados en Flutter, decidí abandonar esta arquitectura en favor de BLoC. Me acostumbré rápidamente a esta plantilla, es muy similar a la MVVM con la que trabajé anteriormente, pero no quería aguantar un detalle. Al llamar a los métodos de repositorio, tuve que detectar excepciones, convertirlas en algún tipo y, según el tipo, crear el estado necesario. En mi opinión, esto abarrota el bloque y porté el tipo Either utilizado anteriormente en proyectos de Android basados ​​en Fernando.


Cualquiera vino de lenguajes de programación funcionales. Proporciona el valor de uno de los tipos posibles:


  • Izquierda (en caso de falla);
  • Correcto (si tiene éxito).

Implementación básica de cualquiera
/// 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); } 

La implementación es muy básica, inferior a las soluciones en otros idiomas, pero hace frente a su tarea. Utilizo este tipo como resultado de todos los métodos del repositorio, y el manejo de excepciones se transfiere a la capa de datos. Esto elimina la necesidad de construcciones try / catch, lo que hace que el código sea más legible.


Ejemplo de prueba / 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); } 

Ejemplo con cualquiera
 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); } 

Con respecto a la legibilidad, alguien podría discutir. Quizás alguien está acostumbrado a probar / atrapar y estará en lo correcto a su manera, en su mayor parte esto es saborizante. Una ventaja adicional es que nosotros mismos podemos definir la jerarquía de Fallas y regresar en el lado izquierdo. Hagamos una falla abstracta, que sea común para todas las funciones ServerFailure, NetworkFailure y algunas funciones específicas de ContactFailure para la función actual, con herederos. En el bloque, sabremos exactamente qué falla esperar.


La desventaja de implementar Failure on Dart es la falta de clases selladas como en kotlin, de lo contrario no habría ningún problema con el lanzamiento. El lenguaje es joven, se está desarrollando activamente y espero que llegue el momento y tengamos herramientas para escribir manejadores de manera más sucinta.


Puede que a alguien no le guste esta implementación, puede que no tenga sentido, pero solo quería presentarle la posibilidad de un enfoque funcional para el manejo de errores en Dart, aunque el uso no resultó ser tan elegante como en otros idiomas.


Recursos:


Código fuente

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


All Articles