нашими мобильными приложениями для обмена данными с сервером. Свою роль может сыграть даже ограничение пропускной способности мобильной сети, которое заставит задуматься именно о ширине полосы пропускания. Различные виды взаимодействия могут приводить к быстрой разрядке батарей, отсекая тем самым часть потребителей.
Изменяется и характер взаимодействий. Я, к примеру, не могу так же запросто, как на простых компьютерах, щелкнуть правой кнопкой мыши, если работаю на планшете. А для мобильного телефона у меня может возникнуть желание разработать интерфейс под преимущественное управление одной рукой, чтобы большинство операций можно было вызвать с помощью манипуляций большим пальцем. А где-то еще, в местах с очень высокой платой за ширину полосы пропускания, например в странах со средним уровнем развития, где SMS используются в качестве интерфейса весьма часто, я могу позволить людям взаимодействовать с сервисами с помощью SMS.
Итак, несмотря на то, что наши основные сервисы или же основные предложения могут быть одинаковыми, нужен способ их адаптации под различные ограничения, существующие для каждого типа интерфейса. Рассматривая различные стили композиций пользовательского интерфейса, мы должны удостовериться в том, что они отвечают решению этой непростой задачи. Рассмотрим несколько моделей пользовательских интерфейсов, чтобы посмотреть, как можно будет достичь желаемого результата.
Предположим, что наши сервисы уже общаются друг с другом посредством XML или JSON через HTTP и очевидный доступный нам вариант заключается в непосредственном взаимодействии пользовательского интерфейса с теми API, которые показаны на рис. 4.7. В пользовательском интерфейсе на основе веб-технологий для извлечения данных можно воспользоваться написанными на JavaScript GET-запросами, а для изменения этих данных можно воспользоваться POST-запросами. Даже для чисто мобильных приложений инициировать обмен данными по протоколу HTTP довольно легко. Для пользовательского интерфейса затем придется создать различные компоненты, составляющие интерфейс, справляющиеся с синхронизацией состояния и тому подобным с сервером. Если для обмена данными между серверами использовался двоичный протокол, ситуация может усложниться для клиентов, работающих на основе веб-технологий, но при этом может вполне подойти для чисто мобильных устройств.
Рис. 4.7. Использование нескольких API для представления пользовательского интерфейса
Но у этого подхода есть ряд недостатков. В первую очередь это ограниченный круг возможностей для адаптации ответов под устройства различных типов. Например, должен ли я при извлечении записи клиента вытаскивать все те же данные для мобильного магазина, что и для приложения службы поддержки клиентов? При таком подходе одним из решений может стать разрешение пользователям указывать, какие поля извлекать при выдаче ими запроса, но это предполагает, что такой вид взаимодействия должен поддерживаться всеми сервисами.
И еще один ключевой вопрос: кто создает пользовательский интерфейс? Тех, кто приглядывает за сервисами, не беспокоит то, как их сервисы появляются перед пользователями. Например, если пользовательский интерфейс создает другая команда, мы можем вернуться к прежним не самым благополучным временам многоуровневой архитектуры, когда внесение даже самых незначительных изменений вело к перемене требований сразу к нескольким командам.
Этот обмен данными может быть слишком многословным. Открытие множества вызовов непосредственно к сервисам может оказаться слишком обременительным для мобильных устройств и привести к весьма неэффективному расходу мобильного плана клиента! Здесь может помочь наличие API-шлюза, поскольку вы сможете выдать вызовы, объединяющие несколько исходных вызовов, хотя само по себе это может иметь ряд недостатков, которые мы вскоре рассмотрим.
Вместо того чтобы пользовательский интерфейс занимался API-вызовами и отображал все обратно на свои элементы управления, мы можем сделать так, чтобы сервисы предоставляли части пользовательского интерфейса напрямую, а затем, как показано на рис. 4.8, просто вставить эти фрагменты с целью создания пользовательского интерфейса. Представьте себе, к примеру, что рекомендационный сервис предоставляет рекомендационный виджет, который с целью создания полноценного пользовательского интерфейса объединяется с другими элементами управления или фрагментами этого интерфейса. Он может отображаться на веб-странице наряду с другим ее содержимым в виде блока.
Рис. 4.8. Сервисы, непосредственно обслуживающие компоненты пользовательского интерфейса, предназначенные для создания сборки
Разновидностью такого подхода, который может вполне достойно справиться со своей работой, является сборка крупномодульных частей пользовательского интерфейса. Здесь вместо создания небольших виджетов собираются вместе целые панели солидного клиентского приложения или, возможно, набор страниц для сайта.
Эти довольно крупные фрагменты подаются приложениями серверной стороны, которые, в свою очередь, делают соответствующие API-вызовы. Эта модель лучше всего работает, когда фрагменты точно распределяются по принадлежности между командами. Например, возможно, команда, которая несет ответственность за управление заказами в музыкальном магазине, обслуживает все страницы, связанные с управлением заказами.
Для того чтобы собрать все эти части вместе, понадобится некий сборочный уровень. Этот вопрос может быть решен простым созданием шаблонов на серверной стороне, или же там, где каждый набор страниц поставляется другим приложением, вам, наверное, потребуется некая интеллектуальная URI-маршрутизация.
Одним из ключевых преимуществ этого подхода является то, что та же команда, которая вносит изменения в сервисы, может также заниматься внесением изменений в соответствующие части пользовательского интерфейса. Это позволяет ускорить получение изменений. Но с этим подходом все же связаны некоторые проблемы.
В первую очередь нужно обратить внимание на обеспечение соответствия пользовательского восприятия. Пользователям хочется получать цельное восприятие, чтобы у них не возникало ощущения, что разные части интерфейса работают по-разному или что они представляют разные языки дизайна. Но существуют технологии, позволяющие обойти эту проблему, например действенные стилевые ориентиры (living style guides), где такие ресурсы, как HTML-компоненты, CSS и изображения, могут использоваться совместно, содействуя тем самым выдерживанию определенного уровня взаимного соответствия.
А вот с другой проблемой справиться сложнее. Что происходит с чистыми приложениями или полноценными клиентами? Мы не можем обслуживать компоненты пользовательского интерфейса. Можно применить гибридный подход и использовать для обслуживания HTML-компонентов чистые приложения, но в этом подходе постоянно обнаруживаются недостатки. Поэтому, если вам требуется получить естественное восприятие, придется вернуться назад, к подходу, при котором интерфейсное приложение самостоятельно осуществляет API-вызовы и управляет пользовательским интерфейсом. Но даже если рассматривать только пользовательские интерфейсы на основе веб-технологий, все равно может потребоваться иметь совершенно разную трактовку для разного типа устройств. Разумеется, помочь в данном вопросе может создание отзывчивых компонентов.
У этого подхода имеется еще одна ключевая проблема, в вероятности решения которой я не уверен. Иногда возможности, предлагаемые сервисом, не вписываются в виджет или страницу. Конечно, мне может потребоваться выдать какие-либо общие рекомендации в блоке страницы нашего сайта, а что, если я захочу создать систему динамических рекомендаций где-либо в другом месте? При поиске я, к примеру, хочу, чтобы набираемый текст автоматически продолжался выводом новых рекомендаций. Чем больше сквозных форм взаимодействия, тем меньше надежд на то, что эта модель подойдет, и больше подозрений, что придется вернуться назад, к простой выдаче API-вызовов.
Обычным решением проблемы многословных интерфейсов внутренних сервисов, или проблемы, связанной с необходимостью изменения содержимого для разных типов устройств, является использование объединяющей конечной точки на серверной стороне, или API-шлюза. Тем самым можно будет выстраивать несколько внутренних вызовов в случае необходимости изменять и собирать содержимое для различных устройств и, как показано на рис. 4.9, обслуживать все это. Я видел, что подобные конечные точки на серверной стороне, становясь довольно мощными уровнями со слишком развитым поведением, приводили к катастрофе. Все заканчивалось тем, что ими управляли различные команды разработчиков и они становились еще одним местом, где функциональные изменения приводили к необходимости внесения изменений в логику работы.
Рис. 4.9. Использование единого монолитного шлюза для управления вызовами из пользовательских интерфейсов и к самим этим интерфейсам
Может возникнуть проблема, связанная с тем, что у нас вполне естественным образом получится просто гигантский уровень для всех наших сервисов. Это приведет к тому, что все будет свалено в кучу и мы внезапно начнем терять изолированность различных пользовательских интерфейсов, что ограничит возможности независимой реализации. Я отдаю предпочтение работоспособной, на мой взгляд, модели (рис. 4.10), которая заключается в сведении использования таких внутренних интерфейсов к одному конкретному пользовательскому интерфейсу или приложению.
Рис. 4.10. Использование для внешних интерфейсов предназначенных конкретно для них внутренних интерфейсов
Эту схему иногда называют внутренними интерфейсами для внешних интерфейсов (backends for frontends (BFF)). Она позволяет команде, отвечающей за любой отдельно взятый пользовательский интерфейс, вдобавок ко всему обслуживать его собственные компоненты, находящиеся на стороне сервера. Эти внутренние интерфейсы можно рассматривать как часть пользовательского интерфейса, встроенную в сервер. Некоторые типы пользовательских интерфейсов требуют минимальной площади опоры на сервере, а некоторым нужна опора посолиднее. Если нужен уровень API-аутентификации и авторизации, то он может располагаться между BFF-интерфейсами и пользовательскими интерфейсами. Более подробно этот вопрос рассматривается в главе 9.