BLoC – Компонент бизнес-логики – Часть 1

Перевод статьи https://plugfox.dev/business-logic-component-1/

Введение

Мы начинаем цикл статей о компонентах бизнес-логики, также известных как BLoC.

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

Эта статья будет обзором, а не попыткой быть всеобъемлющим учебным пособием или пересказом документации. Скорее, это вишенка на торте и попытка поделиться реальным инженерным опытом после четырех лет использования подхода.

Здесь вы не увидите рекламу подхода или пакета. Это объективная статья, освещающая проблемы, а не очевидные моменты. Мы постараемся натолкнуть на разные мысли, дать пищу для размышлений, показать интересные моменты.

Также не будет лжи типа: «ЭФФЕКТИВНОСТЬ», «ПРОИЗВОДИТЕЛЬНОСТЬ», «ОРГАНИЗАЦИЯ», «ЛЕГКОВЕСНЫЙ», «МОЛНИЕНОСНЫЙ», «ПАМЯТИБЕРЕЖЛИВЫЙ», «ПРОСТОЙ» — мы уверены, что все уже достаточно устали от таких заявлений, а для разнообразия иногда надо быть инженером, а не торговцем воздуха.

Если вы хотите как можно быстрее начать работу с BLoC, эта статья — не лучший выбор, лучше сразу перейти к документации к пакету.

Считайте это попыткой передать опыт, накопленный нами за годы практики и ошибок.

Эта статья не привязана строго к конкретному пакету или решению и носит универсальный характер.

Тем не менее, в начале можно сохранить ссылку на пакеты stream_bloc (0.5.2) и block (8.1.1).

А также постараемся указать на недостатки пакета и вину авторов.

Текущие стабильные версии flutter 3.3.4 и dart 2.18.2.


Что вам нужно, чтобы начать работу с BLoC

Если вы новичок в Dart и Flutter, лучше всего начать с основных понятий.
Эта статья не для вас.
Учебники
Быстрый старт
Экскурсия по языку Dart
Экскурсия по основным библиотекам

Также, чтобы начать разбираться с BLoC, вы должны быть знакомы с Future и понимать Stream, так что если вы еще не заставили себя думать «реактивно», то сейчас самое время. Без этого дальнейшее чтение бессмысленно.

Чтобы наверстать упущенное, вы можете попробовать «Async & Await» и «Streams», и вам поможет ежедневная асинхронная ката в dartpad.

Также неплохо прочитать “Простое управление состоянием приложения“.
Узнайте, как использовать ChangeNotifier, ValueListenable, AnimatedBuilder и ValueListenableBuilder.

Если вы уже знакомы с вышеизложенным, вы готовы, и у вас не должно возникнуть никаких проблем.


Шаблоны

Шаблон “Состояние”

Состояние — это поведенческий шаблон проектирования, который позволяет объекту изменять свое поведение при изменении его внутреннего состояния. Создается впечатление, что объект изменил свой класс.

Шаблон “Состояние”

Как видно на картинке, один и тот же персонаж Марио может находиться в разных состояниях.
А возможность перехода из одного состояния в другое реализуется через конечный автомат, но об этом ниже.

Подробнее о состоянии:
refactoring.guru
wikipedia.org

Шаблон “Наблюдатель”

Наблюдатель — это поведенческий шаблон проектирования, который позволяет вам определить механизм подписки для уведомления нескольких объектов о любых событиях, происходящих с объектом, за которым они наблюдают.

Шаблон “Наблюдатель”

Для этого во флаттере вы можете использовать:
Listenable (например, ChangeNotifier, ValueNotifier) с виджетами AnimatedBuilder или ValueListenableBuilder
Stream с помощью виджета StreamBuilder

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

Подробнее о преобразованиях Listenable и использовании с ValueListenableBuilder можно прочитать здесь.

Шаблон “Наблюдатель”

Например: Представьте, что вы делаете игру «горячо-холодно» в сфере, где пользователь с помощью телефона должен найти место, где зарыт «клад».

Каждый раз, когда расстояние до сокровища сокращается, экран становится краснее. При удалении, наоборот, синеет.

Получается нужно связать геопозицию с цветом виджета на экране.

И каждое изменение геолокации должно вызывать отклик интерфейса.

Подробнее о наблюдателе:
refactoring.guru
wikipedia.org

Конечный автомат
Конечный автомат — это модель поведения. Он состоит из конечного числа состояний и называется автоматом с конечным числом состояний. Машина выполняет переходы между состояниями и производит выходные данные на основе текущего состояния и данного ввода.

Конечный автомат

Например: представьте, что пользователь должен пройти некий поток или последовательность шагов в строго определенном порядке, когда за определенными состояниями должны следовать строго определенные события.
Проснуться -> Одеться -> Выйти на улицу -> Купить молока -> Вернуться -> Сделать латте
или же
Проснись -> Позвони и закажи молоко -> Дождись курьера -> Забери молоко -> Приготовь латте

Как бы вы спроектировали такую последовательность событий и состояний?

Подробнее о конечном автомате:
refactoring.guru
wikipedia.org

Шаблон “Издатель-подписчик”

Издатель-подписчик — это шаблон, который разделяет и упраздняет связь между двумя уровнями и улучшает масштабируемость.
Это один из лучших шаблонов для масштабирования программного обеспечения и систем.

Шаблон “Издатель-подписчик”

Подробнее о шаблоне “издатель-подписчик”:
refactoring.guru
wikipedia.org


Проблемы, которые необходимо решить – состояние гонки

Одной из наиболее типичных проблем при проектировании является состояние гонки и непоследовательность. Такое поведение довольно сложно обнаружить, отследить и предсказать. Он превращается в плавающие ошибки.

Чтобы избежать таких проблем, вам нужно будет обновить свой контроллер, добавив в него управление очередями.
Но контроллер с такой функциональностью уже существует, и называется он BLoC.

Нам нужно управлять порядком событий.

Наиболее часто используемая стратегия обработки – один за другим, строго в хронологическом порядке вызова. Это наиболее часто используемый случай, который рекомендуется по умолчанию.

Мы можем отбрасывать последующие события, когда идет обработка. Например, если персонаж в компьютерной игре прыгнул, он не может снова прыгнуть, находясь в воздухе, пока не приземлится. Также этот метод хорош для аутентификации.

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

Пример 1
Предположим, у вас есть обновляемый ListView.
И для него вы создали контроллер (ChangeNotifier, mobx или любой другой «менеджер состояний») с методом «request».

Вызывая этот метод, вы делаете следующее:
а) Установить состояние Загрузка – показывать лоадер
б) Взять текущее состояние курсора и запросить N элементов у бэкенда.
в) Установить статус Готово
г) Разверните текущий список и установите новый курсор.

Что произойдет, если пользователь вызовет этот метод дважды или более? Какие беды ждут нас на этом пути?

а) Вы можете сделать много дополнительных запросов к серверной части. Сколько раз на кнопку нажмешь, столько и запросов будет.
Например: Инициализация -> Загрузка -> Загрузка -> Загрузка -> Загрузка -> …

б) Состояние «Готово» будет установлено первым завершенным (не первым в хронологическом порядке!) методом, в то время как второй еще выполняется. То есть ваш список будет отображать обработанное состояние, пока обработка еще продолжается. А потом вдруг состояние снова изменится.
Например: Инициализация -> Загрузка -> Загрузка -> Готово[Все еще загружается, но состояние уже Готово]-> Готово

в) Окончательное состояние «Готово» будет устанавливаться не последним вызовом, а тем, который будет обрабатываться дольше всех. Например, даже если один из первых запросов зависнет, а потом отвалится по таймауту, будет отображаться ошибка просто потому, что запрос длился дольше остальных. Даже если текущее состояние уже получено и передано.
Например: Инициализация -> Загрузка -> Загрузка -> Готово -> Ошибка

Пример 2
У вас есть форма ввода с несколькими полями (например, профиль пользователя), которые можно обновлять и есть кнопка, при нажатии которой данные обновляются.
Запросы к серверной части могут занимать больше времени, чем обычно, или вообще завершаться ошибкой.

Попробуйте сами представить, что может пойти не так в транзакциях обновления данных.

Например: asyncController..update(‘Джон’)..update(‘Энн’)..update(‘Илон’);
Можете ли вы точно предсказать, какое значение будет установлено на сервере?

Вероятно, стоит считать результат только последнего вызова или вообще выполнять строго в очередь на добавление таких действий.

Чем больше параллельной обработки внутри контроллера, тем больше накапливается ошибок несогласованности!

Пример 3
У вас есть контроллер аутентификации.

Можно ли одновременно войти в систему как пользователь №1 и войти в систему как пользователь №2 и выйти из системы?

Или все же нужно игнорировать остальные при выполнении одного из этих действий?


Проблемы, которые необходимо решить – связность

В программной инженерии cвязность — это степень взаимозависимости между программными модулями, мера того, насколько тесно связаны две подпрограммы или модули, а также сила взаимосвязей между модулями.

Свсязность и сплоченность

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

Прим. пер. – Подробнее про связнность и плотность

Чтобы уменьшить связанность и повысить удобство сопровождения кода, нам может помочь многоуровневая архитектура.

Например, во Flutter SDK мы можем наблюдать такой подход при отделении слоя Widget от слоя Presentation построения интерфейса с помощью WidgetsBinding.

Связность между виджетами и элементами устанавливается с помощью BuildOwner.

Подробнее о внутренностях Flutter можно прочитать здесь:
Flutter architectural overview
Inside Flutter
Flutter internals
Widget – State – Context -InheritedWidget

Как bloc помогает нам организовать наш код и разделить его по уровням?
Мы можем наслоить наше приложение, как торт Наполеон. Например, мы можем выбрать следующие слои:
– Слой виджетов(Widgets layer) — декларативное описание конфигурации нашего приложения (например, HomeScreen, InheritedUser, LogoutButton)
– Компонент бизнес-логики — посредник между виджетами и данными, а также управление порядком обработки (например, AuthenticationBLoC, SettingsBLoC, CartBLoC)
– Данные — базы данных, клиенты, репозитории, поставщики данных (например, HttpClient, SQLiteDatabase, AuthenticationRepository, SettingsLocalDataProvider)
Таким образом, мы отделяем слой виджетов от слоя данных с помощью: состояния, потока состояний и метода, который добавляет события.
Кроме того, в виджетах не фиксируются логические ошибки.


BLoC как концепция

Основная идея использования BLoC (компонент бизнес-логики) — отдельные уровни виджета и данных. Разделение происходит по шаблону “Издатель-подписчик”. Это также служит цели предсказуемой последовательности преобразования событий в состояния, избавления от состояния гонки и изоляции логических ошибок от виджетов.

Это делает архитектуру более удобной, понятной и масштабируемой. Это также уменьшает связность модулей, разделяя код на уровни абстракции. BLoC — типичный шаблон для объектно-ориентированного реактивного программирования.

BLoC

Идея концепции заключается в том, что UI может генерировать события (пользователь нажал на кнопку) и реагировать на изменение состояния (сгенерирован запрос, отправлен запрос, получен ответ из кэша, получен ответ от сервера). Внимательный читатель заметит важный момент: одному событию может (и должно) соответствовать множество состояний. А также для события может вообще не быть изменений состояния, например, попытка выхода из системы неавторизованным пользователем. Такой подход сделает ваш интерфейс более отзывчивым.

Виджет добавляет событие и преобразование BLoC и выдает на нем состояния 0..n.

Поскольку виджеты могут взаимодействовать только с методом «добавить», «потоком» и геттерами «состояния», это помогает ослабить связь между слоями. А поскольку порядком событий можно управлять, невозможно получить состояние гонки или несогласованное состояние при правильном использовании. В любом другом менеджере состояний или решении такие проблемы являются обычными и очень трудно решаемыми.

Думайте о слое бизнес-логики как о мосте между виджетами (уровень приложений) и уровнем данных. Уровень бизнес-логики уведомляется о событиях/действиях от уровня виджетов, а затем связывается с репозиторием, чтобы создать новое состояние для использования уровнем представления.


BLoC как успешный конечный автомат

Первоначальная реализация паттерна BLoC, представленная Google, не содержала никаких событий и сопоставителей событий, которые работали по сути как редюсеры. Его можно было бы по существу сжать до фразы – «тонет, вытекает». Самую первую реализацию можно увидеть на этой презентации, которая состоялась в 2018 году.

После этого была написана статья фантастическим парнем-флаттеристом — Дидье Буленсом. Эта статья расширила концепцию, добавив еще один уровень косвенности и создав конечный автомат с централизованным процессором событий.

Это не первая реализация машины состояний массового использования, которая связывает состояния с событиями с помощью редюсера. Первым был Elm. Elm — это функциональный язык для веб-приложений с синтаксисом, очень близким к языку Haskell, который изначально использовал подход FRP (функционально-реактивное программирование), но позже принял другую однонаправленную архитектуру, получившую название The Alm Architecture, TEA или MVU. Обновление представления модели. Позже, в дополнение к этой работе, был создан Redux, который подпадает под категорию MVU и открыто заявляет, что он сильно вдохновлен Elm.

Учитывая все различия, еще одним, что демонстрирует BLoC, является его декларативность. Учитывая, что BLoC теряет чистоту и полноту своего «редьюсера», он возвращает декларативность, управление побочными эффектами и ошибками управления — вещи, с которыми борются предыдущие реализации реактивных конечных автоматов, особенно Redux. Ниже приведены примеры воображаемых запросов статей, выраженных в MVU, переведенных в Dart, Redux с преобразователями и BLoC с генераторами (stream_bloc). Можно заметить разницу в декларативности.

BLoC не только позволяет декларативно констатировать, что нужно, не создавая императивные события, которые только изменяют состояние, но и управляет ошибками, позволяя, таким образом, повторно вызывать редюсер — объявляя, что что-то пошло не так. Исходя из этого, редюсер в стиле BLoC не только самый декларативный, но и самый лаконичный.


Если мы говорим о ранних версиях, то bloc Felix Angelov был сделан на rxdart, но сейчас не имеет сторонних зависимостей (зависимость от «мета» не рассматриваем, потому что ее можно считать частью SDK).

Пакет flutter_bloc экспортирует пакет основного блока и содержит только виджеты, реагирующие на изменения состояния. Пакет относительно стабилен, с недавно приобретенными интерфейсами для основных классов, удовлетворяющих требованиям LSP, ISP и DIP.

Несмотря на то, что это очень популярный пакет, у него есть несколько фундаментальных проблем, о которых следует знать.

  1. Вместо того, чтобы вручную отображать события в поток состояний, декларативную асинхронную последовательность изменений, bloc использует эмуляцию сопоставления с шаблоном, чтобы выбрать вариант события, на который следует реагировать, и использует функции более высокого порядка для императивной генерации состояний. Сопоставление с шаблоном хорошо работает, если не используется freezed, но императивное испускание состояний делает API более подробным, более императивным и нарушает последовательность состояний. Проблема с последовательностью обсуждается ниже.
  2. Пакет block не может создавать идентичные состояния. Это поведение можно сравнить с вызовом ValueNotifier или .distinct() в начале потока. Звучит хорошо на первый взгляд, но на самом деле это бессмысленно и вредно одновременно. Поскольку состояния блоков обычно потребляются посредством сопоставления на уровне виджетов либо через BlocSelector, либо через context.select Provider, различие в начале потока бессмысленно. Насчет вредности — реагировать даже на одинаковые состояния через BlocObserver можно — но нельзя в данной реализации. Более того, такая фильтрация не гарантирует, что в билдер не попадут одинаковые состояния, так как перед обратным вызовом билдера происходит повторная фильтрация состояний с помощью buildWhen, на выходе которой можно получить одинаковые состояния.
  3. Преобразование событий в состояния также достигается за счет функций высшего порядка, а преобразования переходов не существует. Это создает тот же набор проблем, что и с первым пунктом — стремление уйти от родных средств языка ограничивает.

BLoC как логичный выбор для Dart

Каждый язык особенный и имеет свои хитрости в рукаве. Точно так же не все решения подходят для всех языков.

MVU — логичное решение для Elm, потому что Elm — это MVU. Вся его работа с побочными эффектами (включая HTTP-запросы) построена с использованием абстракции Cmd, которую использует его редьюсер, у него есть кортежи, и все в нем прекрасно вписывается в эту парадигму.

JS/TS — очень динамичный язык, и Redux с его сильно полиморфными и динамическими типами и подключаемыми модулями, которые подходят для всех видов слотов, JS отлично работает с ним. Самый очевидный пример — Thunk middleware — с ним редюсер может принимать не только действия, но и их преобразователи.

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

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


Чем не является BLoC

– BLoC не является функцией от события к состоянию. Любое заданное добавленное событие может произвести ноль, одно или бесконечное количество изменений состояния и привести к тому же одному или совершенно другому состоянию после обработки события.

– BLoC не является ViewModel или Presenter. BLoC — это компонент бизнес-логики с сохранением состояния — он ничего не знает о представлении. Все происходит наоборот — представление или виджеты знают о BLoC и адаптируются к использованию его значений, возможно, получая состояния.

– BLoC — это не хранилище данных для одного экрана, содержащего все состояния. Неправильно создавать «один BLoC на экран».

– BLoC не является централизованным хранилищем состояния. В отличие от Redux, TEA и некоторых других однонаправленных решений, BLoC децентрализован, и должно быть несколько BLoC для разных целей. Это Компонент.

– BLoC не реализован в своей основе. Это абстракция. Таким образом, BLoC ничего не должен знать о базовой реализации, с которой он работает, и не должен раскрывать какие-либо модели баз данных или DTO в своих состояниях.


В следующих статьях мы рассмотрим:
– слои и структура проекта
– примеры
– полезные советы, советы по дизайну и хитрости
– подводные камни

 

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

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