Введение
В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру front-end приложений.
Также, я работаю сейчас на языке Dart, в проектах использую Flutter. Поэтому часть постов будет более актуальна именно для этого стека. Я постараюсь помечать это тегами.
В основном, я придерживаюсь подхода Clean Architecture. Я буду говорить о бизнес-логике, о бизнес-моделях, провайдерах, сторейджах, презентаторах и представлениях, и как все это реализовать во Flutter-проектах, и почему это все надо реализовывать.
Основные действующие лица:
- Модель — это класс, описывающий единицу данных, с которыми работает приложение: "пользователь", "задача", "проект".
- Логика — это use cases, варианты использования. Это действия, которые пользователь может совершить над данными, чтобы их изменить. Например, "изменить права пользователя".
- Презентатор — платформо независимая штука, которая подготавливает данные к отображению и обрабатывает действия пользователя.
- ВьюМодель, ViewModel — класс, который содержит данные, необходимые для отображения интерфейса пользователя: подготовленные данные моделей, состояния экрана и т.д.
- Представление — реализация интерфейса пользователя, код, написанный на каком-то фреймверке, зависимый от платформы.
- data source — класс, загружающий данные из внешнего источника.
- Фактори — класс, который раздает конкретные реализации интерфейсов. Нужен для установки архитектурных границ.
Не все мысли являются законченными. Часто с опытом я их пересматриваю, ищу лучшие решения.
В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру front-end приложений.
Также, я работаю сейчас на языке Dart, в проектах использую Flutter. Поэтому часть постов будет более актуальна именно для этого стека. Я постараюсь помечать это тегами.
В основном, я придерживаюсь подхода Clean Architecture. Я буду говорить о бизнес-логике, о бизнес-моделях, провайдерах, сторейджах, презентаторах и представлениях, и как все это реализовать во Flutter-проектах, и почему это все надо реализовывать.
Основные действующие лица:
- Модель — это класс, описывающий единицу данных, с которыми работает приложение: "пользователь", "задача", "проект".
- Логика — это use cases, варианты использования. Это действия, которые пользователь может совершить над данными, чтобы их изменить. Например, "изменить права пользователя".
- Презентатор — платформо независимая штука, которая подготавливает данные к отображению и обрабатывает действия пользователя.
- ВьюМодель, ViewModel — класс, который содержит данные, необходимые для отображения интерфейса пользователя: подготовленные данные моделей, состояния экрана и т.д.
- Представление — реализация интерфейса пользователя, код, написанный на каком-то фреймверке, зависимый от платформы.
- data source — класс, загружающий данные из внешнего источника.
- Фактори — класс, который раздает конкретные реализации интерфейсов. Нужен для установки архитектурных границ.
Не все мысли являются законченными. Часто с опытом я их пересматриваю, ищу лучшие решения.
Безопасное программирование
Выделю два типа ошибок в коде: ошибки, которые есть прям щас, и ошибки будущие.
Первый тип ошибок устранять в основном просто: запусти код и убедись, что он работает. Можно даже написать тесты для проверки. Или даже сначала написать тесты, а потом код, который их проходит.
Второй тип ошибок сложный для обнаружения и исправления.
Например, в одном месте проекта есть перебор вариантов значения параметра value через множество
В реальности это может выглядеть так, что в каком-то далеком диалоговом окне, на первый взгляд слабо связанным с новыми изменениями кода, не отобразятся дополнительные параметры, или даже будет исключение при попытке его открыть.
Если бы перебор значений происходил не с помощью условий, а с помощью оператора выбора
То есть, если мы пишем код определенным образом, ошибки становятся очевидными и простыми для исправления. Это я и называю безопасным программированием.
Многие архитектурные решения в большой степени существуют и для того, чтобы код был безопасным.
Выделю два типа ошибок в коде: ошибки, которые есть прям щас, и ошибки будущие.
Первый тип ошибок устранять в основном просто: запусти код и убедись, что он работает. Можно даже написать тесты для проверки. Или даже сначала написать тесты, а потом код, который их проходит.
Второй тип ошибок сложный для обнаружения и исправления.
Например, в одном месте проекта есть перебор вариантов значения параметра value через множество
if-else. Когда через месяц в другом месте проекта мы расширим набор возможных значений value, ничто не скажет нам о том, что нужно изменить первое место.В реальности это может выглядеть так, что в каком-то далеком диалоговом окне, на первый взгляд слабо связанным с новыми изменениями кода, не отобразятся дополнительные параметры, или даже будет исключение при попытке его открыть.
Если бы перебор значений происходил не с помощью условий, а с помощью оператора выбора
switch, статический анализатор сразу бы показал необходимые места исправлений.То есть, если мы пишем код определенным образом, ошибки становятся очевидными и простыми для исправления. Это я и называю безопасным программированием.
Многие архитектурные решения в большой степени существуют и для того, чтобы код был безопасным.
Не слушайте модели
Однажды мальчик использовал на проекте мутабельные модели... И у него отвалилась жопа.
Были времена еще первых версий языка Dart и веб-фреймверка Polymer v0.5. И было задание написать программу для редактирования и просмотра всяких дашбордов. Дашборды состояли из виджетов, были группы, было миллион пропертей и внутренний язык программирования.
Когда дело дошло до экрана редактора, нужно было, чтобы изменения параметров виджета мгновененько отображались в центральной части экрана.
Был там такой интерфейс
Сначала было все прекрасно. Немного поработал, чтобы не было бесконечного цикла обновлений между вьшкой панели свойств виджета и компонентом, отображающим его на экране.
Лежу и жмурюсь, все так мгновенно меняется, а мне и делать не сильно чего надо было. Красота!
Но вот Google Chome сказал: "А ну ка все обновили Shadow Dom, а то ваши сайты работать не будут." То есть, надо было перебираться на новые версии своих фреймверков, так как спецификация WebComponents опять изменилась.
Полимер вдруг умер, а пакет observable обновился, потеряв обратную совместимость.
И к черту полимер, есть и другие фреймверки. Но вот только этот
За несколько месяцев аккуратного рефакторинга я так и не смог перенести весь проект на новый фреймверк, осталось несколько очень толстых компонентов. Этот перенос отменили, я нашел всякие хаки и устранил несовместимости с современными браузерами. И по сей день этот монстр так и поддерживается на очень старой базе кода и древних зависимостях.
О чем это я. Никогда, никогда не используйте никакие сторонние библиотеки в слоях бизнес-логики и бизнес-моделей. И никогда не делайте модели прослушиваемыми. Пусть будут иммутабельными.
Однажды мальчик использовал на проекте мутабельные модели... И у него отвалилась жопа.
Были времена еще первых версий языка Dart и веб-фреймверка Polymer v0.5. И было задание написать программу для редактирования и просмотра всяких дашбордов. Дашборды состояли из виджетов, были группы, было миллион пропертей и внутренний язык программирования.
Когда дело дошло до экрана редактора, нужно было, чтобы изменения параметров виджета мгновененько отображались в центральной части экрана.
Был там такой интерфейс
Observable, с помощью которого дарт мог заполнять HTML шаблоны данными. И вот пришла идея все-все данные этого дашборда хранить как observable-классы.Сначала было все прекрасно. Немного поработал, чтобы не было бесконечного цикла обновлений между вьшкой панели свойств виджета и компонентом, отображающим его на экране.
Лежу и жмурюсь, все так мгновенно меняется, а мне и делать не сильно чего надо было. Красота!
Но вот Google Chome сказал: "А ну ка все обновили Shadow Dom, а то ваши сайты работать не будут." То есть, надо было перебираться на новые версии своих фреймверков, так как спецификация WebComponents опять изменилась.
Полимер вдруг умер, а пакет observable обновился, потеряв обратную совместимость.
И к черту полимер, есть и другие фреймверки. Но вот только этот
Observable насколько густо пропитал проект, что наверное, в нем увязло более 80% всех классов.За несколько месяцев аккуратного рефакторинга я так и не смог перенести весь проект на новый фреймверк, осталось несколько очень толстых компонентов. Этот перенос отменили, я нашел всякие хаки и устранил несовместимости с современными браузерами. И по сей день этот монстр так и поддерживается на очень старой базе кода и древних зависимостях.
О чем это я. Никогда, никогда не используйте никакие сторонние библиотеки в слоях бизнес-логики и бизнес-моделей. И никогда не делайте модели прослушиваемыми. Пусть будут иммутабельными.
Use cases и два типа API
В клиент-серверных приложениях могу выделить два вида серверного API: CRUD и методы.
CRUD
В первом варианте модели хранятся по таблицам, и из front-end мы обращаемся к методам типа create, update, delete, read, readAll и так далее. То есть, у data source для каждого типа данных есть такие методы.
В таком варианте use cases принимают на себя много обязанностей:
- Разделяют сложные модели на более простые data objects (если нужно).
- Обращаются к соответствующим data sources по каждому типу таблиц в отдельности и принимают решения при обработке ошибок.
- Собирают результаты из разных data sources и формируют новые модели.
- Обновляют репозитории.
Методы сервера
Во втором варианте при хорошем серверном API, наверное, получится так, что имена методов будут совпадать с именами use cases.
Тогда выходит, что и у data source имена методов тоже будут такими, как имена методов API. У data sources все так же мало обязанностей, как и в варианте с CRUD: принять data objects, вызвать метод сервера, отдать data objects.
А uses cases становятся более "прозрачными". Возникает вопрос: а нужны ли они нам?
И вот что думаю: в них просто становится чуть меньше проверок, а обязанности они сохраняют прежние:
- Наверняка все еще придется иногда делать разделение одной модели на несколько более мелких (серверное api может оказаться не безупречным).
- Все еще нужно самостоятельно собирать новые модели из data objects.
- Все еще нужно принимать решения об ошибках обращений к серверу (пользователь прав не имеет, нет сущности с таким id, и т.д.).
- Все еще нужно обновлять репозитории.
В клиент-серверных приложениях могу выделить два вида серверного API: CRUD и методы.
CRUD
В первом варианте модели хранятся по таблицам, и из front-end мы обращаемся к методам типа create, update, delete, read, readAll и так далее. То есть, у data source для каждого типа данных есть такие методы.
В таком варианте use cases принимают на себя много обязанностей:
- Разделяют сложные модели на более простые data objects (если нужно).
- Обращаются к соответствующим data sources по каждому типу таблиц в отдельности и принимают решения при обработке ошибок.
- Собирают результаты из разных data sources и формируют новые модели.
- Обновляют репозитории.
Методы сервера
Во втором варианте при хорошем серверном API, наверное, получится так, что имена методов будут совпадать с именами use cases.
Тогда выходит, что и у data source имена методов тоже будут такими, как имена методов API. У data sources все так же мало обязанностей, как и в варианте с CRUD: принять data objects, вызвать метод сервера, отдать data objects.
А uses cases становятся более "прозрачными". Возникает вопрос: а нужны ли они нам?
И вот что думаю: в них просто становится чуть меньше проверок, а обязанности они сохраняют прежние:
- Наверняка все еще придется иногда делать разделение одной модели на несколько более мелких (серверное api может оказаться не безупречным).
- Все еще нужно самостоятельно собирать новые модели из data objects.
- Все еще нужно принимать решения об ошибках обращений к серверу (пользователь прав не имеет, нет сущности с таким id, и т.д.).
- Все еще нужно обновлять репозитории.
