Pull to refresh
81.76
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Особенности использования библиотеки RxJs в системе онлайн-банкинга

Reading time8 min
Views5.4K

Введение


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

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

В узком смысле реактивное программирование веб 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) => {
       /** какие-то действия */
  });


Разберем пошагово:

  1. Отправляем запрос. request()
  2. Ответ переходит в expand. Expand — это оператор RxJS, который рекурсивно повторяет код в рамках своего блока на каждое оповещение next для внутреннего и внешнего Observable, пока поток не сообщит о своем успешном завершении. Поэтому, чтобы завершить поток, нужно вернуть такой Observable, чтобы не было ни одного next — EMPTY.
  3. Если в ответ пришел {empty: true}, то делаем повторный запрос через определенное время delay(delayTime). Чтобы не перегружать сервер запросами, увеличиваем время интервала у пинга с каждым новым запросом.
  4. Если в ходе очередного запроса пришло что-то иное в ответ, то прекращаем пинговать (возвращаем EMPTY) и отдаем результат последнего запроса подписчику (оператор last()).
  5. После получения ответа берем результат и обрабатываем. В сабскрайб попадет объект вида:

{
    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”) можно отобразить прелоадер или сделать любую иную стилизацию формы.

Автоматическое сохранение


Разработка банковского программного обеспечения (ПО) подразумевает работу с различными типовыми документами. Например, это формы-заявления или анкеты, которые могут состоять из десятков обязательных полей. При работе с такими объемными документами для дополнительного удобства пользователя требуется поддержка автосохранения, чтобы при потере соединения с интернетом или иных технических проблемах данные, которые пользователь вводил ранее, остались сохраненными на сервере в черновом варианте.

Приведем основные аспекты процедуры автосохранения для клиент-серверной архитектуры:

  1. Запросы на сохранение должны быть обработаны сервером в порядке, в котором производились изменения. Если на каждое изменение сразу посылать запрос, то нельзя гарантировать, что более ранний запрос не придет следом и не перезапишет новые изменения.
  2. Не нужно отправлять на сервер большое количество запросов, пока пользователь не закончил ввод, достаточно делать это по таймингу.
  3. Если было сделано несколько изменений с относительно большой задержкой, а запрос на первые изменения еще не вернулся, то нет необходимости посылать запросы на каждое последующее изменение сразу по возвращению первого запроса. Можно взять только последний, чтобы не отсылать неактуальные данные.

С первым кейсом можно с легкостью справиться с помощью оператора 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 станет незаменимым инструментом при решении задач асинхронной обработки потоков разнородных данных в режиме реального времени.
Tags:
Hubs:
+5
Comments0

Articles

Information

Website
www.simbirsoft.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия