Введение
В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру 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, и т.д.).
- Все еще нужно обновлять репозитории.
Что такое модель
Что я в этом канале подразумеваю под словом "модель"? Коротко говоря, это классы с данными в слоях Entities и Use cases. Теперь чуть подробнее.
Практически любая программа обрабатывает данные. Даже если задача программы — отобразить информацию на экране, ее сначала нужно откуда-то взять, а потом — сохранить в памяти.
Модель — это как раз класс для хранения данных, с которыми работает программа.
Например, у нас есть Task Tracker. Он работает с данными Issue, User, Project или типа того. Вот это и будут те штуки, которые будут представляться в программе моделями.
Помимо моделей у нас программе есть и другие классы, хранящие информацию: хэш-таблицы, кэши и т.д. То есть, не каждый класс с данными — это модель.
Модели — штуки очень важные в программе. У них я вижу определенную ответственность, и потому предъявляю определенные требования к тому, как они написаны.
Ответственность моделей
Модели должны предоставлять нам правдивые данные.
Ведь они представляют собой что-то реальное, что хранится в базах данных (или файлах). Это имя-фамилия человека, id, или может, это упорядоченный список виджетов, который мы показываем в дашборде.
Обычно существуют какие-то правила, которые описывают, правильная модель или нет. Например, наверняка поле id не должно быть пустым или равным null. У задачи обязательно должны быть указан ее автор и время создания.
Класс модели должен эти правила реализовывать. Например, кидать ошибки из конструктора класса, когда входящие данные содержат ошибки. Или не позволять вносить изменения в себя, если они нарушают бизнес-правила.
Требования к реализации моделей
И вот как я вижу реализацию моделей, чтобы она могла нести свою ответственность.
Модели должны быть иммутабельными. Полностью. Даже внутренние коллекции и то, что коллекции содержат. Таким образом мы гарантируем, что никто и никогда не нарушит правильность данных.
Модели не должны содержать методов для изменения ее данных. Если нам нужно выполнить операцию над данными, для этого стоит выделить отдельный use case. Модель не должна знать, какие там операции мы можем захотеть над ней произвести.
Модель не должна зависеть ни от каких фреймверков. Хотя иногда и жутко хочется что-нибудь добавить. Я вот сдался, и разрешил себе использовать пакет Equatable для моделей с коллекциями (хотя этого можно избежать, если просто написать себе live templates).
Модель не должна знать, как ее было бы удобно отобразить на экране или хранить в базе данных. Поэтому никаких fromJson и toJson, никакого важного синтаксиса в методе toString.
Модель должно быть нельзя создать с неправильными данными. Значит, в конструкторе необходимо сделать все нужные для этого проверки и кидать исключения.
Если у вас другое мнение об этом всем, дайте мне знать. Мне правда интересно.
#модель
Что я в этом канале подразумеваю под словом "модель"? Коротко говоря, это классы с данными в слоях Entities и Use cases. Теперь чуть подробнее.
Практически любая программа обрабатывает данные. Даже если задача программы — отобразить информацию на экране, ее сначала нужно откуда-то взять, а потом — сохранить в памяти.
Модель — это как раз класс для хранения данных, с которыми работает программа.
Например, у нас есть Task Tracker. Он работает с данными Issue, User, Project или типа того. Вот это и будут те штуки, которые будут представляться в программе моделями.
Помимо моделей у нас программе есть и другие классы, хранящие информацию: хэш-таблицы, кэши и т.д. То есть, не каждый класс с данными — это модель.
Модели — штуки очень важные в программе. У них я вижу определенную ответственность, и потому предъявляю определенные требования к тому, как они написаны.
Ответственность моделей
Модели должны предоставлять нам правдивые данные.
Ведь они представляют собой что-то реальное, что хранится в базах данных (или файлах). Это имя-фамилия человека, id, или может, это упорядоченный список виджетов, который мы показываем в дашборде.
Обычно существуют какие-то правила, которые описывают, правильная модель или нет. Например, наверняка поле id не должно быть пустым или равным null. У задачи обязательно должны быть указан ее автор и время создания.
Класс модели должен эти правила реализовывать. Например, кидать ошибки из конструктора класса, когда входящие данные содержат ошибки. Или не позволять вносить изменения в себя, если они нарушают бизнес-правила.
Требования к реализации моделей
И вот как я вижу реализацию моделей, чтобы она могла нести свою ответственность.
Модели должны быть иммутабельными. Полностью. Даже внутренние коллекции и то, что коллекции содержат. Таким образом мы гарантируем, что никто и никогда не нарушит правильность данных.
Модели не должны содержать методов для изменения ее данных. Если нам нужно выполнить операцию над данными, для этого стоит выделить отдельный use case. Модель не должна знать, какие там операции мы можем захотеть над ней произвести.
Модель не должна зависеть ни от каких фреймверков. Хотя иногда и жутко хочется что-нибудь добавить. Я вот сдался, и разрешил себе использовать пакет Equatable для моделей с коллекциями (хотя этого можно избежать, если просто написать себе live templates).
Модель не должна знать, как ее было бы удобно отобразить на экране или хранить в базе данных. Поэтому никаких fromJson и toJson, никакого важного синтаксиса в методе toString.
Модель должно быть нельзя создать с неправильными данными. Значит, в конструкторе необходимо сделать все нужные для этого проверки и кидать исключения.
Если у вас другое мнение об этом всем, дайте мне знать. Мне правда интересно.
#модель
Немного об именовании локальных переменных
Есть задача: есть список элементов на экране, которые нужно раскрасить в разные цвета в зависимости от их внутреннего содержимого. Плюс некоторые элементы должны быть особого цвета для обозначения, что они выделены пользователем.
Чтобы это сделать, программист написал функцию, которая для каждого элемента сопоставляет нужный цвет. Она берет 2 параметра: список элементов и цвет для обозначения выделенных элементов. И выглядит это так:
Посмотрите на переменную
Лучше назвать эту переменную как
* Переменная сразу описывает, какой смысл она несет в себе. Не важно, откуда она получает значение и какое оно.
* Вторая строчка становится чище от дополнительной логики: ведь именно на ней мы говорим, что цвет выбранных элементов должен быть
* Вызов функции выглядит очень читаемо: теперь мы понимаем, что за цвет мы передаем в функцию и что он обозначает.
Пишите код легче и понятнее, и мир станет лучше.
#простокод
Есть задача: есть список элементов на экране, которые нужно раскрасить в разные цвета в зависимости от их внутреннего содержимого. Плюс некоторые элементы должны быть особого цвета для обозначения, что они выделены пользователем.
Чтобы это сделать, программист написал функцию, которая для каждого элемента сопоставляет нужный цвет. Она берет 2 параметра: список элементов и цвет для обозначения выделенных элементов. И выглядит это так:
Color accentColor = Theme.of(context).accentColor;
Map<Item, Color> itemsWithColors = _mapItemsWithColors(widget.items, accentColor);
Посмотрите на переменную
accentColor: почему она так называется? Какой смысл в том, что это именно accent цвет? Ведь мы можем захотеть взять из темы другой цвет.Лучше назвать эту переменную как
selectedColor. Таким образом мы делаем сразу несколько полезных вещей:* Переменная сразу описывает, какой смысл она несет в себе. Не важно, откуда она получает значение и какое оно.
* Вторая строчка становится чище от дополнительной логики: ведь именно на ней мы говорим, что цвет выбранных элементов должен быть
accentColor. Но задача обладать такой логикой должна находится на плечах переменной.* Вызов функции выглядит очень читаемо: теперь мы понимаем, что за цвет мы передаем в функцию и что он обозначает.
Color selectedColor = Theme.of(context).accentColor;
Map<Item, Color> itemsWithColors = _mapItemsWithColors(widget.items, selectedColor);
Пишите код легче и понятнее, и мир станет лучше.
#простокод
Цепочки против деревьев
Вот есть операция. На вход получаем какие-то данные, их нужно подготовить к отправке на сервер, отправить, получить результат, распарсить и вернуть какие-то новые объекты. Обычное дело.
И есть такой вариант, когда программист разбивает операцию следующим образом:
И получается, что выполнение некоторой задачи разнесено по куче методов, иногда по разным классам и файлам. Из этого вытекает первый минус: не знакомому с этим местом программисту сложно построить итоговый алгоритм выполнения всей операции.
Второй минус: не понятна ответственность функций. Каждая функция возвращает некий итоговый вариант, о качестве которого она понятия не имеет, так как конечное решение делегировано следующей функции.
Также в такой системе сложно отслеживать ошибки, сложно разделять работу между классами, сложно протестировать код, сложнее разбить задачу на подзадачи.
В общем, такие цепочки писать не стоит. А лучше стоит разделять вфункции в деревообразную структуру, где первая функция наглядно показывает общий алгоритм работы:
Мы можем вытащить сам запрос к серверу в
Что-то не ясно, не согласны? Задавайте вопросы, пишите свое мнение, мне интересно.
Вот есть операция. На вход получаем какие-то данные, их нужно подготовить к отправке на сервер, отправить, получить результат, распарсить и вернуть какие-то новые объекты. Обычное дело.
И есть такой вариант, когда программист разбивает операцию следующим образом:
Future<bool> doOperation(param1, param2) async {
final chekRes = _doChecks(param1);
if (chekRes) return false;
return await _doOperationA(param1, param2);
}
Future<bool> _doOperationA(param1, param2) async {
final endpoint = _getEndpoint(param1);
final headers = _getHeaders(param2);
final res = await _doOperationB(endPoint, headers);
return _isResGood(res);
}
Future<Smth> _doOperationB(url, headers) async {
final res = await http.get(url, headers: headers);
return _convertRes();
}
И получается, что выполнение некоторой задачи разнесено по куче методов, иногда по разным классам и файлам. Из этого вытекает первый минус: не знакомому с этим местом программисту сложно построить итоговый алгоритм выполнения всей операции.
Второй минус: не понятна ответственность функций. Каждая функция возвращает некий итоговый вариант, о качестве которого она понятия не имеет, так как конечное решение делегировано следующей функции.
Также в такой системе сложно отслеживать ошибки, сложно разделять работу между классами, сложно протестировать код, сложнее разбить задачу на подзадачи.
В общем, такие цепочки писать не стоит. А лучше стоит разделять вфункции в деревообразную структуру, где первая функция наглядно показывает общий алгоритм работы:
Future<bool> doOperation(param1, param2) async {
final chekRes = _doChecks(param1);
if (chekRes) return false;
final endpoint = _getEndpoint(param1);
final headers = _getHeaders(param2);
final res = _doCall(endpoint, headers);
final data = _convertRes();
return _isResGood(data);
}
Мы можем вытащить сам запрос к серверу в
_doCall, и это не будет проблемой: ведь мы не вешаем на плечи этой функции ответственность за следующий шаг.Что-то не ясно, не согласны? Задавайте вопросы, пишите свое мнение, мне интересно.
Не используйте toString для преобразования типов
Если вы хотите на экране отобразить какой-то текст на основе данных, не надо описывать это в методах
Почему? Вот несколько причин.
Ответственность
Преобразование данных в строки — это ответственность презентатора, а вовсе не модели данных. В разных случаях по разным причинам мы можем захотеть отобразить одни и те же данные по разному.
Число можно округлить до 3 значащих цифр, или до целых, или до двух знаков после точки. Если число
В итоге, модель не должна знать, кто и как хочет преобразовывать ее в строку, это не ее дело.
Поиск использования
Поиск использования метода
Как надо делать
Напишите отдельные функции или классы для конвертации данных в нужный вид и используйте их в презентаторе.
В крайнем случае, если вы твердо намерены оставить этот метод в классе модели, дайте ему уникальное имя, опишите в doc comment, почему именно такой вариант и кто его использует.
Когда использовать toString
Для отладки. Когда хочешь написать что-нибудь в консоль функцией
В своих классах я обычно возвращаю строку типа
Если вы хотите на экране отобразить какой-то текст на основе данных, не надо описывать это в методах
toString. Это касается классов данных, моделей, вью-моделей и вообще практически всего остального.Почему? Вот несколько причин.
Ответственность
Преобразование данных в строки — это ответственность презентатора, а вовсе не модели данных. В разных случаях по разным причинам мы можем захотеть отобразить одни и те же данные по разному.
Число можно округлить до 3 значащих цифр, или до целых, или до двух знаков после точки. Если число
NaN, мы можем захотеть написать "..." или "In error". Или в одних случаях нам нужно добавить единицы измерения, а в других — не нужно.В итоге, модель не должна знать, кто и как хочет преобразовывать ее в строку, это не ее дело.
Поиск использования
Поиск использования метода
toString вам не поможет, это базовый метод класса Object. Так что вы не сможете легко найти, кто и почему использует именно такой вариант конвертации в строку.Как надо делать
Напишите отдельные функции или классы для конвертации данных в нужный вид и используйте их в презентаторе.
В крайнем случае, если вы твердо намерены оставить этот метод в классе модели, дайте ему уникальное имя, опишите в doc comment, почему именно такой вариант и кто его использует.
Когда использовать toString
Для отладки. Когда хочешь написать что-нибудь в консоль функцией
print. И делать такой метод только когда возникает такая необходимость. Заранее делать — смысла нет.В своих классах я обычно возвращаю строку типа
"MyClass(val: $val)".Assert или throw в конструкторе?
Я создаю экземпляр класса и передаю в конструктор параметры. И класс предъявляет некоторые требования для их значений. Строка не должна быть пустой, а провайдер не должен быть null.
Да, у меня все еще Flutter < 2.0 на основных проектах. И проверки на
Вопрос в том, каким образом делать проверки:
Первый случай
Первый случай — класс презентатора (ну BLoC, если хотите). В него (по фен-шую) передаются dependency через конструктор. В этом случае хватит
Почему? Такого рода классы не зависят от внешних данных. Они всегда создаются в одинаковых условиях. Для проверки достаточно в отладочном режиме перетыкать все экраны приложения.
Потому что параметры презентаторов — это бизнес-логики, дата-провайдеры, фабрики и подобное. И все эти классы не зависят от данных, получаемых из вне.
Но если такой класс нуждается в передаче данных типа модель и вы используете старый флаттер — на этот случай лучше кидать исключение.
Второй случай
Класс модели создается на основе данных: примитивных значений типа строк, чисел, булевых и т.д. Прежде чем собирать модель из данных, их нужно тщательно проверить.
Я делаю это в конструкторе модели, считаю, это лучшее место.
Так как данные поступают из внешних источников, они могут быть какими угодно. Ошибки нужно обрабатывать и на продакшене обязательно. Поэтому здесь мы используем
Я создаю экземпляр класса и передаю в конструктор параметры. И класс предъявляет некоторые требования для их значений. Строка не должна быть пустой, а провайдер не должен быть null.
Да, у меня все еще Flutter < 2.0 на основных проектах. И проверки на
null — самое частое, что мне приходится делать.Вопрос в том, каким образом делать проверки:
assert или throw? И у меня есть ответ.Первый случай
Первый случай — класс презентатора (ну BLoC, если хотите). В него (по фен-шую) передаются dependency через конструктор. В этом случае хватит
assert.Почему? Такого рода классы не зависят от внешних данных. Они всегда создаются в одинаковых условиях. Для проверки достаточно в отладочном режиме перетыкать все экраны приложения.
Потому что параметры презентаторов — это бизнес-логики, дата-провайдеры, фабрики и подобное. И все эти классы не зависят от данных, получаемых из вне.
Но если такой класс нуждается в передаче данных типа модель и вы используете старый флаттер — на этот случай лучше кидать исключение.
Второй случай
Класс модели создается на основе данных: примитивных значений типа строк, чисел, булевых и т.д. Прежде чем собирать модель из данных, их нужно тщательно проверить.
Я делаю это в конструкторе модели, считаю, это лучшее место.
Так как данные поступают из внешних источников, они могут быть какими угодно. Ошибки нужно обрабатывать и на продакшене обязательно. Поэтому здесь мы используем
throw.Просто код pinned «Введение В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру front-end приложений. Также, я работаю сейчас на языке Dart, в проектах использую Flutter. Поэтому часть постов будет более актуальна именно…»
3 уровня безопасности моделей
Сегодня расскажу про классы данных: модели и вью-модели.
За свою долгую практику я выработал три уровня безопасности таких классов:
1. иммутабельность,
2. исключения в конструкторе: отсекаем неправильные состояния,
3. разные интерфейсы для разных состояний.
Объясню каждый уровень.
Иммутабельность
В первую очередь классы должны быть неизменяемы, тогда в рантайме не будет неожиданных ошибок, не нужно будет проектировать параллельные асинхронные процессы, когда в одном месте программы изменили модель и ui, а в другом месте этого не произошло.
Например, удалили из списка пару задач, а на экране счетчик не обновился, и показывает уже не актуальное количество. Или в выпадающем списке все еще лежат штуки, которые уже удалили.
Не надо будет строить кучу стримов про обновление каждого поля данных или кучу проверок, что обновилось, а что — нет. Стримов и ошибок будет гораздо меньше.
Когда такие ошибки видны на одном экране, их, конечно, легко находить. Если они разнесены по приложению, или вообще не отображаются визуально — проблема может вылезти уже на релизе.
Что делаем: все поля
Что получили: состояния не меняются, проще код, меньше ошибок совместного обращения к данным.
Сегодня расскажу про классы данных: модели и вью-модели.
За свою долгую практику я выработал три уровня безопасности таких классов:
1. иммутабельность,
2. исключения в конструкторе: отсекаем неправильные состояния,
3. разные интерфейсы для разных состояний.
Объясню каждый уровень.
Иммутабельность
В первую очередь классы должны быть неизменяемы, тогда в рантайме не будет неожиданных ошибок, не нужно будет проектировать параллельные асинхронные процессы, когда в одном месте программы изменили модель и ui, а в другом месте этого не произошло.
Например, удалили из списка пару задач, а на экране счетчик не обновился, и показывает уже не актуальное количество. Или в выпадающем списке все еще лежат штуки, которые уже удалили.
Не надо будет строить кучу стримов про обновление каждого поля данных или кучу проверок, что обновилось, а что — нет. Стримов и ошибок будет гораздо меньше.
Когда такие ошибки видны на одном экране, их, конечно, легко находить. Если они разнесены по приложению, или вообще не отображаются визуально — проблема может вылезти уже на релизе.
Что делаем: все поля
final, избегаем List, Map и прочие изменяемые типы, указываем @immutable. И никакого наследования или расширения (кроме редких и действительно нужных случаев).Что получили: состояния не меняются, проще код, меньше ошибок совместного обращения к данным.
