Левицкий Даниил @levitckii-daniil
Пользователь
Информация
- В рейтинге
- Не участвует
- Откуда
- Санкт-Петербург, Санкт-Петербург и область, Россия
- Дата рождения
- Зарегистрирован
- Активность
Специализация
Mobile Application Developer
Lead
Flutter
Dart
Kotlin
Android development
Development of mobile applications
Java
Designing application architecture
SQL
JavaScript
Node.js
Мы согласны, что throw является допустимым подходом для обработки ошибок и исключений, особенно с учетом разделения Error и Exception в Dart. Но давайте я попробую расшифровать, какие структурные проблемы решает Either в нашем примере использования.
Это больше история про реализацию, а не про контракт. Например, у вас есть интерфейс:
Пока вы не обратитесь к его реализации, вы не сможеет узнать, выбрасывает он UnAuthorizedError или нет (может он вообще ходит в сеть и выбрасывает Http-ошибки), а если реализаций несколько, то все еще хуже, ошибки могут быть разные, а ваш код об этом даже не узнает. В java, например, было ключевое слово 'throws', позволяющее указать на уровне интерфейса, какие ошибки выбрасывает метод, также оно заставляет вас обработать эти ошибки в месте вызова. В Dart, кажется, такой функциональности нет.
А если говорить об Either, то вы четко на уровне контракта описываете допустимые ошибки:
При вызове такой функции вы получите Either, а не результат TimeResponse, что гарантированно заставит вас обработать ошибку (CommonResponseError). В случае, если тип ошибки изменится, то типизация вам подскажет все места, где сломался контракт.
Этот случай как раз и показывает проблемы, которые могут возникать при перевыбрасывании. Например, если ваш интерактор обращается к 3 репозиториям, каждый из которых выбрасывает ошибки со своими типами, то, перевыбрасывая ошибки этих типов, его контракт раздувается и становится очень сложен для восприятия, а это очень частый кейс при работе с throw. Потом эта ошибка начинает протекать во все слои вашего приложения и, при смене контракта, вносить правки становится очень тяжело.
Хотел заметить, что ошибки, которые генерируюстя не в UI-слое, архитектурно находятся за пределами UI-слоя, поэтому проникновение обработки ошибок в UI нарушает чистоту архитектуры. Еще одна проблема - завязывание виджета на ошибки делает его плохо переиспользуемым, потому что для воспроизведения этих кейсов в других местах, придется выбрасывать такие же ошибки.
После досточного длительного опыта работы в нативной разработке с Java/Kotlin, где сильно распространены throw/try/catch, довольно сложно к ней "не привыкнуть" :)
Сейчас в проекте у нас сгенерированый файл с деревом зависимостей приложения занимает 600 строк, но это не человеко-читаемый код, потому что он генерируется, если бы мы его писали руками и с использованием Provider, он скорее всего бы дошел до 1000-1500 строк. Плюс проект у нас пока достаточно молодой, если апроксимировать цифры на наш основной нативный проект, то это была бы история про 10000+ строк.
Мы не утверждаем, что это колосальная проблема или что Provider не применим для реальных проектов. Также скорее всего придумать решение по разбиению зависимостей на модули, чтобы работа с зависимостями была организована проще и удобнее, не очень сложно.
Однако для нашей команды намного привычнее концепция GetIt для управления зависимостями, весь инфраструктурный код генерируется, а ЖЦ каждого объекта наглядно виден в коде самого объекта. Это досточно серьезные плюсы для нашей команды, и при этом мы не видим каких-то минусов или проблем, создаевамых из-за использования GetIt+Injectable.
Благодарим за обратную связь, мы всегда рады услышать мнение других и понять движемся ли мы в правильную сторону!
Хотим обратить внимание, что мы просто делимся опытом решения, которое выработали за длительное время и с проведением экспериментов конкретно под наши задачи. Мы не настаиваем, что оно единственно верное и все проекты должны быть такими, поэтому просим не расценивать ответы на тезисы, как вступление спор:
1. Const - подразумевают исключительно константы. Если называть их util, подразумеваются утилитарные вещи - константы / общие функции без домена / экстеншены над dart core. У нас был такой опыт, утилиты стали доходить до нескольких тысяч строк кода, поэтому мы приняли решение сделать более жесткое сужение.
2. Про arch вы действительно правы, мы указали, что в идеальном мире они должны быть вынесены в отдельные pub-package, но на этапе, пока решение не задокументировано и покрыто тестами, его лучше не выливать в package для других внутренних/внешних потребителей. Сделать arch саб-модулем решение хорошее, мы к нему тоже пришли. Про свой опыт модуляризации, в том числе пакета arch, планируем рассказать в будущем, это достаточно неоднозначная тема.
3. mappers - вещь опциональная, даже на уровне статьи мы нигде не указываем ее, как обязательную часть нашей структуры директорий. Хотим заметить, что их область использования чуть шире, чем fromJson и toJson, потому что вы можете работать не только с REST, а также в Mapper вы можете добавить проверки соблюдения контракта с бекендом, аналитику нарушений контрактов и тд. Из-за этого логика маппинга начинает распухать и раздувать модели, что, по-нашему мнению, удобнее вынести в изолированный объект. Вполне возможно наше восприятие было искажено длительной Kotlin/Java разработкой, и поэтому мы приходим к таким выводам.
4. Вы правы, SingleResult фактически и есть StreamController, развёрнутый над Bloc, и StreamListener, развёрнутый в SrBlocBuilder, поэтому не очень понятно, в чем состоит костыльность абстракции. Ввод дополнительной абстракции, полностью совместимой с оригинальным Bloc, мы обосновываем экономией кода в каждом отдельном Bloc, но самое важное - мы можем строить инфраструктуру вокруг этой абстракции: централизованное логирование SR, аналитика, реализация TimeMachineDebuger с учетом SR.
5. Мы пробовали строить приложение исключительно на Provider, но скорость разработки значительно уменьшалась при росте используемых объектов. Когда проект разрастается и подходит к 30, 50 тысячам строк кода, собирать объекты руками становится достаточно проблематично. А для понимания ЖЦ объекта приходится обращаться к дереву виджетов, что не всегда очевидно, плюс иногда было необходимо обращаться к дереву без контекста.
Мы не исключаем, что мы просто не смогли его приготовить, но решение GetIt + Injectable позволило решить все наши боли без видимых потерь и повысить скорость работы. Мы не сделали абсолютно все объекты на GetIt, а использовали Provider(BlocProvider) из-за не стандартного ЖЦ объекта (factory/singleton), BLoC должен вести себя как Scoped-объект. А наши тесты использования Scope в GetIt показали, что проект начинает усложняться и пока она нужна только для StateManagment-объектов, нам удобнее использовать BlocProvider.
6. Мы упомянули в статье, что ThemeData + ColorScheme + TextTheme не бился с дизайн-токенами, которые были сформированы в нашей компании задолго до начала разработки на Flutter и поэтому поделились свои решением, которое позволяет решить эту проблему. Нам показалось, что это достаточно полезно для команд в похожей ситуации. Также решение в виде централизованного Bloc над сменой темы позволяет включать всю инфраструктуру Bloc (логирование, аналитика и так далее) в этот процесс, а также менять тему без контекста. Хотим заметить, что мы сделали минимально необходимый mapping нашего объекта темы в стандартную тему.
По нашему опыту обилие StateManager-решений характерно и для нативной разработки. Если говорить про Flutter-решения, то их обилие косвенно связанно с разным бэкграундом разработчиков, переходящих во Flutter, также у каждого StateManager есть свои плюсы, минусы и ограничения, так что тема действительно актуальная, будем стремиться ее раскрыть. Благодарим за обратную связь!
В нашей статье мы не разрабатывали новый StateManager, а использовали один из наиболее популярных - Bloc, с небольшой доработкой SingleResult для приближения к MVI. Но ваша идея хорошая, постараемся в будущем разработать статью, сравнивающую разные StateManagment решения, возможно в окружении нашего “шаблона“.