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

    Введение


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

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

    В узком смысле реактивное программирование веб 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 станет незаменимым инструментом при решении задач асинхронной обработки потоков разнородных данных в режиме реального времени.
    SimbirSoft
    74,00
    Лидер в разработке современных ИТ-решений на заказ
    Поделиться публикацией

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое