что такое неконсистентность данных
Паттерн «сага» как способ обеспечения консистентности данных
Всем привет. Уже сейчас в OTUS открывает набор в новую группу курса «Highload Architect». В связи с этим я продолжаю серию своих публикаций, написанных специально для этого курса, а также приглашаю вас на свой бесплатный демо урок по теме: «Индексы в MySQL: best practices и подводные камни». Записаться на вебинар можно тут.
Введение
Как известно, переход от монолита к микросервисной архитектуре вызывает ряд сложностей, связанных как с технической частью проекта, так и с человеческим фактором. Одна из самых сложных технических проблем вызывает обеспечение согласованности в распределенной системе.
В прошлый раз мы обсудили причины возникновения проблем с согласованностью в микросервисной архитектуре, оптимистичный подход к обеспечению согласованности и обеспечение согласованности с применением двухфазного коммита.
Паттерн «Сага»
Сага — это механизм, обеспечивающий согласованность данных в микросервисной архитектуре без применения распределенных транзакций.
Для каждой системной команды, которой надо обновлять данные в нескольких сервисах, создается некоторая сага. Сага представляет из себя некоторый «чек-лист», состоящий из последовательных локальных ACID-транзакций, каждая из которых обновляет данные в одном сервисе. Для обработки сбоев применяется компенсирующая транзакция. Такие транзакции выполняются в случае сбоя на всех сервисах, на которых локальные транзакции выполнились успешно.
Типов транзакций в саге несколько, целых четыре:
В случае с хореографической саги выделенный оркестратор отсутствует. На примере сервиса заказов и пользователей она может выглядеть так: сервис заказов получает запрос и создает заказ в состоянии PENDING, а затем публикует событие «Заказ создан». Обработчик событий в сервисе пользователей обрабатывает данное событие, пытается зарезервировать товар и публикует результат в виде события. Сервис заказов обрабывает данное событие, подтверждая или отменяя заказ в зависимости от прочитанного результата.
Сага с оркестрацией выглядит чуть более интересно. На примере указанных выше сервисов может получиться так: сервис заказов получает запрос, создает сагу, которая создает заказ в состоянии PENDING, а затем отправляет команду резервирования товара для сервиса пользователей. Сервис пользователей пытается зарезервировать товар и отправляет ответное сообщение с указанием результата. Сага одобряет или отменяет заказ.
Паттерн «сага» позволяет приложению поддерживать согласованность данных между нескольких сервисов без использования распределенных транзакций (двухфазных коммитов) и с избежанием проблем, обсужденных в предыдущей статье. Но с другой стороны, сильно осложняется модель программирования: например, разработчик для каждой транзакции должен писать компенсирующую транзакцию, которая отменяет изменения, сделанные внутри саги ранее.
Сага позволяет добиться ACD-модели (Atomicity + Consistency + Durability в терминах ACID), но одну букву мы потеряли. Недостаток буквы I приводит к известным проблемам недостатка изолированности. К ним относятся: потерянные обновления (lost updates) — одна сага перезаписывает изменения, внесенные другой, не читая их при этом, «грязное чтение» (dirty reads) — транзакция или сага читают незавершенные обновления другой саги, нечеткое/неповторяемое чтение (fuzzy/nonrepeatable reads) — два разных этапа саги читают одни и те же данные, но получают разные результаты, потому что другая сага внесла изменения. Существует ряд паттернов, позволяющих пофиксить те или иные аномалии: семантическая блокировка, коммутативные обновления, пессимистическое представление, повторное чтение значения, файл изменений и по значению. Вопрос обеспечения изоляции остается открытым.
Еще одна интересная проблема заключается в невозможности атомарных обновления базы данных и публикации сообщения в брокер сообщений для запуска дальнейших шагов саги.
Заключение
Мы поговорили о способах организации саги с применением хореографии и оркестрации, а также о проблемах, которые влечет применения данного паттерна. Далее мы поговорим о способах исправления некоторых аномалий и транзакционной отправки сообщений в брокер сообщений.
Контроль консистентности кода в Go
Если вы считаете консистентность важной составляющей качественного кода — эта статья для вас.
Консистентность — наше что-то
Для начала определимся с тем, что мы называем «консистентностью».
Чем больше исходные коды программы выглядят так, будто их написал один человек, тем более они консистентны.
Стоит заметить, что зачастую даже один и тот же человек может менять свои предпочтения с течением времени, однако по-настоящему остро проблема консистентности встаёт в крупных проектах, где вовлечено большое количество разработчиков.
Иногда вместо слова «консистентность» используется «согласованность». В этой статье я иногда буду употреблять контекстные синонимы во избежание частой тавтологии.
Существуют разные уровни консистентности, например мы можем выделить три наиболее очевидные:
Чем ниже по списку, тем сложнее соблюдать консистентность. При этом отсутствие консистентности на уровне одного файла исходного кода выглядит наиболее отталкивающе.
Можно также спускаться от файла к уровню функции или одного statement, но это, в нашем случае, уже лишняя детализация. Ближе к концу статьи станет понятно почему.
Эквивалентные операции в Go
Как вы думаете, какой из этих двух способов создать слайс длины 100 используется большинством Go программистов?
Ни один из вариантов не является предпочтительным. В реальном коде я ни разу не видел использования ни одного из них.
Одиночный import
Импортирование единственного пакета можно выполнить двумя способами:
Выделение указателя под нулевое значение
Создание пустого слайса
Для создания пустого слайса (не путать с nil-слайсом) есть как минимум два популярных способа:
Создание пустой хеш-таблицы
Шестнадцатеричные литералы
Проверка на вхождение в диапазон
Оператор and-not
Я проводил опрос, чтобы убедиться, что здесь вообще могут быть разные предпочтения. В конце концов, если выбор в пользу &^ был бы единогласным, то это должно было бы быть проверкой линтера, а не частью выбора в вопросе согласованности кода. К моему удивлению, сторонники нашлись у обеих форм.
Литералы вещественных чисел
Есть множество способов записать вещественный литерал, но одна из самых часто встречающихся особенностей, которая может ломать консистентность даже внутри одной функции — это стиль записи целой и вещественной части (сокращённый или полный).
LABEL или label?
К сожалению, устоявшихся конвенций к именованию меток, нет. Всё, что остаётся, выбрать один из возможных способов и придерживаться ему.
Указание типа для untyped числового литерала
Перенос закрывающей скобки вызываемой функции
В случае простейших вызовов, которые умещаются на одной строке кода, проблем со скобочками быть не может. Когда по тем или иным причинам вызов функции или метода расползается на несколько строк, появляются несколько степеней свободы, например, вам нужно будет определиться, куда ставить закрывающую список аргументов скобку.
Проверка на ненулевую длину
Расположение default метки в switch
Разумных выборов два: ставить default первой или последней меткой. Остальные варианты, вроде «где-то посередине» — это работа для линтера. Проверка defaultCaseOrder из gocritic находит поможет прийти к более идиоматичному варианту, а go-consistent предложит из двух возможных вариантов тот, что сделает код более единообразным.
go-consistent
Выше мы перечислили список эквивалентных операций.
Как определить, какую из них нужно использовать? Наиболее простой вариант ответа: ту, которая имеет более высокую частоту использования в рассматриваемой части проекта (как частный случай, во всём проекте).
Программа go-consistent анализирует указанные файлы и пакеты, подсчитывая количество использования той или иной альтернативы, предлагая заменить менее частые формы на идиоматичные в рамках анализируемой части проекта, те, у которых частота максимальна.
Насколько это оптимальная стратегия пока не понятно. Эту часть алгоритма будет несложно доработать или предоставить пользователям выбирать одну из нескольких предложенных.
Возвращаясь к границам консистентности, вот как проверить каждую из них:
go-consistent устроен таким образом, что может выдать ответ даже для огромных репозиториев, где загрузить все пакеты в память единовременно довольно сложно (по крайней мере, на персональной машине без огромного количества оперативной памяти).
Другая важная особенность — это zero configuration. Запуск go-consistent без каких-либо флагов и конфигурационных файлов — это то, что работает для 99% случаев.
Предупреждение: первый запуск на проекте может выдать большое количество предупреждений. Это не значит, что код написан плохо, просто контролировать согласованность на таком микро уровне достаточно сложно и не стоит трудозатрат, если контроль выполняется исключительно вручную.
go-namecheck
Снизить консистентность кода может непоследовательное именование параметров функции или локальных переменных.
Задачу проверки консистентности имён переменных решить методами go-consistent нельзя. Здесь сложно обойтись без манифеста локальных конвенций.
go-namecheck определяет формат этого манифеста и позволяет проводить его валидацию, упрощая следование определённым в проекте нормам именования сущностей.
Выражается это правило следующий образом:
Все типы рассматриваются с игнорированием указателей. Любой уровень косвенности будет удалён, поэтому не нужно определять отдельные правила для указателей на тип и самого типа.
Файл, который описывал бы оба правила, выглядел бы так:
Предполагается, что проект начинается с пустого файла. Затем, в определённый момент, на code review, происходит запрос на переименование переменной или поля в структуре. Естественной реакцией может быть просьба закрепить эти, ранее неформальные, требования, в виде верифицируемого правила в файле конвенций именования. В следующий раз проблему можно будет находить автоматически.
Заключение
Особенности, рассмотренные выше, не являются критичными по отдельности, но влияют на общую консистентность в совокупности. Мы рассмотрели однородность кода на микро уровне, который не зависит от архитектуры или других особенностей приложения, поскольку эти аспекты проще всего валидировать с почти нулевым количеством ложных срабатываний.
Если по описаниям выше go-consistent или go-namecheck вам понравились, попробуйте запустить их на своих проектах. Обратная связь — по-настоящему ценный подарок для меня.
Важно: если у вас есть какая-то идея или дополнение, пожалуйста, скажите об этом!
Есть несколько способов:
Предупреждение: добавлять go-consistent и/или go-namecheck в CI может быть чересчур радикальным действием. Запуск раз в месяц с последующей правкой всех несоответствий может быть более удачным решением.
Баланс между общим и частным в большой компании: консистентность, переиспользование кода и поиск чётких метрик
В Яндексе я руковожу службой общих интерфейсов. О них и поговорим. О том, как трудно (но приходится) делать что-то для всех. Позволю себе аналогию: сидишь, пишешь код и захотел пить. Налил себе сразу три стакана из одной бутылки, даже от клавиатуры не отрываясь. А если, к примеру, бутылка оказалась пустой, можно не пить. И по стаканам не разливать. Но если кто-то другой попросит, то придётся идти, ставить чайник, спрашивать: чай или кофе, чёрный или с молоком, нужен ли сахар, — если нужен, то сколько, резать бутерброды… Это намного сложнее. А когда задача — разработать общие компоненты, всё становится совсем сложно. Но мы попробуем разобраться.
Яндекс — не просто большая компания. Это гигантская компания, состоящая из множества больших компаний. Когда мы хотим переиспользовать компоненты, важно понимать, сколько у нас сервисов, насколько они разные и сложные внутри.
Это не всё, что у нас есть, но представление получить можно.
Консистентность: плюсы и минусы
+ Пользовательский опыт
Пользователям удобно, когда одинаковые по семантике компоненты интерфейса выглядят и ведут себя одинаково на разных сервисах — не нужно переучиваться.
+ Экономия при разработке
Повторное использование — это экономия на дизайне, реализации и дальнейшей поддержке. Но, к сожалению, у него есть дополнительная цена.
— Время поставки до пользователей
В борьбе за консистентность на масштабе сотен сервисов один только этап сбора требований может затянуться так, что не все проекты доживут до его завершения.
— Гибкость
Приходится разбираться, как компоненты используются в других проектах, чтобы ничего не сломать и не добавить проблем коллегам. Это замедляет и осложняет работу.
— Продуктовые метрики
Бывает, что на части сервисов компонент работает, но если внедрить его в остальные, возникают проблемы. В отдельных случаях компонент может навредить продуктовым метрикам.
Получается, что единственный способ эффективно использовать консистентность в интерфейсах в большой компании — соблюдать баланс.
Часть проблем консистентности — это баги. Их находят, исправляют и делают так, чтобы одинаковые сущности вели себя одинаково.
Есть и обратная сторона, когда неконсистентность оставляют осознанно. Например, если требуется сделать что-то быстро или что-то уникальное. Могут быть и другие причины, но важно понимать — здесь отклонение от общего работает лучше, и мы готовы с этим жить.
Загадочная метрика консистентности
Ключевые сервисы Яндекса обвешаны метриками сверху донизу, но нам пока не удалось найти способ точно посчитать, в каких случаях стоит прикладывать усилия ради единообразия.
Тем не менее мы исправляем баги консистентности между сервисами. Каждый раз это интуитивное решение на основе опыта без математического подтверждения. Если у вас есть идеи, как это подтверждение получить — добро пожаловать в комментарии, обсудим.
С повторным использованием кода всё будто бы проще. Есть гигантское количество сервисов, бери и внедряй уже написанный код. Но на практике это не так просто.
Повторное использование кода
Проблемы с переиспользованием кода плюс-минус те же, что и с консистентностью. Разработка замедляется и становится дороже из-за долгого сбора требований.
Код из единой библиотеки иногда порождает недостаточно гибкие решения для пользователей, а попытка сбалансировать это, добавляя новые и новые фичи, приведёт вас в мир неэффективных и медленных приложений.
Возникают сложности с внедрением. Одно дело, когда команда пишет локальный код и сразу отправляет в продакшн. Совсем другое, когда есть общая библиотека: приходится писать универсальный код по особым правилам, выпускать версии, доставлять до проектов и прикручивать к бизнес-логике.
Когда потребуется обновление, могут появиться проблемы с обратной совместимостью. Поддержка должна репортить баги в общую библиотеку и следить, чтобы исправления ничего не ломали соседям. Может, писать каждый раз заново получится быстрее и выгоднее?
Метрика повторного использования
Давайте попробуем найти метрику для оценки успешности библиотеки с универсальными компонентами. Мы в работе используем разные критерии:
Количество скачиваний (как в npm).
Посещаемость сайта с документацией — чем выше, тем активнее пользуются библиотекой.
Процент пользователей общей библиотеки от количества фронтендеров в компании.
Процент пользователей на upstream. Кто-то обновляется регулярно, а кто-то однажды подключил библиотеку и с тех пор не получает никаких обновлений.
Количество используемых компонентов из библиотеки. Некоторые разработчики берут только логотип, а другие — полный набор.
Количество показов конечным сервисам. Одно дело, когда компонент используется парой человек во внутренней админке, и совсем другое — в результатах поисковой выдачи для десятков миллионов пользователей в день.
Ускорение процесса разработки.
Применимость инфраструктуры общей библиотеки для нужд других команд.
Но даже получив данные по этим критериям, мы не сможем с уверенностью сказать, пора ли создавать единую дизайн-систему в компании.
Универсальный компонент vs. локальные фичи
Давайте конкретизируем вопрос: сейчас лучше написать общий компонент или вложиться в несколько локальных фич? Чтобы ответить на него, хватит одной формулы, которая показывает полезность разработки в деньгах, позволяет ранжировать задачи и распределять ресурсы.
Формула для подсчёта окупаемости общих компонентов.
Разберём формулу: умножаем количество внедрений (подразумевается количество сервисов, где будет использован компонент) на среднюю стоимость разработки того же компонента на любом из сервисов. Делим произведение на количество внедрений (совпадает с тем, что в числителе), умноженное на стоимость внедрения. Так мы учитываем разницу между стоимостью программирования кнопки и внедрением из библиотеки. Добавляем стоимость унификации — усилия на то, чтобы вместо конкретной кнопки получить общую:
время на написание кода,
создание и публикация документации и примеров,
Пример расчёта для компонента, который используют три сервиса.
Попробуем проверить. Например, есть три проекта, которым нужен одинаковый компонент. Стоимость разработки — пять дней. Пусть стоимость внедрения будет два дня. При этом стоимость унификации в три раза выше, чем у конкретного решения в сервисе. C такими вводными делать общий компонент невыгодно (тут мы не учитываем профит от унификации дизайна).
Возможна ситуация, когда внедрение универсального компонента окажется дороже, чем стоимость разработки очередного велосипеда. Например, если технологические стеки библиотеки и проекта разные.
Давайте перейдём от абстрактных примеров к реальным компонентам, которые мы разрабатываем в opensource-пакете @yandex/ui.
Реализация ссылок в Яндексе.
Яндекс у многих ассоциируется с таким списком ссылок. Разумеется, эти ссылки используются и в других сервисах компании. Рассчитаем рентабельность разработки общего компонента для ссылки. Сейчас он внедрён в 104 сервиса. Средняя стоимость разработки такого компонента на отдельном сервисе с запасом — один день, стоимость внедрения — 2 часа или 0.25 от рабочего дня. Время унификации с учётом написания документации — 3 дня.
Считаем выгоду для компонента Link.
Видно, что сделать ссылки универсальными в 3.5 раза выгоднее, чем каждый раз писать с нуля. Хотя, согласитесь, интуитивно казалось, что повторное использование готового компонента на 104 проектах даст больше экономии. Рассмотрим более сложный компонент — Select.
Реализация Select в Яндексе.
Он состоит из кнопки, иконки, попапа и меню, обладает относительно сложной логикой и требует учесть множество нюансов, чтобы оставаться доступным на всех платформах.
Считаем выгоду для компонента Select.
И хотя он работает всего на 61 сервисе, видно, что выгода от повторного использования в 11.3 раз выше, чем если бы команды писали компонент самостоятельно. Выгода продолжит расти по мере того, как всё больше сервисов будут внедрять Select. Похоже, мы нашли правильный баланс.
Как же достичь баланса?
Единственно верного ответа нет. Однако есть решения, которые помогают нам на практике. Мы выделили разработчиков общих компонентов в отдельную команду. Разработчик конкретного сервиса вряд ли поставит в приоритет проблемы других команд. У продуктов разные цели, разный релизный цикл, разная браузерная поддержка и много чего ещё разного даже внутри одной компании.
Важно настроить взаимодействие с дизайнерами, потому что писать универсальный код, когда дизайнеры между собой не договорились, бесполезно — сэкономить не получится.
И, конечно, нужно вкладываться в унификацию — дорогое внедрение съедает профит. В эту же категорию попадает обратная совместимость. Если каждый апдейт требует реальных действий со стороны пользователя — выгода нивелируется.
Дизайн-система дизайн-систем
Яндекс оставляет сервисам собственные дизайн-системы, чтобы использовать кастомные решения, когда они помогают продуктовым метрикам. Всё это объединено в систему систем со слоями: первый подходит всем сервисам, дальше мы поднимаемся на следующие уровни, и с каждым следующим шагом базовая система уточняется. У нас есть что-то для всего Яндекса, для группы сервисов, для конкретного сервиса и финальные штрихи, применимые к отдельной странице на конкретной платформе.
Изменяя компонент на базовом слое, мы автоматически поставляем его в остальные слои. Именно для создания таких систем разработана методология БЭМ. Для реализации визуальной части есть Whitepaper. А недавно у нас появился themekit, который реализует наследование по слоям для дизайн-токенов.
Когда у каждого сервиса есть своя дизайн-система, приходится консолидировать многообразие. Поэтому мы используем общий storybook со всеми компонентами: их можно найти, посмотреть, и увидеть, что где-то решение уже есть. Если оно понадобится на других проектах, мы сделаем его общим.
Монорепозиторий
Вносить изменения, которые затронут множество сервисов, проще, если код лежит в едином репозитории. Но даже если нельзя физически положить код в одно место, реально написать инструменты, которые сделают внедрение сквозных изменений удобнее.
Canary
У нас есть возможность выпускать canary-версии общих библиотек с компонентами и сразу проверять, что тесты сервисов не ломаются. Так, ещё на этапе пул-реквеста будет понятно, что изменения стабильны и ничего не затронули. Или наоборот.
Авторелизы
Доставлять изменения в сервисы нужно быстро и дёшево. Без автоматизации править версию во всех зависимостях было бы затруднительно.
Умные чейнджлоги
Пользователи могут сидеть на разных версиях, поэтому чейнджлоги выдают срез от текущей до той, что прилетела в пул-реквест. Это необходимо, чтобы тестировщики проверяли только то, что поменялось в сервисе.
А как же технологии?
Проблема более низкого уровня — сочетаемость технологий. Почему бы не ввести технофашизм? Выбрать определённые технологии, а остальные запретить. Этот подход кажется логичным, но приведёт к тому, что сервисы перестанут развиваться, не имея свободы пробовать новое.
Мы пришли к концепции школ разработки — островков с понятными разрешёнными технологиями в отдельных командах или объединениях команд, которые живут по прозрачным правилам. Переиспользование внутри одной школы становится простым и дешёвым.
Над балансом придётся поработать.
Выводы и ссылки
Польза от консистентности определённо есть, но мы пока не придумали, как её измерить. Польза от переиспользования кода тоже есть, и она измерима. Однако за ней стоят сложные процессы, которые требуют соблюдения баланса.
О разных подходах к общим компонентам я рассказываю уже давно, и если вам интересна эта тема — вот ссылки на другие материалы:
Дублирование кода и неконсистентность дизайна: ищем решение
Привет! Меня зовут Игорь Дубровин, я Android-разработчик в SuperJob. Давно хотел поднять тему неконсистентности дизайна в приложении, поговорить о проблеме отсутствия единого стиля. Представьте: вы открываете приложение с вакансиями и на разных экранах видите предложения о работе в разном дизайне – в поисковой выдаче одно, а в ленте избранного немного другое. По факту блоки могут иметь совсем незначительные отличия, но пользователь все равно начинает путаться. Почему? Все просто. Он привык к единому внешнему виду элементов экрана.
Есть несколько основных причин, почему может сбиваться заданный стиль. Предлагаю рассмотреть подходы, которые мы использовали в SuperJob. Заглядывай под кат!
Почему теряется общий стиль
Первая причина – это дублирование кода. Разберем проблему на простом примере: в приложении на нескольких экранах необходимо сделать квадратную кнопку. Этими экранами занимаются разные разработчики и изначально делают кнопки одинаково. Казалось бы, в чем проблема?
Дело в том, что через некоторое время появляется задача скруглить углы всем кнопкам, и ее берет другой разработчик. В коде он находит реализацию первой кнопки, скругляет ей углы, а реализацию второй кнопки не исправляет, потому что просто не находит ее. Итог: две кнопки с разным дизайном.
Вот так дублирование кода и вредит приложению. В случае необходимости изменить поведение или внешний вид элемента, реализация которого дублируется, бывает сложно найти все копии в приложении и внести в них соответствующие изменения. Поэтому в приложении остаются места, в которых используются устаревшие элементы.
Есть много причин, из-за которых может возникать дублирование кода. Я постарался выделить основные, которые касаются внешнего вида элементов экрана при разработке Android-приложений:
Несогласованность между разработчиками. Несколько разработчиков работают над одним или несколькими экранами приложения, и им одновременно требуется некий элемент экрана. И каждый разработчик создает свой.
Особенности работы со списками в Android. Может быть так, что элемент экрана возможно использовать только в списках. И когда этот элемент понадобится применить вне списка, обнаружится два варианта решения проблемы: либо придется затронуть уже протестированные и хорошо работающие экраны, на которых используется этот элемент, либо скопировать уже написанный код элемента в нужное место.
Использование гибких элементов экрана. Такие элементы обладают широким функционалом и определенным набором методов, с помощью которых их можно конфигурировать. Для того чтобы настроить такой элемент нужным образом приходится писать много кода. И во всех местах, в которых требуется настроить элемент одинаково, код будет однотипным, что по сути является дублированием.
Вторая причина проблем со стилями – отсутствие единого списка элементов, используемых в приложении. Из-за этого нельзя однозначно сказать, был ли реализован элемент с требуемым функционалом ранее или нет. Это приводит к созданию новых элементов, которые по функционалу дублируют существующие, но могут иметь внешние отличия.
Как мы решали проблему неконсистентности
В нашем случае всплыли две основных проблемы: дублирование кода и отсутствие перечня реализованных элементов, доступных для использования разработчиками. Как их решать? Ответ мы нашли в дизайн-системе.
Дизайн-система (ДС) – это стандартизация дизайна, некий набор компонентов и их состояний, которые могут быть использованы в приложении. ДС помогает разрабатывать приложение так, чтобы всегда использовалась только одна кнопка. Кроме того, в случае изменения внешнего вида элемента, он будет меняться везде без необходимости исследовать весь код.
Давайте рассмотрим, как мы выделяем дизайн-компоненты на примере одного из экранов, «Мои связи». Все элементы этого экрана являются частями дизайн-системы. Каждый выделенный квадрат заведен как отдельный компонент в ДС. Кроме того, некоторые компоненты могут являться частью других, как, например, блок рекомендаций является частью сниппета контакта. На данный момент все наши экраны строятся на основе дизайн-системы, и разработчик всегда использует только те компоненты, которые есть в доступе.
Но бывает так, что компонент с требуемым функционалом или внешним видом еще не был реализован. Для таких случаев мы выработали алгоритм:
Дизайнер отрисовывает новый компонент и все его состояния. Дополнительно, если у компонента есть неочевидное поведение, невозможное к прорисовке, дизайн сопровождается текстовым описанием;
Происходит обсуждение нового компонента с разработчиками. Четко описывается для чего он нужен, где будет использоваться, в каких состояниях может находиться;
Создается отдельная задача и начинается разработка компонента;
Обязательное дизайн-ревью. Дизайнер проверяет правильность реализации компонента с точки зрения внешнего вида и поведения.
Давайте немного подробнее рассмотрим наш единый подход к разработке новых дизайн-компонентов. Каждый раз создается отдельная сущность ViewState, которая является контрактом внешнего вида компонента. ViewState выглядит максимально просто: это data class с набором полей, которые компонент может визуализировать. Кроме этого есть мета-информация компонента, такая как id, payload, декоратор элемента списка.
Под спойлером код в текстовом формате
Далее создается еще один контракт, описывающий действия, которые можно совершить над компонентом – listener. В нашем случае можно только нажать на компонент, поэтому в листенере только один коллбэк. В более сложных случаях количество коллбэков листенера может быть увеличено.
Код в текстовом формате
Дальше все достаточно тривиально:
Верстаем компонент. Все отступы компонента, внешние и внутренние, задаются в верстке;
Описываем логику работы компонента;
3.Если предполагается, что компонент будет использоваться в списке, то создаем для него элемент списка и адаптер-делегат, в котором описана тривиальная логика передачи текущего состояния в дизайн-компонент.
Дополнительный уровень абстракции
Небольшое отступление, еще один уровень абстракции. Рассмотрим его на примере работы со строками. В Android существуют различные типы строк: это может быть непосредственно String, а может быть строка, хранящаяся в строковых ресурсах strings.xml или plurals.xml. У EmptyStateMediumVs есть primaryMessage (сообщение об ошибке), и оно может прийти из бекенда. В таком случае надо будет туда положить эту строку. Но может быть и такая ситуация, что понадобится достать ее из ресурсов.
У нас было несколько вариантов решения этой проблемы:
Сделать несколько полей для primaryMessage. Выглядело бы это следующим образом:
В зависимости от ситуации мы бы передавали в EmptyStateMediumVs строку или id строкового ресурса, а вьюшка бы сама решала как их обработать. Но primaryMessage является обязательным аргументом EmptyStateMediumVs, и его нельзя игнорировать, наш компонент не умеет работать без него. А в данном случае мы позволяем вообще ничего не передавать.
Реализовать ResourceProvider, который по id ресурса отдавал бы нам сообщение, и использовать его в тот момент, когда необходимо достать строку из ресурсов. В данном случае мы сохраним у view state только одно поле primaryMessage, и это поможет нам оставить его обязательным. Но есть несколько ограничений: во-первых, пришлось бы прокидывать эту зависимость в места создания view state, т.е. во все view model; во-вторых, раскрывались бы детали реализации для view model: как именно мы получаем ту или иную строку из ресурсов. Из strings.xml и plurals.xml они достаются по разному, соответственно в ResourceProvider у нас бы были разные методы.
Мы хотели научить наши компоненты самостоятельно визуализировать строки вне зависимости от того, какое они имеют представление. Поэтому мы пошли по другому пути решения проблемы и ввели отдельную абстракцию StringVs, которая описывает все необходимые нам варианты. Выглядит она следующим образом:
Дополнительно мы написали несколько экстеншенов, которые помогают нам все создавать:
Таким образом нам больше не нужна дополнительная зависимость в наших view model, и им больше не надо знать, как переводить разные типы строк в String. Осталось только научить наши компоненты с ними работать. Для этого мы написали небольшой экстеншен-парсер:
И наконец нужно внутри нашего дизайн-компонента, в методе применения ViewState, установить соответствующее значение:
Такие абстракции мы сделали для всех типов данных, которые могут храниться как в ресурсах, так и в любом другом источнике данных.
Цвета:
Размеры:
Картинки:
Все остальное реализовано также, как и у StringVs. Есть экстеншены, которые помогают нам создавать абстракции. Передаем их в ViewState дизайн-компонентов, а они уже сами знают, как будут использовать тот или иной тип абстракции.
Гибкость дизайн-компонент
Мы рассмотрели весь процесс создания нового дизайн-компонента, но остался один момент – речь про гибкость дизайн-компонентов. Ведь при создании библиотеки для компонента хочется, чтобы ее использовало как можно больше людей с максимально-возможной гибкостью.
Но поскольку мы делаем компоненты для определенного приложения с определенным стилем, для нас гибкость является излишней. Она усложняет код, а мы стараемся делать компоненты максимально простыми.
По нашей философии компоненты должны быть гибкими ровно до той степени, чтобы удовлетворять нуждам проекта.
То есть они должны настраиваться как нужно только нашему проекту, чтобы не усложнять код и не привносить настройки, которые не будут использоваться. Тем более при явной необходимости всегда можно что-то расширить.
Первый наш дизайн-компонент GeneralItem достаточно гибко настраивается. У него есть много состояний, комбинаций элементов, в которых он может находиться. Однозначно описать связь его элементов друг с другом сложно. Помимо большого количества состояний, у него также могут настраиваться отступы между элементами.
Серый квадрат в левом верхнем углу – это контейнер для других элементов экрана. На данный момент у нас поддерживается check-box, radio-button, progress-bar и picture. С учетом всех особенностей GeneralItem, код компонента получился сложным и большим. Его достаточно дорого поддерживать. Кроме того такие компоненты могут отрицательно влиять на скорость работы приложения.
Также в этом компоненте мы опять столкнулись с проблемой дублирования кода. Ведь для того, чтобы на разных экранах GeneralItem выглядел одинаково, его ViewState приходится одинаково настраивать, а это значит передавать одни и те же отступы, одинаково описывать состояния одних и тех же элементов, которые вставляются в серый квадрат.
Так как GeneralItem получился достаточно гибким, то использоваться он может в любом месте приложения. Изначально этот дизайн-компонент делался для экрана выбора статуса видимости резюме. Но в дальнейшем мы стали использовать GeneralItem и на других экранах. Например во всплывающем подтверждении удаления подписки. Выглядят они совершенно по разному, и для подтверждения можно было бы создать новый компонент с текстом и иконкой, а не использовать большой и тяжелый. И, возможно, он уже создан.
Именно из-за этого сейчас мы делаем небольшие компоненты с ограниченной областью применения и ограниченным набором состояний. Все это для того, чтобы на всех экранах они выглядели одинаково и выполняли только свое предназначение. Кроме прочего это позволяет исключить фактор человеческой ошибки (например передачу неправильных отступов во ViewState компонента).
Итоги
Также мы получили несколько дополнительных плюсов. Во-первых, единый интерфейс для работы с дизайн-компонентами, что снижает порог входа в использование нового незнакомого элемента. Во-вторых, создание компонентов без привязки к месту их использования. Мы можем создавать новые компоненты и тестировать их в дизайне приложения не дожидаясь реализации фичи, для которой требуется этот компонент. И, наконец, шаблон для AndroidStudio автоматически генерирует нам скелет наших дизайн-компонентов. Это помогает не заниматься скучной рутинной работой, а сразу переходить к реализации логики работы компонентов.
Суммарный итог всего вышесказанного выливается в ускорение разработки. Плюс, за счет вышеперечисленных шагов, мы добиваемся консистентности дизайна нашего приложения и минимизируем силы и расходы на реализацию новых фич.