Хабрахабр

[Из песочницы] Функциональный подход обработки ошибок в Dart

В каких-то случая мы вынуждены смириться с их отсутствием из-за каких-то технических ограничений, но при возможности переносим инструменты с собой. При переходе на новую технологию, мы лишаемся привычных инструментов для разработки. Разбираясь с используемыми во Flutter шаблонами проектирования, я решил отказаться от этой архитектуры в пользу BLoC. Разрабатывая android приложения, за основу я брал пример чистой архитектуры предложенной Fernando Cejas. При вызове методов репозитория я должен был ловить исключения, кастить их к какому-то типу и в соответсвии с типом, создавать необходимое состояние. К данному шаблону я быстро привык, он очень похож на MVVM, с которым работал ранее, но с одной деталью я никак не хотел мириться. На мой взгляд, это очень захламляет блок и я портировал тип Either используемый ранее в android проектах, основанных на Fernando.

Он предоставляет значение одного из возможных типов: Either пришел из функциональных языков программирования.

  • Left (в случае неудачи);
  • Right (в случае успеха).

Базовая реализация 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> /// 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);
}

Я использую этот тип как результат всех методов репозитория, а обработку исключений перенес в слой данных. Реализация совсем базовая, уступает решениям на других языках, но со своей задачей справляется. Это избавляет блока от конструкций try/catch, за счет чего код становится более читаем.

Пример с try/catch

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

Пример с either

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

Возможно кому-то привычен try/catch и будет по своему прав, по большей части это вкусовщина. По поводу читаемости, кто-то может возразить. Допустим сделать абстрактный Failure, от него сделать общие для всех фич ServerFailure, NetworkFailure и какие-нибудь специфичные для текущей фичи ContactFailure, с наследниками. Дополнительным преимуществом является то, что мы сами можем определить иерархию Failure и возвращать в левой части. В блоке мы точно будем знать, какие из Failure ожидать.

Язык молод, активно развивается и надеюсь, что придет время и у нас появятся инструменты, позволяющие более лаконично писать обработчики. Минусом в реализации Failure на Dart, является отсутсвие sealed classes как в kotlin, иначе бы не было этих if'ов с кастингом.

Кому-то может не понравится данная реализация, возможно сочтет ее бессмысленной, но я лишь хотел ознакомить Вас с возможностью функционального подхода обработки ошибок в Dart, хоть использование не получилось таким изящным как в других языках.

Ресурсы:

Исходный код

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть