Это глава 43 раздела «SDK и UI-библиотеки» моей книги «API». Второе издание книги будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа была для вас полезна, пожалуйста, оцените книгу на GitHub, Amazon или GoodReads. English version on Substack.

Введение в состав SDK UI-компонентов обогащает и так не самую простую конструкцию из клиент-серверного API и клиентской библиотеки дополнительным измерением: теперь с вашим API взаимодействуют одновременно и разработчики (которые написали код приложения), и пользователи (которые используют приложение). Хотя это изменение на первый взгляд может показаться не очень значительным, с точки зрения дизайна API добавление конечного пользователя — огромная проблема, которая требует на порядок более глубокой и качественной проработки дизайна программных интерфейсов по сравнению с «чистым» клиент-серверным API. Попробуем объяснить, почему так происходит, на конкретном примере.

Пусть мы решили поставлять в составе нашего кофейного API также и клиентский SDK, который предоставляет готовые компоненты для разработчиков приложений. Достаточно простая функциональность: пользователь вводит поисковый запрос и видит результаты в виде списка.

Основной экран приложения с результатами поиска.
Основной экран приложения с результатами поиска.

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

Панель просмотра предложения.
Панель просмотра предложения.

Для реализации этого сценария мы предоставим объектно-ориентированный API в виде, ну скажем, класса SearchBox, который реализует описанную функциональность поверх клиент-серверного метода search нашего клиент-серверного API.

Проблемы

С одной стороны нам может показаться, что наш UI — это просто надстройка над клиент-серверным search, визуализирующая результаты поиска. Увы, это не так; перечислим проблемы, с которыми мы никогда не сталкивались при разработке API без визуальных компонент.

1. Объединение в одном объекте разнородной функциональности

Посмотрим на панель действий с предложениями. Допустим, мы размещаем на ней две кнопки — «заказать» и «показать на карте» — плюс действие «отменить». Эти кнопки выглядят одинаково и реагируют на действия пользователя одинаково — но при этом осуществляют абсолютно не имеющие ничего общего друг с другом действия.

Допустим, мы предоставили программисту возможность добавить свои кнопки действий на панель, для чего предоставим в составе SDK класс Button. Достаточно быстро мы выясним, что этой функциональностью будут пользоваться в двух основных диаметрально противоположных сценариях:

  • для размещения на панели дополнительных кнопок, ну скажем, «позвонить в кафе», выполненных в том же дизайне, что и стандартные;

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

Более того, возможен и третий сценарий: разработчики заходят сделать кнопку «позвонить», которая будет и выглядеть иначе, и программно выполнять другие действия, но при этом будет наследовать UX кнопки — т.е. нажиматься при клике, располагаться в ряд с другими кнопками и так далее.

С точки зрения разработчика SDK это означает, что класс Button должен позволять независимо переопределять и внешний вид кнопки, и реакцию на действия пользователя, и элементы UX — или, иначе говоря, каждая из трёх подсистем может быть заменена альтернативной имплементацией так, чтобы две остальные системы продолжили работать без изменений.

2. Разделяемые ресурсы

Предположим, что мы хотим разрешить разработчику подставить в наш SearchBox свой поисковый запрос — например, чтобы дать возможность разместить в приложении баннер «найти лунго рядом со мной», по нажатию на который происходит переход к нашему компоненту с введённым запросом «лунго». Для этого разработчику потребуется программно показать соответствующий экран и вызвать нужный метод SearchBox-а — допустим, не мудрствуя лукаво, мы назовём его просто search.

Два наших метода search («чистый» клиент-серверный и компонентный SearchBox.search) принимают одни и те же параметры и выдают один и тот же результат. Но ведут себя эти методы совершенно по-разному:

  • если вызвать несколько раз SearchBox.search, не дожидаясь ответа сервера, то все запросы, кроме последнего во времени, должны быть проигнорированы; даже если ответы пришли вразнобой, только тот из них, который соответствует новейшему запросу, должен быть показан в UI;

    • дополнительная задача — что должен вернуть вызов метода SearchBox.search, если он был прерван выполнением другого запроса? Если неуспех, то в чём состоит ошибка вызывающего? Если успех, то почему результат не был отражён в UI?

  • что порождает другую проблему: а если в момент вызова SearchBox.search уже исполнялся какой-то запрос, инициированный пользователем — что должно произойти? Какой из вызовов приоритетнее — выполненный разработчиком или выполненный самим пользователем?

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

Любая асинхронная операция в UI-компонентах, особенно если она индицируется визуально (с помощью анимации или другого длящегося действия), может помешать любой другой визуальной операции — в том числе вследствие действий пользователя.

3. Множественная иерархия подчинения сущностей

Предположим, что разработчик хочет обогатить дизайн списка предложений иконками сетей кофеен. Если изображение известно, оно должно быть показано всюду, где происходит работа с предложением конкретной кофейни.

Результаты поиска с иконкой кофейни.
Результаты поиска с иконкой кофейни.

Теперь предположим, что разработчик также переопределил внешний вид всех кнопок в SDK, добавив иконки действий.

Панель показа предложения с иконками действий.
Панель показа предложения с иконками действий.

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

Можно легко продемонстрировать, как пересечение нескольких предметных областей в одном объекте быстро приводит к крайне запутанной и неочевидной логике. Поскольку те же соображения справедливы и для кнопки «Показать на карте», вроде бы очевидно, что по умолчанию более частные свойства должны побеждать более общие, т.е. тип кнопки должен быть приоритетнее какой-то абстрактной «иконки» в данных.

Но на этом история не заканчивается. Если разработчик всё-таки хочет именно этого, т.е. показывать иконку сети кофеен (если она есть) на кнопке создания заказа — как ему это сделать? Из той же логики, нам необходимо предоставить ещё более частную возможность такого переопределения. Например, представим себе следующую функциональность: если в данных предложения есть поле checkoutButtonIconUrl, то иконка будет взята из этого поля. Тогда разработчик сможет кастомизировать кнопку заказа, подменив в данных поле checkoutButtonIconUrl для каждого результата поиска:

const searchBox = new SearchBox({
  // Предположим, что мы разрешили
  // переопределять поисковую функцию
  searchFunction: function (params) {
    const res = await api.search(params);
    res.forEach(function (item) {
        item.checkoutButtonIconUrl = 
          <URL нужной иконки>;
    });
    return res;
  }
})

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

Если бы возможность кастомизации вообще не предоставлялась, эту функциональность было бы гораздо проще поддерживать. Да, разработчики были бы не рады необходимости разработать с нуля собственную панель поиска просто для замены иконки. Но в их коде замена иконки хотя бы будет находиться в ожидаемом месте — где-то в функции рендеринга панели.

NB: существует много других возможностей позволить разработчику кастомизировать кнопку, запрятанную где-то глубоко в дебрях компонента: разрешить dependency injection или переопределение фабрик суб-компонентов, предоставить прямой доступ к отрендеренному представлению компонента, настроить пользовательские макеты кнопок и так далее. Все они страдают от той же проблемы: крайне сложно консистентно описать порядок и приоритет применения инъекций / обработчиков событий рендеринга / пользовательских шаблонов.

С решением вышеуказанных проблем, увы, всё обстоит очень сложно. В следующих главах мы рассмотрим паттерны проектирования, позволяющие в том числе разделить области ответственности составляющих компонента; но очень важно уяснить одну важную мысль: полное разделение, то есть разработка функционального SDK+UI, дающего разработчику свободу в переопределении и внешнего вида, и бизнес-логики, и UX компонентов — невероятно дорогая в разработке задача, которая в лучшем случае утроит вашу иерархию абстракций. Универсальный совет здесь ровно один: три раза подумайте прежде чем предоставлять возможность программной настройки UI-компонентов. Хотя цена ошибки дизайна программных интерфейсов для UI-библиотек, как правило, не очень высока (вряд ли клиент потребует рефанд из-за неработающей анимации нажатия кнопки), плохо структурированный, нечитабельный и глючный SDK вряд ли может рассматриваться как сильное клиентское преимущество вашего API.

Комментарии (0)