Это первый пост из серии публикаций, в которых объясняется мое понимание архитектуры приложений для Flutter. Предупреждаю — это будет весьма самоуверенным.
Пока запланированы:
- Введение (этот пост)
- Основы Dart Streams
- RxDart: магические трансформации потоков
- Основы RxVMS: RxCommand и GetIt
- RxVMS: Службы и Менеджеры
- RxVMS: самодостаточные виджеты
- Аутентификация пользователя посредством RxVMS
Предисловие
Я в программировании уже около 20 лет. Начал мобильную разработку 4 года назад с Xamarin.Forms, ибо кроссплатформенность была единственной побудительной причиной для меня в качестве инди-разработчика. Xamarin.Forms буквально толкают тебя к использованию паттерна MVVM, так как определение UI ведется в XAML, и тебе необходим какой-то слой, чтобы склеивать UI с Моделью. В процессе работы с Xamarin я познакомился с ReactiveUI и был буквально покорен потоками и реактивными расширениями (Rx), сделавшими мои приложения более надежными.
В то время, как в Xamarin.Forms MVVM были "из-коробки", при переходе к Flutter я был удивлен, что в нем не было никаких похожих шаблонов проектирования. Я начал исследовать различные предлагаемые подходы, но ничего из имеющегося не удовлетворило меня в полной мере:
- InheritedWidget: никак не получалось заставить обновлять только изменившуюся часть дерева виджетов, так что я использовал его лишь для доступа к классам модели, публикующим dart-потоки (Dart Streams), но вскоре отказался от этой идеи в пользу шаблонных Service Locator
- Scoped Model поинтереснее, нежели
InheritedWidget
, однако не давал мне столько гибкости, к которой я привык с ReactiveUI - Redux был тем шаблоном, который рекомендовало множество разработчиков, знакомых с React Native. У меня есть целый пост на тему, почему он мне не по душе
- BLoC: если бы я не приступил уже к разработке собственного паттерна в то время, когда BLoC стал продвигаться, скорее всего я бы взялся за него, так как это действительно гибко и реактивно. Что мне не нравится, так это то, что он публикует приемники потоков (Stream Sinks) и я не могу просто взять и передать функции или команды в обработчик событий виджета. Кроме того, BLoC не говорит вам, как следует структурировать ваше приложение в целом, нет также четкого определения, насколько большим должен некий BLoC или же какова его область действия
- MVVM: так как я работал именно с ним, это первое, что я надеялся претворить во Flutter. Но нет! Смысл ViewModel чтобы изящно обеспечить представление вашей модели во View посредством привязок. Но Flutter не обновляет свои модели новыми данными, он всегда перестраивает их, как я уже описывал. Кроме того, ViewModels должна всегда быть синхронизирована с базовой моделью, что приводит к неприятным багам, и реальность показывает, что обещанное преимущество повторного использования ViewModels в приложениях почти никогда не достигается. У Adam Pedley есть отличный пост по поводу этих недостатков
Жестокая правда об избыточности слоев
Является почти догмой представление о том, что в разработке вы всегда должны строить свое приложение в несколько слоев, каждый из которых имеет доступ лишь к нижележащему, поскольку это позволит вам:
- переиспользовать слои в других проектах
- прозрачно заменять один слой на другой
- упрощать тестирование
Однако:
- в моей практике не было случая, где бы я наблюдал полное переиспользование слоев. Если у вас есть универсальный код, который можно переиспользовать, имеет гораздо больше смысла собрать его в некую универсальную же библиотеку;
- замена слоев целиком также не является общераспространенной практикой. Большинство людей вряд ли заменят базу данных после того, как приложение выйдет на определенный этап разработки, так зачем добавлять слой абстракции для нее. Ну и в случае если потребуются какие-то наши текущие инструменты разработки, сделать рефакторинг довольно легко;
- то, что действительно работает, так это упрощение тестирования
Я отнюдь не против использования слоев, однако должны ли мы так безоглядно следовать этому правилу, как было ранее. Избыточное их применение приводит к увеличению кода, и потенциально может создать проблемы при сохранении единого источника состояния приложения. Поэтому применяйте слои тогда, когда это действительно необходимо, а не исходя из "лучших практик".
Идеальная архитектура для Flutter
Так что же я ожидаю от идеальной архитектуры?
- Простоты понимания работы приложения. Для меня это наиболее важная цель. Новые разработчики, включающиеся в работу с существующим кодом должны легко понять структуру разработки
- Простоты командной разработки
- Сама архитектура должна быть проста для понимания и сопровождения
- Никакого шаблонного кода для костылей в разработке
- Поддержка реактивного стиля Flutter
- Простоты отладки
- Производительность не должна страдать
- Легкости расширения
- Простоты тестирования
- Возможности сфокусироваться на работе приложения вместо плутания в исходниках
Самостоятельность виджетов
Исходя из природы элементов интерфейса "без состояния" (stateless) ни одна страница/виджет во Flutter не должна зависеть от других или влиять на них. Это приводит к мысли, что каждая страница/виджет должна автономно нести ответственность за отображение себя и всех своих взаимодействий с пользователем.
RxVMS
RxVMS это эволюция паттерна RxVAMS, описанного в предыдущем посте, в процессе практического применения которого были выявлены и исправлены некоторые проблемы.
Текущий результат всех этих мыслей — паттерн RxVMS, или Rx-View-Managers-Services. Он выполняет все вышеперечисленные задачи с единственным требованием, что вы должны понимать потоки и элементы Rx. Чтобы помочь вам с этим, я посвящаю следующий пост.
Вот краткая схема моего приложения
Services (Службы)
Это абстракции интерфейсов во внешний мир, которыми могут служить базы данных, REST API, и т.д. Они никак не влияют на состояние приложения.
Managers (Менеджеры)
Менеджеры группируют семантически схожий функционал, такой как аутентификация, процедуры заказа, и тому подобное. Менеджеры манипулируют состоянием объекта.
Любое изменение состояния (изменения данных приложения) должно производиться только через менеджеры. Как правило сами менеджеры не хранят данные, за исключением случаев, критичных для производительности, или констант времени выполнения.
Менеджеры могут служить прокси-источниками данных, в случае, если требуется определенная их трансформация после запроса из сервисов. Примером может служить комбинирование данных из двух и более источников для отображения во View.
Менеджеры могут взаимодействовать между собой.
Views (Отображение, вьюхи)
Как правило это StatefullWidget или StreamBuilder, которые в состоянии использовать данные из менеджеров и сервисов. Это может быть целая страница или виджет. Вьюхи не хранят никакие состояния и могут напрямую контактировать с сервисами пока соблюдается это правило.
Domain Objects (объекты предметной области)
Хоть они и отсутствуют в диаграмме, это важные сущности, которые представляют бизнес-модель. В других паттернах они могут принадлежать отдельному слою (бизнес-модели) совместно с бизнес-логикой. Здесь, в RxVMS, они не содержат никакой логики, изменяющей состояние приложения. Почти всегда это простые типы данных — plain data objects (если бы я включил их в паттерн, он бы стал выглядеть как RxVMMS, что длинновато, и ведет к путанице — VM мог бы быть неправильно воспринят, как ViewModel). Логически domain objects располагаются в слое менеджеров.
В следующих постах будет раскрыта сущность и взаимодействие этих элементов.
Комментарии (4)
mayorovp
07.05.2019 09:51Если у вас есть универсальный код, который можно переиспользовать, имеет гораздо больше смысла собрать его в некую универсальную же библиотеку;
А эта библиотека что, уже как бы и не слой?
rookie_cruekie Автор
07.05.2019 11:45Ну, автор высказывается по поводу избыточного вертикального «расслоения», а эта библиотека — фундамент для Managers, другими словами, разделение архитектуры по горизонтали.
rumyancevpavel
Надеюсь это просто опечатка — нижележащие слои должны быть зависимы на более высокоуровневых слоях. Смысл подобного разделения на слои не только в повторном использовании, но ещё и в локализации влияния изменений (и тестируемости, как вы и сказали). Изменения в низкоуровневый код не должны приводить к изменениям в высокоуровневом коде — бизнес логике, сущностях и юзкейсах. Горизонтальное разделение архитектуры — это так же и способ организации процесса командной разработки. Повторно использовать, как правило, приходится только высокоуровневый код, именно для его изоляции и существует слоистая архитектура. Игнорирование этого принципа приводит к монолитному коду и всем вытекающим последствиям. Я не могу похвастаться таким большим опытом как у вас, но за свою скромную карьеру мне приходилось не раз повторно использовать существующую бизнес логику.
rookie_cruekie Автор
Думаю да, опечатка, я вроде аккуратно переводил. Возможно, для автора английский не совсем прям, как родной.
Два момента:
sqlite
вообще безальтернативна.InheritedWidgets
&BLoC
для меня это действительно находка. Вот, на днях переведу остальное.