Вы не стали чаще слышать о таком трендовом явлении, как Server Driven UI? Если вы ещё с ним не сталкивались, то в будущем обязательно столкнётесь. Я, как инженер, познакомилась с таким подходом чуть больше года назад, перейдя в другой проект в Альфа-Банке.

И если вы, как и я тогда, задаёте себе вопрос: «Что же это такое творится-то?», то рекомендую прочитать эту статью, где я на примере нашего нового функционала в приложении для физических лиц расскажу, что это есть на самом деле и как лёгким взмахом волшебной палочки backend-разработчик становится отчасти frontend’ером, реализуя на стороне серверной части не только логику, но и вёрстку всех экранов приложения.

Мы привыкли к тому, что приложение в глобальном смысле состоит из трёх компонент: пользовательского интерфейса, модулей обработки логики и базы данных. Является ли для кого-то секретом тот факт, что каждая из компонент большой системы разрабатывается людьми, имеющими разную специализацию: user interface (UI) пишут frontend-разработчики, за логику «под капотом» отвечают backend-разработчики?

В случае, когда нужно написать, к примеру, только web-приложение, всё выглядит весьма понятным. Но что делать, если вдруг возникла потребность написать приложение сразу под несколько платформ: web-версию, Android и iOS, а лишнего времени и рук нет? На помощь приходит Server Driven UI (SDUI). 

«Но что это?» — может задать себе вопрос неопытный разработчик, не сталкивавшийся с таким паттерном.

Server Driven UI (SDUI) — это пользовательский интерфейс, управляемый сервером. SDUI является архитектурным шаблоном, сокращающим клиентскую логику и обеспечивающим согласованность между клиентскими платформами (web, iOS, Android и т. д.) за счёт возврата информации о продукте (элементов экрана для отображения и их содержимом) из API.

Как это делается? Всё довольно просто. UI на любой из платформ делает необходимый ему запрос к API, а в ответ получает JSON, в котором содержатся компоненты экрана (кнопки, checkbox’ы, поля для ввода и т. д.), а также логика их отображения.

Рис 1. Принцип работы SDUI
Рис 1. Принцип работы SDUI

Исходя из неё, UI либо показывает эти элементы экрана, либо скрывает их, либо предзаполняет нужными значениями, либо отдает это на откуп самому пользователю.

Рассмотрим два экрана ниже (отмечу, что данный пример носит информативный характер и не является реальным):

Рис 2. Экраны мобильного приложения, основанные на SDUI с использованием зависимых полей
Рис 2. Экраны мобильного приложения, основанные на SDUI с использованием зависимых полей

Как видно из рисунка, изначально на экране отображаются только поля «Лицевой счёт», «Адрес», «Период оплаты». После того, как пользователь активирует checkbox «Внести авансовый платёж», отобразится дополнительное поле для ввода значения.

Чем это может быть для нас полезным? И как вообще строится backend-разработка на основе SDUI? Рассмотрим эти вопросы на примере нашей новой фичи в мобильном банке, оплаты коммунальных услуг по шаблону.

Grooming, или начало начал

Прежде, чем приступить к реализации любого большого и сложного функционала, необходимо разбить его на более мелкие подзадачи. Как это сделать и что для этого нужно? Здесь, казалось бы, всё довольно понятно — нам необходимы требования от бизнеса, на которые мы можем опираться.

Как правило, бизнес-требования до того момента, как попасть в руки разработчика, проходят этап аналитики. Аналитик уточняет требования, прорабатывает концепцию будущего функционала и в конечном итоге выдаёт чёткое ТЗ на разработку.

Отталкиваясь от результатов его работы, разработчик может разбить большую и, казалось бы, неподъёмную задачу на более мелкие, руководствуясь правилом: «Один кусок функциональности — одна задача». Такая декомпозиция действует как психологический трюк, показывая нашему мозгу, что это не «что-то большое и пугающее», а пошаговый план. Также это даёт возможность более чёткого планирования работ и контроля прогресса.

Но в случае разработки приложения с использованием SDUI всё выглядит не так, как мы, backend’еры, привыкли.

Когда что-то пошло не по плану

Придя на grooming для обсуждения новой задачи, ты ожидаешь увидеть текст требований, UML-диаграммы и т.д. Но с SDUI всё иначе.

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

Представим ситуацию, в которой нам необходимо реализовать шаблон оплаты коммунальных услуг. Как он должен работать? Клиент заходит на первый экран, выбирает одно из предложенных ему действий: посмотреть историю операций по этому поставщику, его реквизиты, редактировать или удалить шаблон, произвести оплату. И если это оплата, то далее он вводит реквизит, следуя предложенным инструкциям. Но вы как backend-разработчик должны написать не просто бизнес-логику конкретного кейса. Ваше приложение должно основываться на SDUI! Что же будет дальше? 

Признаюсь честно, первое время я чувствовала себя «не в своей тарелке», несмотря на то, что в начале своего карьерного пути мечтала стать Android-разработчиком (об этом вы можете прочитать в моей предыдущей статье «Ещё одна статья про карьеру: 15 убеждений которые превратились в инсайты»).

Думаю, меня поймут многие backend’еры. Мне было максимально странно всматриваться в какие-то картинки, а уж тем более их обсуждать. Первое время я просто не понимала, что происходит.

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

Рис 3. Клиентский путь оплаты ЖКХ по шаблону
Рис 3. Клиентский путь оплаты ЖКХ по шаблону

Из имеющихся дизайнов (например, оплаты ЖКХ по шаблону выше) и информации от аналитика я могла понять:

№1. Что должен делать наш новый микросервис, который в будущем будет отвечать за эту часть логики.

В примере, что я рассматриваю в этой статье, сервис должен давать возможность клиенту произвести оплату коммунальных услуг через шаблон. Клиент может зайти в раздел «Шаблоны и автоплатежи», выбрать необходимый и оплатить.

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

Рис. 4. Выбор шаблона для оплаты коммунальных услуг в разделе «Шаблоны и автоплатежи»
Рис. 4. Выбор шаблона для оплаты коммунальных услуг в разделе «Шаблоны и автоплатежи»

№2. Что мы можем переиспользовать в нашем сервисе.

Например, endpoint’ы сторонних сервисов, если вдруг уже есть API, отдающие нужную нам верстку с данными.

№3. Какой экран будет полностью «собран» нами, сколько на нём будет элементов и, в соответствии с этим, решить, сколько отдельных доработок потребуется для реализации логики каждого из них в новом сервисе.

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

№4. Какие элементы на экране характерны только для нашего кейса.

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

Для реализации логики оплаты мы переиспользовали уже имеющийся микросервис, который отдавал нам необходимые элементы вёрстки и данные для них, а наш сервис просто пробрасывал их front’у. Так мы поступили почти со всей логикой оплаты, кроме экрана подтверждения. Для него в уже имеющуюся верстку, которую отдавал готовый сервис, мы решили добавить отображение заголовка (2) и наименования поставщика (3), а также убрать информацию из header’a (1), реализовав это на стороне нашего микросервиса.

Рис 5. Экран подтверждения оплаты ЖКУ и доработанный экран подтверждения оплаты ЖКУ по шаблону
Рис 5. Экран подтверждения оплаты ЖКУ и доработанный экран подтверждения оплаты ЖКУ по шаблону

Слева можно увидеть экран подтверждения оплаты ЖКУ, элементы которого отдает готовый сервис, а справа — наш доработанный экран подтверждения оплаты ЖКУ по шаблону.

№5. Какие изменения необходимы в соседних API для безошибочного запуска нашего flow.

И швец, и жнец, и на дуде игрец

После того как планирование (которое я не буду описывать, так как вы и так знакомы с этим процессом) будет успешно проведено, наступает первый спринт для работы над новой функциональностью. И что же здесь? А здесь есть два варианта.

  • Техническая реализация задачи на разработку новой части функционала тебе понятна. Аналитик сопроводил её подробным описанием с применением диаграмм последовательности. Вопросов не возникает.

  • Задача не понятна ни тебе, ни твоему аналитику. На первом этапе варианты решения отсутствуют у вас обоих.

Если с первым пунктом всё ясно, то как быть со вторым? Легким движением руки брюки превращаются в элегантные шорты Task конвертируется в Spike в Jira, а разработчик входит в роль аналитика. Чтобы понять, как в дальнейшем должна быть реализована доработка, программисту на этом этапе приходится:

  • Штудировать документацию для большего понимания работы смежных сервисов и систем. Зная перечень API и то, за что они отвечают, порой приходится погружаться в них более детально. С помощью документации требуется полностью изучать работу сервиса: какие у него есть endpoint’ы, для чего они нужны, какие параметры в них передаются, какие данные из них возвращаются. И главное — сможем ли мы задействовать эти данные в разработке нашего микросервиса? Будут ли они для нас полезны? 

  • Изучать код API уже имеющейся реализации для более глубокого погружения в логику.

  • Тестировать готовый функционал, чтобы понять, каким образом «впилить» в него свой: какие сервисы по каким endpoint’ам вызывать, какие данные передавать и получать.

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

Хорошая новость заключается в том, что разработчик не одинок в этом занятии, ему, конечно же, помогает аналитик.

От теории к практике

Наконец-то концепция технического решения готова! Настало время для кодинга!

Вспоминаем, что архитектура backend-приложения для «AlfaMobile» основана на микросервисах — часть бизнес-логики переехала на так называемый многошаг. Именно он основан на принципах SDUI, что позволяет единообразно и динамически создавать экраны для web и мобильных приложений (подробнее о многошаге и нашей реализации SDUI можно прочитать в статье Анны Саботович «Эволюция Server-Driven UI: динамические поля, хэндлеры и многошаг»).

Что же он из себя представляет?

Многошаг состоит из набора API, среди которых есть входной сервис, имеющий публичный интерфейс: он выполняет роль роутера, и сервисов с бизнес-логикой. Роутер в зависимости от параметров запроса маршрутизирует трафик в конкретный микросервис, который отвечает за определенную часть логики приложения. Помимо этого входной API отвечает за аутентификацию и авторизацию.

Рис. 6. Структура шаблона API Gateway
Рис. 6. Структура шаблона API Gateway

Если вы по предыдущему описанию узнали реализацию одного из паттернов микросервисной архитектуры — API Gateway — вы молодцы! А если нет, или не знаете, что это такое, то рекомендую прочитать статью «Миграция микросервисной архитектуры на API Gateway»

Так как нам нужно добавить новый функционал, основанный на многошаге, то первым делом необходимо создать проект-шаблон, который в дальнейшем мы будем дополнять бизнес-логикой на Kotlin. Через систему автоматической сборки (у нас это Gradle) не забываем подключить все необходимые библиотеки и стартеры, включая стартер многошага.

Ура! Мы готовы к программированию логики, но сначала расскажу немного об multistepflow-starter’е и о том, из чего он состоит.

Как я уже говорила, у нас, в AlfaMobile, каждый сервис реализует одну из платежных операций: денежные переводы внутри банка, по реквизитам, трансграничные переводы, оплату коммунальных услуг и т.д. У каждого из этих сервисов есть обязательный единообразный набор endpoint’ов с определённым количеством параметров. Также есть сервис-роутер, который я упоминала выше, являющийся входной точкой для всех типов переводов, и который использует frontend.

Задача multistepflow-starter’а — помогать избегать дублирования кода. Он содержит общие интерфейсы, модели, утилиты, конвертеры и т.д. для всех сервисов. Ниже приведен набор endpont’ов, характерный для каждого микросервиса, реализующего отдельную бизнес-логику.

Примечание: Далее будут демонстрироваться изменённые имена сервисов, классов, переменных и методов, а код сервисов будет урезан по принципам безопасности.

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

@RestController
@RequestMapping("/steps")
@ConditionalOnMissingBean(name = ["MultistepFlowController"])
class MultistepFlowController<K>(
    private val multistepFlowService: MultistepFlowService<K>
) {

    @GetMapping
    suspend fun initialize(requestHeaders: RequestHeaders, params: InitParams): K = withTraceContext {
        multistepFlowService.iniialize(headers, params, request.queryParams)
    }

    @PostMapping("/next")
    suspend fun returnNext(requestHeaders: RequestHeaders, @RequestBody nextRequest: NextRequest): K = withTraceContext {
        multistepFlowService.returnNext(requestHeaders, nextRequest)
    }
}

Не будем вдаваться в детали реализации каждого из них — опишу в общем, за что отвечает каждый из endpoint’ов: 

  • GET /steps/ — инициализация flow новой операции, получение элементов 0-ого шага;

  • POST /steps/next/ — получение формы для ввода необходимых реквизитов на первом и последующих шагах.

Примечание: Нумерация шагов в многошаге начинается с 0 и увеличивается, как правило, на 1. Их количество зависит от вида операции и определяется backend’ом.

Каждый из методов контроллера MultistepFlowController вызывает один из следующих методов сервиса MultistepFlowService. Так как MultistepFlowServiceэто интерфейс, то имплементация его методов будет реализовываться непосредственно в классах сервиса с конкретной бизнес-логикой (в классах сервиса, в который подключается стартер для реализации конкретной операции).

interface MultistepFlowService<out K> {

    suspend fun initialize(requestHeaders: RequestHeaders, params: InitParams): K

    suspend fun returnNext(requestHeaders: RequestHeaders, nextRequest: NextRequest): K
}

Следующий контроллер отвечает за регистрацию новой операции в core-системе и её подтверждение пользователем, например, подписание через SMS.

  • POST /payment/ — регистрация операции;

  • PUT /payment/ — подтверждение операции с возвратом элементов финального экрана.

@RestController
@RequestMapping("/payment")
@ConditionalOnMissingBean(name = ["PaymentFlowController"])
class PaymentFlowController(private val paymentFlowService: PaymentFlowService) {

    @PostMapping
    suspend fun registerPayment(requestHeaders: RequestHeaders, @RequestBody registerRequest: RegisterRequest): RegisterResult = withTraceContext {
        paymentFlowService.registerPaymentOperation(requestHeaders, registerRequest)
    }

    @PutMapping
    suspend fun confirmPayment(requestHeaders: RequestHeaders, @RequestBody confirmationRequest: ConfirmationRequest): ConfirmationResult = withTraceContext {
        paymentFlowService.confirmPayment(requestHeaders, confirmationRequest)
    }
}

По аналогии с выше описанным контроллером методы PaymentFlowController также вызывают соответствующие методы PaymentFlowService, реализация которых будет описана в сервисе, что реализует конкретную операцию.

interface PaymentFlowService {

    suspend fun registerPaymentOperation(requestHeaders: RequestHeaders, registerRequest: RegisterRequest): RegisterResult

    suspend fun confirmPayment(requestHeaders: RequestHeaders, confirmationRequest: ConfirmationRequest): ConfirmationResult
}

Таким образом, стартер — это набор необходимых контроллеров, интерфейсов для дальнейшей реализации, классов и моделей, о которых в рамках публичной статьи я рассказывать (конечно же!) не буду. 

Краткий экскурс по multistepdflow-starter’у считаю законченным. Пора переходить к написанию логики операции для оплаты коммунальных услуг по шаблону.

Чтобы реализовать логику для вышеописанных endpoint’ов в микросервисе, отвечающем за эту операцию, достаточно подключить к проекту уже известный нам стартер, прописав его в build.gradle.

buildscript {
    dependencyManagement {
        dependencies {
            dependency "ru.alfabank.multistepflow:multistepflow-starter:3.6.0"
        }
    }
}

Далее необходимо реализовать методы интерфейсов стартера, к которым можно обратиться из endpoint’ов.

Для начала рассмотрим имплементацию логики инициализации операции и возврат последующих шагов для отображения на экране. Все это можно найти в сервисе TemplatePaymentMultistepService.

@Service
class TemplatePaymentMultistepService(
    private val transactionService: TransactionService, // for repository operations
    private val businessLogicService: BusinessLogicService // for retrieving data from the another service
) : MultistepFlowService<Step> {

    override suspend fun initialize(requestHeaders: RequestHeaders, params: InitParams): Tab {
        //some business logic
        val tab = businessLogicService.initializeTransaction(requestHeaders, params.module, params.ref)
        transactionService.saveTransactionEntity(tab)
        //some business logic for enrichment received information
        return tab
    }

    override suspend fun returnNext(requestHeaders: RequestHeaders, nextRequest: NextRequest): Tab {
        val transactionEntity = transactionService.findTransactionById(nextRequest.transactionId)
        //some business logic
        val tab = businessLogicService.nextStep()
        //some business logic for enrichment received information
        return prepareStepResponse(transactionEntity, tab)
    }
}

Чтобы реализовать абстрактные методы initialize() и returnNext() интерфейса MultistepFlowService, достаточно в классе TemplatePaymentMultistepSevice имплементировать его и далее переопределить методы, прописав для каждого из них необходимые бизнес-реализации.

Так как наша оплата ЖКХ по шаблону задумывалась как операция на основе уже имеющейся оплаты ЖКХ, то нам не пришлось писать логику методов с нуля. Мы вызывали уже имеющийся сервис для оплаты, брали данные из него, обрабатывали и обогащали так, как нам нужно. Поэтому в коде выше initialize() и returnNext() также вызывают сторонний BusinessLogicService, манипулируют его данными и сохраняют контекст операции в виде параметров в базу данных (у нас это MongoDB).

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

  • id — уникальный идентификатор;

  • type — тип элемента (INPUT — поле ввода, STRING — строка и т.д);

  • label — заголовок элемента;

  • value — значение поля;

  • hidden — признак необходимости отображения элемента на экране;

  • required — признак обязательности заполнения, и т.д.

Таким образом, каждый ответ от многошага представляет собой JSON, который состоит из набора динамических полей для отображении на front’e и handler’ов, которые помогают скрывать и отображать элементы экрана в зависимости от определенных условий, делая обновление экрана локальным, без дополнительных запросов на сторону backend’a.

На данный момент наша библиотека содержит несколько десятков различных полей, каждое из которых выполняет свою функцию. Подробнее о динамических полях и handler’ах можно также прочитать в статье «Эволюция Server-Driven UI: динамические поля, хэндлеры и многошаг».

Рис. 7. Маппинг полей, отображаемых на экране, на элементы JSON
Рис. 7. Маппинг полей, отображаемых на экране, на элементы JSON

Вернёмся снова к нашему примеру с оплатой ЖКХ по шаблону. Ранее мы говорили о том, что наш сервис переиспользует данные другого. Но что же делать, если нам их не хватает, и мы хотим получить экран с дополнительными полями? Здесь всё достаточно просто! Мы обогащаем полученный ответ от другого сервиса дополнительными элементами и данными для них, получая на выходе более расширенный ответ от нашего сервиса. Например, нам пришлось применить такой трюк с экраном подтверждения операции (см. рис. 5). Мы дополнили его заголовком и названием поставщика услуги.

Рис. 8. Сравнение экрана подтверждения оплаты ЖКХ и экрана подтверждения оплаты ЖКХ по шаблону в формате JSON.
Рис. 8. Сравнение экрана подтверждения оплаты ЖКХ и экрана подтверждения оплаты ЖКХ по шаблону в формате JSON.

На рисунке выше видно, что в JSON добавились два дополнительных элемента: 

  • MARKDOWN — поле для отображения заголовка экрана;

  • DATA_VIEW — поле для отображения имени поставщика.

Но возникают другие вопросы: «Как backend-разработчику узнать, как правильно "сверстать" нужное поле? Как отдать верный кусок JSON’а?»

C этим как раз может помочь frontend-разработчик, использующий специальные приложения, которые были разработаны в банке. Главное — всем договориться заранее о контракте, который от компании к компании будет своим.

Так что же делает frontend’ер? Он собирает поле или даже целый экран через специальную платформу с sample’ами (она у нас называется AlfaUI), далее загружает его в приложение, которое администрирует все SDUI-шаблоны, а так как они хранятся в MongoDB, то backend-разработчик может спокойно «вытянуть» их себе через определенный API, заполнить нужными данными и отдать front’y.

Рассмотрим другой момент. Если в рамках реализации логики необходимо разработать зависимые элементы экрана (пример таких элементов отображен на рисунке 2), то помимо полей JSON также может содержать handler’ы. Handler состоит из следующих элементов:

  • Триггер: событие, на которое должен сработать handler. Триггер включает в себя id поля, от которого должно прийти событие на обновление, и тип события, по которому должен сработать handler.

  • Условие: логическое выражение, которое проверяет данные с форм. Handler выполняется, если условие истинно. При этом условие не является обязательным компонентом handler’а.

  • Операция: действие, которое должен выполнить handler.

В случае с примером, описанным выше (см. рис. 2), поле «Количество месяцев авансового платежа» отображается после переключения пользователем checkbox’а. То есть как раз на этом шаге помимо полей для ввода или отображения информации также приходит ряд handler’ов, которые описывают события на случай, если checkbox будет активирован. 

"handlers": [{
   "triggers": [{
      "sourceFieldId": "CHECKBOX",
      "eventTypes": [
         "CHANGE"
       ]}
   ],
   "condition": {
       "left": {
          "dynamic": {
              "type": "FIELD_VALUE",
              "fieldId": "CHECKBOX"
          }
       },
       "right": {
           "static": true
       },
       "type": "EQUALS"
   },
   "operations": [
      "type": "SHOW_FIELDS",
      "fieldIds": [
         "MONTHS_AMOUNT"
      ]
   ]
}]

Когда пользователь меняет состояние checkbox’а «Ввести авансовый платеж» (триггер CHANGE), отображается поле «Количество месяцев авансового платежа» (операция SHOW_FIELDS).


Возможно, сейчас у вас возник вопрос — «Почему в этой статье так мало рассказано про код?»

Потому что SDUI — это архитектурный шаблон, главной идеей которого является получение от backend’a не только данных, но и элементов экрана для заполнения этими данными. Поэтому вы, как backend-разработчик, при реализации нового функционала или доработке старого будете смотреть дизайн-макеты, читать документацию, работать с элементами, доступными в контракте, и… пытаться не умереть от скуки. Таким образом, backend-разработчик достаточное количество своего рабочего времени будет уделять именно визуальной части приложения, frontend’у. 

А как вам такой подход? Использовали ли вы его в своих проектах?

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


  1. Ab0cha
    19.12.2024 11:11

    По ощущениям sdui помогает ускорить разработку фичи или все таки его основной плюс в возможности релиза фичи без обновления приложения ?


    1. satun
      19.12.2024 11:11

      Его основной плюс отсутствие тз) овнер накидал картинок с дизайнером - делайте


  1. kitchip
    19.12.2024 11:11

    Когда специально пошёл в бекэнд чтобы по минимуму контактировать с фронтом...


  1. sv_kozlov
    19.12.2024 11:11

    осталось сделать еще один шаг и перейти к htmx


  1. 1q2w1q2w
    19.12.2024 11:11

    Интересно, Jmix считается sdui или нет?) а когда то и jsp и Django templates были популярными... но в целом понятно что sdui хорош для определенных задач где нужен контроль шагов с Бэка и динамическое управление логикой прописанной на бэкенде. Недавно натыкался на видео где рассказывали нечто похожее для создания сложной онлайн формы(кажется от vk)