Введение
В этом канале хочу описать ряд мыслей, которые сформировались по ходу работы. В основном они про архитектуру 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.
Модель должно быть нельзя создать с неправильными данными. Значит, в конструкторе необходимо сделать все нужные для этого проверки и кидать исключения.
Если у вас другое мнение об этом всем, дайте мне знать. Мне правда интересно.
#модель
