Введение
Проектирование современной системы онлайн-банкинга представляет собой довольно сложную задачу. При этом ряд задач разработки клиентской части приложения связан с процессом обработки большого количества данных, поступающих практически одновременно из нескольких источников информации. Данные от системы дистанционного банковского обслуживания (ДБО), служб мгновенных сообщений, различных информационных сервисов должны приниматься и обрабатываться в режиме реального времени здесь и сейчас. Для решения задач подобного рода сегодня широко применяются методы реактивного программирования.
Термин «Реактивное программирование» в широком смысле означает такую организацию работы приложения, при которой распространение изменений в системе происходит в результате обработки состояний потоков данных. Важные вопросы при этом методе — это простота представления потоков информации и возможность реакции на ошибки, возникающие в процессе асинхронной обработки результатов представления.
В узком смысле реактивное программирование веб UI может означать использование готовых инструментов разработчика, например, библиотеки RxJs. Данная библиотека обеспечивает дискретное представление последовательностей данных с использованием объекта Observable, который служит источником информации, поступающей в приложение через определенные промежутки времени.
Рассмотрим особенности использования библиотеки на примере проектирования веб-интерфейса онлайн-банка для малого бизнеса. При разработке UI нами была использована платформа Angular 6 компании Google со встроенной библиотекой RxJs версии 6.
Задачи проектирования реактивного UI
Для пользователя выполнение большинства операций в интернет-банке зачастую сводится к трем стадиям:
- выбор необходимой операции из списка, например, погашение кредита или пополнение счета;
- частичное заполнение соответствующей формы (реквизиты платежа заполняются автоматически по вводимому пользователем названию организации или фамилии получателя платежа);
- автоматизированное подтверждение операции с использованием смс-сообщения или электронной подписи.
С позиций разработчика реализация перечисленных стадий включает решение следующих задач:
- проверка состояния системы ДБО, обеспечивающая актуальность данных об операциях в списке;
- асинхронная обработка потоков данных при заполнении формы, включая данные, вводимые пользователем и получаемые от сервисов информационных сообщений (наименование, ИНН и БИК банка, например);
- валидация заполненной формы;
- автоматическое сохранение данных в форме.
Проверка состояния системы ДБО
Процесс получения актуальных данных от системы ДБО, например, информации о кредитной линии или статусе платежного поручения, включает две стадии:
- проверку статуса готовности данных;
- получение обновленных данных.
Для проверки текущего состояния данных производят запросы к АПИ системы с определенным промежутком времени и до получения ответа о готовности данных
Возможно, несколько вариантов ответов системы ДБО:
- { empty: true } — данные еще не готовы;
- обновленные данные могут быть получены клиентом;
{
empty: false
// какие-то другие свойства
}
- ошибка.
В результате получение актуальных данных производится в виде:
const MIN_TIME = 2000;
const MAX_TIME = 60000;
const EXP_BASE = 1.4;
request() // первый запрос без задержки
.pipe(
expand((response, index) => {
const delayTime = Math.min(MIN_TIME * Math.pow(EXP_BASE, index), MAX_TIME);
return response.empty ? request().pipe(delay(delayTime)) : EMPTY;
}),
last()
)
.subscribe((response) => {
/** какие-то действия */
});
Разберем пошагово:
- Отправляем запрос. request()
- Ответ переходит в expand. Expand — это оператор RxJS, который рекурсивно повторяет код в рамках своего блока на каждое оповещение next для внутреннего и внешнего Observable, пока поток не сообщит о своем успешном завершении. Поэтому, чтобы завершить поток, нужно вернуть такой Observable, чтобы не было ни одного next — EMPTY.
- Если в ответ пришел {empty: true}, то делаем повторный запрос через определенное время delay(delayTime). Чтобы не перегружать сервер запросами, увеличиваем время интервала у пинга с каждым новым запросом.
- Если в ходе очередного запроса пришло что-то иное в ответ, то прекращаем пинговать (возвращаем EMPTY) и отдаем результат последнего запроса подписчику (оператор last()).
- После получения ответа берем результат и обрабатываем. В сабскрайб попадет объект вида:
{
empty: false
// какие-то другие свойства
}
Реактивные формы
Рассмотрим задачу проектирования реактивной веб-формы платежного документа с использованием библиотеки ReactiveForms из состава фреймворка Angular.
Три базовых класса библиотеки FormControl, FormGroup и FormArray позволяют использовать декларативное описание полей формы, задавать начальные значения полей, а также устанавливать валидационные правила для каждого поля:
this.myForm = new FormGroup({
name: new FormControl('', Validators.required), // определено пустое текстовое поле с проверкой на пустые значения
surname: new FormControl('')
});
Для форм с большим количеством полей принято использовать сервис FormBuilder, позволяющий создавать их с применением более компактного кода
this.myForm = this.fb.group({
name: ['', Validators.required],
surname: ''
});
После создания формы в шаблоне страницы платежного поручения достаточно указать ссылку на форму myForm, а также имена ее полей name и surname
<form [formGroup]="myForm">
<label>Name:
<input formControlName="name">
</label>
<label>Surname:
<input formControlName="surname">
</label>
</form>
Полученная конструкция позволяет формировать и отслеживать любые потоки информации, проходящей через поля формы как в результате ввода данных пользователем, так и на основе бизнес-логики приложения. Для этого достаточно подписаться на события, генерируемые асинхронным наблюдателем формы ValueChanges
this.myForm.valueChanges
.subscribe(value => {
… // код обработки значений формы
})
Предположим, бизнес-логика определяет требования по автоматическому заполнению реквизитов адресата платежа при вводе пользователем ИНН получателя или наименования организации. Код обработки данных, вводимых пользователем в поля ИНН/наименование организации, будет иметь вид:
this.payForm.valueChanges
.pipe(
mergeMap(value => this.getRequisites(value)) // функция запроса реквизитов через внешний сервис
)
.subscribe(requisites => {
this.patchFormByRequisites(requisites) // функция обновления полей формы с платежными реквизитами
})
Валидация
Валидаторы бывают двух видов:
- синхронные;
- асинхронные.
С синхронными валидаторами сталкиваемся регулярно — это функции, которые проверяют введенные данные при работе с полем. В терминах реактивных форм:
«Синхронный валидатор — это функции, которые принимают control формы и возвращают truthy-значение, если есть ошибка и falsy в противном случае.»
function customValidator(control) {
return isInvalid(control.value) ? {
code: "mistake",
message: "smth wents wrong"
} : null;
}
Реализуем валидатор, который проверит, указал ли пользователь в форме серию документа, если ранее в качестве типа документа, удостоверяющего личность, был указан паспорт:
function requredSeria(control) {
const docType = control.parent.get("docType");
let error = null;
if (docType && docType.value === "passport" && !control.value) {
error = {
code: "wrongSeria",
message: "Укажите серию документа"
}
}
return error;
}
Здесь также обращаемся к родительской форме и с помощью нее получаем значение другого поля. В качестве ошибки можно было вернуть просто true, но в данном случае было решено поступить иначе. Эти сообщения ошибок можно перехватить в поле errors элемента управления или формы. Если у поля несколько валидаторов, можно точно указать, какой из валидаторов не прошел, чтобы отобразить нужное сообщение об ошибке или скорректировать валидацию других полей.
Валидатор в форму будет добавлен следующим образом:
this.documentForm = this.fb.group({
docType: ['', Validators.required],
seria: ['', requredSeria],
number: ''
});
Из коробки также доступно несколько часто встречающихся валидаторов. Все они представлены статическими методами класса Validators. Также есть методы для композиции валидаторов.
Некорректность одного поля ведет сразу же к невалидности всей формы. Это можно использовать в случае, когда нужно деактивировать некую кнопку ОК, если в форме есть хотя бы одно невалидное поле. Тогда все сводится к проверке одного условия “myform.invalid”, которое вернет true, если форма невалидна.
У асинхронного валидатора есть одно отличие — тип возвращаемого значения. Значение truthy или falsy должно быть передано в промисе или в Observable.
У каждого контрола или у каждой формы есть статус (mySuperForm.status), который может быть “VALID”, “INVALID”, “DISABLED”. Поскольку при использовании асинхронных валидаторов может быть непонятно в каком состоянии в данный момент форма, есть особый статус “PENDING”. Благодаря этому условию (mySuperForm.status === “PENDING”) можно отобразить прелоадер или сделать любую иную стилизацию формы.
Автоматическое сохранение
Разработка банковского программного обеспечения (ПО) подразумевает работу с различными типовыми документами. Например, это формы-заявления или анкеты, которые могут состоять из десятков обязательных полей. При работе с такими объемными документами для дополнительного удобства пользователя требуется поддержка автосохранения, чтобы при потере соединения с интернетом или иных технических проблемах данные, которые пользователь вводил ранее, остались сохраненными на сервере в черновом варианте.
Приведем основные аспекты процедуры автосохранения для клиент-серверной архитектуры:
- Запросы на сохранение должны быть обработаны сервером в порядке, в котором производились изменения. Если на каждое изменение сразу посылать запрос, то нельзя гарантировать, что более ранний запрос не придет следом и не перезапишет новые изменения.
- Не нужно отправлять на сервер большое количество запросов, пока пользователь не закончил ввод, достаточно делать это по таймингу.
- Если было сделано несколько изменений с относительно большой задержкой, а запрос на первые изменения еще не вернулся, то нет необходимости посылать запросы на каждое последующее изменение сразу по возвращению первого запроса. Можно взять только последний, чтобы не отсылать неактуальные данные.
С первым кейсом можно с легкостью справиться с помощью оператора concatMap. Второй кейс без проблем решится с помощью debounceTime. Логику третьего можно описать в виде:
const lastRequest$ = new BehaviorSubject(null); // Последний запрос
queue$.subscribe(lastRequest$);
queue$
.pipe(
debounceTime(1000),
exaustMap(request$ => request$.pipe( // Пропускаем запросы, пока выполняем текущий запрос
map(response => ({request$, response})), // Соединяем отправленный запрос с его результатом
catchError(() => of(null) // Игнорирование ошибок
)
)
.subscribe(({request$, response}) => {
if (lastRequest$.value !== request$) {
queue$.next(lastRequest$.value); // Повторяем последний пропущенный запрос
}
});
Осталось в saveQueue$ отправить запрос. Отметим присутствие оператора exaustMap вместо concatMap. Данный оператор необходим для игнорирования всех нотификаций внешнего Observable, пока внутренний не завершил свое наблюдение («закомплитился»). Но в нашем случае если во время запроса будет очередь новых нотификаций, мы должны взять последний, а остальные отбросить. exaustMap отбросит все, в том числе и последний. Поэтому сохраняем последнюю нотификацию в BehaviorSubject, а в подписке, в случае если текущий отработанный запрос отличается от последнего — кидаем последний запрос в очередь заново.
Также стоит отметить игнорирование ошибок в ходе запросов, реализованное с помощью оператора catchError. Можно написать более сложную обработку ошибок с выводом уведомлений для пользователя, что при сохранении произошла ошибка. Но его суть в том, что при возникновении ошибки в потоке, не должно произойти закрытие потока, как это происходит при оповещениях error и complete.
Заключение
Сегодняшний уровень развития технологий реактивного программирования с использованием библиотеки RxJS позволяют создавать полноценные клиентские приложения для систем онлайн-банкинга без дополнительных трудозатрат на организацию взаимодействия с высоконагруженными интерфейсами систем ДБО.
Первое знакомство с RxJS может отпугнуть даже опытного разработчика, столкнувшегося с “хитросплетениями” библиотеки, реализующими шаблон проектирования “Наблюдатель”. Но, возможно, преодолев эти трудности, в дальнейшем RxJS станет незаменимым инструментом при решении задач асинхронной обработки потоков разнородных данных в режиме реального времени.