Cardoteka — техническая начинка и аналитика решений типобезопасной SP [кто любит вдаваться]

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Цикл выпусков:

  • Я сделал Cardoteka и вот как её использовать [кто любит черпать] / Хабр

  • Cardoteka — техническая начинка и аналитика решений типобезопасной SP [кто любит вдаваться] (>Вы здесь<)

Содержание

  • Предпосылки создания

  • Концепция

  • Стадии реализации

  • Реализация

    • Как из SP синглтона сделать экземпляры

    • Разные экземпляры кардотеки — разное пространство имён

    • Как сделать типизированный доступ

    • Определяем место, где хранятся ключи и значения по умолчанию

    • Как сохранить null, если SP это не поддерживает?

    • Сохраняем комплексные объекты

    • Ассертные проверки

    • Реализация Watcher

      • Рефакторим старый код и используем прослушку

  • Архитектура в блок-схемах

  • Минимальная версия Dart SDK

  • Подходы к реализации конечного АПИ

  • Тестирование

  • Песочница реактивных подходов

  • Последствия

Предпосылки создания

Создание приложения для отслеживания погодных условий было весьма увлекательным занятием. И те или иные сложности, вставлявшие палки в колёса очередного только что мной придуманного велосипеда, были кстати: так зарождался первый практический опыт. Весьма быстро я понял, что использовать просто так shared_preferences не получится: нужно подумать о том, где хранить ключи и значения по умолчанию, как обновлять состояния при обновлении значений и почему не получается избавиться от бойлерплейта. Проблемы были решены, написана добротная статья на этот счёт

и где-то там зародилась идея всё упростить и улучшить. Вот фраза:

Но, спешу вас обрадовать! Господа, у меня есть отличное решение 2, 3, 4 и 5 проблемы (частично, потому что есть некоторые интересности с типизацией. Я осветил данный вопрос более подробно на Stackoverflow здесь). Я не могу и не хочу на данный момент раскрывать всех карт по поводу реализации, но упорно разрабатываю пакет, призванный помочь решить данные проблемы и не только :) Спойлеры примерно такие: удобное хранение ключей и значений | слушатель изменений | несвязанные "базы" ключей | конвертеры сложных объектов. Руки чешутся и горят опубликовать статью по "благоприятному" использованию, но сейчас готовлю приложение в production, основанное полностью на вышеуказанном пакете, и пишу тесты, которые очень необходимы ;)

25 мар 2023 в 15:15

Забавно и страшно, сколько прошло времени. Оставим мюсли на закуску для 3-ей части и перейдём к обзору реальных проблем.

  1. Действительно не ясно, как удобно мы можем хранить ключи. А если думаем о том, что хотим избавить наш код от работы с null, то и подавно.

    • Допустим, можно вполне удобно собрать всё под одним крылом в классе со статическими полями вида themeModeKey и themeModeDefaultValue. Так я и сделал в приложении погоды.

    • В одарённом порядке можно просто разбросать всё по коду. Вариант так себе, особенно когда проект нужно поддерживать в дальнейшем.

    • В общем и целом это не выглядит как большая проблема. Однако:

      • имена, к которым мы вынуждены добавлять префиксы — бойлерплейт

      • между парой ключ-деф_значение фактически нет связи —> ошибки в использовании

      • при копировании пар можно ошибиться в значении ключа

  2. Сохранять сложные объекты — сложно. Вы можете сказать, что SP не для этого, а мой ответ — класс из трёх полей, который мы не хотим дробить по ключам. А теперь представьте, что это список из таких объектов... Дорогой дневник, мне не передать всю боль, что вынес я, когда занимался этими преобразованиями. Факт: нам может потребоваться возможность сохранить объект в SP. И хочется сделать это простой операцией.

    • нам нужен отдельный метод для преобразования В и метод для преобразования ИЗ. Конечно мы воспользуемся json_serializable и также jsonEncode/jsonDecode. Но это не спасёт от бойлерплейта, а также ошибок в коде при неправильном преобразовании

    • и это всё при условии, что архитектура выстроена таким образом, что всякий может пользоваться этими методами. А можно всё делать без методов и дублировать на каждом шагу этот код... То есть мы уже говорим про классы-мапперы. Как я попал в мир java?

  3. Для каждого типа данных — свой метод получения и сохранения. Так устроена dart-овая часть SP. И мне показалось это слишком болезненным, ведь была нужда использовать интерфейсы и отдать их бизнес-логике. Это полезно для "смокования" и тестирования бизнес-логики в дальнейшем, да и просто для упрощения кода и отделения слоёв.

    • но для этого нет ни-ка-кой возможности

    • борьбу с этим я видел такой — для каждой бизнес-логики свой собственный интерфейс для доступа к хранилищу. С собственными методами сохранения/получения и даже методом удаления, если он нужен. Но что это за кисель на ровном месте?

    • к тому же единственные get и set очень упростили бы код, вместо зоопарка методов.

  4. Обновление состояния и обновление данных. Необходимость выглядит так: в тот момент, когда пользователь взаимодействует с приложением, например, указывает номер стартовой страницы, возникает новое значение. Сначала мы обновляем состояние с новым значением, а затем сохраняем это значение в хранилище.

    • то есть всякий раз делаем две одинаковых операции. А ещё я скажу, что при старте приложения (или при задействовании модуля) мы должны взять сохранённое значение из хранилища и присвоить его в состояние. А это уже 4 условных операции и ещё больше дублирования кода.

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

    • *я согласен с тем фактом, это может вылиться в сильное нарушение принципов границ слоёв. Но для true|false-переключателей хочется иметь лёгкий способ быстрого изменения и реактивности.

  5. Проблема при riverpod-ной (и прочей) разработке — как синхронно инициализировать SP? Ведь если сделать это в условном FutureProvider, то устанешь потом выдумывать костыли с доступом в синхронном коде.

    • решение такое: использовать обычный Provider и вернуть в нём throw UnimplementedError. А в main асинхронно инициализировать SP и сделать переопределение провайдера с новым значением в ProviderScope.overrides

    • сделать FutureProvider и инициализировать наш SP внутри него, а затем применить недавно появившийся геттер requireValue. Вариант неплох, признаю, однако для всех участников он строится на доверии "Отвечаю, там есть значение, клянусь табуреткой".

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

  6. Вытекающий факт: добавление новой функциональности по типу "выбор_значения—сохранение—обновление_состояния" оказалось очень утомительным занятием, даже при использовании пакета riverpod. И сильно подвержено ошибкам копипаста.

Шести пунктов показалось мне достаточным, чтобы увенчать попытку отдельным проектом.

Концепция

Избушка продукта стоит на двух ногах: решение вышеуказанных проблем и удобный апи взаимодействия. Вот ряд условий:

  • Основано на пакете shared_preferences и используется как обёртка. "Сладко" дополняет взаимодействие, а не изменяет поведение. Доступ к оригинальным функциям. Минимум прочих зависимостей (ноль).

  • Гибкое апи и широкие возможности при поддержке правила "если что-то можно сделать, то только так".

  • Без генерации кода.

  • Инкапсуляция сложности работы с комплексными данными: их лёгкое преобразование и минимум бойлерплейта.

  • get|set|remove для обычного использования и CRUD-модель взаимодействия для нуждающихся.

  • Функциональность прослушки для реактивной реализации поведения.

Закрепляю тот факт, что всё это должно действовать совокупно и единовременно.

Стадии реализации

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

Есть несколько моментов, на которые мы обратим внимание.

Для полноценного production понадобилось целых 6 итераций проектирования. Именно на них я буду ссылаться всякий раз. Ниже представлены временны́е итерации со ссылками на состояние репозитория к концу итерации:

  1. наивная реализация — 4-9 ноябрь 22г (код написан в голове с октября)

    • заложены основы пакета: ядро работы с SP называемое RDatabase, карточка RKey, конвертер RConverter, миксин для CRUD-операций, Watcher для прослушки значений

    • очень интересно выглядит Watcher: для хранения и обновления состояния используется StateNotifier

    • коллекция примеров в папке example

    • первая документация в файле readme.md

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

  2. 3-10 марта 23г

    • появляются специальные проверки (в виде ассертов) для проверки корректности предоставленных карточек и конвертеров, инициализации

    • переименование хранилища в CardDb

    • появляется getOrNull и setOrNull, а также класс конфигурации CardConfig

    • интерфейс прослушки изменяется (в том числе отказ от пакета state_notifier) и появляется знакомый метод notify и attach

    • появляются первый тесты, направленные на методы get и set и на работу с riverpod

  3. 8-17апр

    • сильный структурный рефакторинг папок

    • значительное улучшение проверок конфигурации

    • значительное документирование кода

    • наконец-то теперь это любимая Cardoteka

    • появляется проксирование к статическим методам SP

  4. 15-24мая

    • первое графическое отображение структуры проекта с помощью mermaid схем

    • появление набора различных конвертеров для обычных классов и коллекций

  5. 25июля-1авг

    • написание тестов для конвертеров и ассертов, и ядра

    • ощущение, что проект уже в хорошей кондиции для релиза

  6. публичный релиз — 25ноя-22дек 23г

    • полное осмысление и доработка всего кода, всех частей

    • создание мини-примеров в example

    • много тестов, документации кода

    • совершенно новый и обдуманный readme.md

    • появляется идея довести Quiz Prize до ума в качестве мини-амбассадора

    • подготовка первой публикации "об использовании"

В графическом представлении этапы выглядят как одинокие айсберги:

И будет очень удобно, если вместо шести этапов мы будем рассматривать всего лишь 3 основных стадии:

  1. Наивная реализация (закладывание основы ядра — левый айсберг)

  2. Совершенствование (удобство апи, корректная работа — кучка айсбержков в середине)

  3. Оттачивание (подготовка к релизу — правый айсберг)

Реализация

Удивительно, как много времени понадобилось для создания казалось бы простейшей обёртки. Но стоит учитывать подбор удобного апи для взаимодействия, обдумывание dart-концепций, желание выпустить полноценный законченный продукт и природную лень человека. Всё это купно очень страшные вещи, оттягивающие релиз перфекциониста (насколько это слово применимо) до невнятных сроков.

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

Как из SP синглтона сделать экземпляры

Пожалуй это не такая уж сложная задачка, достаточно поместить наш синглтон в поле класса, экземпляры которого можно создавать. Однако, какой в этом смысл? Более удобная инициализация и пространство имён. Получаем какой-то код: (тут и далее я буду удалять незначимые детали из кода, дабы убрать отвлекающий шум)

Моя особая любовь к букве R не давала покоя: у нас были ключи RKey, хранилище RDatabase и конвертеры RConverters.., не то, чтобы всё это было категорически необходимо в создании пакета, но если уж начал брендировать, то к делу надо подходить серьёзно. Это я отвлёкся; обратите внимание на способ инициализации. Да, без слёз не взглянешь, но имейте ввиду — эти слёзы превратились в крепкий опыт, который в свою очередь стал частью меня. И мне не стыдно. Метод initSync действительно ничего не делает(1), поскольку он возвращает ровно тот же экземпляр, метод которого вызывается. А вот асинхронный init делает что-то важное: принимает нужные аргументы (2) и инициализирует саму SP, присваивая экземплярному(3) полю ссылку.

Но в середине айсбергов я постепенно понимаю, что подход нужен иной. Например, вот такой:

RDatabase преобразовался в Cardoteka, всюду убран ненужный префикс R, в late теперь нет нужды.

Если что-то (1) не делает ничего полезного — от этого можно избавиться. Важные аргументы (2) стоит собрать в класс-конфигуратор и передать прямо в конструктор Cardoteka — местоположение зависимостей в шапке конструктора показалось мне хорошей идей. А экземпляр класса CardotekaConfig легко перебрасывать ассертным поверочным функциям, а также удобно добавлять в него новые поля.

Теперь _prefs (3) является статичным полем, собственно как и единственный метод инициализации init(). Это весьма логично, потому что инициализацию нужно провести один раз, а экземплярам кардотеки нет необходимости иметь метод init().

Есть ещё маленькая хитрость: карточки лежат в UnmodifiableListView, чтобы никто не смог их изменить извне (да и изнутри тоже).

Сейчас я вижу здесь одно желаемое упрощение: разрешить создание экземпляров Cardoteka. Это упростит жизнь тем пользователям, которым не нужны дополнительные функции (прослушка, доступ к SP и т.д.). Issue добавил.

Разные экземпляры кардотеки — разное пространство имён

Это просто необходимо, иначе нет ровным счётом никакого смысла в создании множества экземпляров (ну может быть только ради разделения интерфейсов). В общем, идентичность каждого экземпляра складывается из двух вещей:

  • имя в конфигурационном классе

  • префикс в ключе (если ключи основаны на Enum.name)

Вот как выглядит конфигурационный класс:

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

Источник: https://habr.com/ru/articles/801089/


Интересные статьи

Интересные статьи

Помните, как вы были студентами, и готовились к экзаменам по ночам?Предлагаю вашему вниманию простую шпаргалку по SQL с теорией и практикой, которой вы сможете воспользоваться в любое время.Изучите те...
Привет! 28 января 2022 в прошел питчинг проектов-участников третьего набора Privacy Accelerator в рамках технического трека ежегодной конференции Privacy Day, посвященной теме защиты персональных данн...
Зачем придумали нотации (прим. система условных обозначений, принятая в какой-либо области)? Все просто, они помогают предотвратить много споров и конфликтов между людьми. Давайте посмотрим как BPMN (...
Marketing Mix Modeling - это метод, который позволяет проанализировать вашу маркетинговую стратегию при использовании вами нескольких рекламных каналов. Цель МММ - количественно оценить влияние отдель...
Его система фильтрует световое загрязнение и улучшает цвета фотографий при помощи Jetson Nano Днём Алан Пайю руководит комиссией, отвечающей за качество воды в Бургундии, районе Франции. Ког...