Как можно отделить бизнес-логику от вызовов API.

Мотивация и обзор

Нашей целью было автоматизировать определенные запросы API, используя определенный язык запросов. Вместо прямого запроса API отправляется запрос, содержащий вызовы методов. Каждый метод может обрабатываться напрямую или делегироваться другому экземпляру. Если метод делегирован, часть запроса, содержащая вызов метода, будет отправлена ​​другому экземпляру. Ключевой особенностью подхода является возможность разбивать запрос на куски и выполнять каждый кусок отдельно, как другой запрос. Каждый из таких фрагментов получает один преобразователь. Резолверы назначаются чанкам автоматически на основе делегированных методов. Запрос также может содержать присваивание переменных, условия и операторы возврата, что позволяет нам использовать базовую логику в запросах. Мы разработали экспериментальную версию JavaScript, библиотека называется limbo и доступна на npm.

Как выглядят эти запросы?

Запрос состоит из строк, каждая строка должна заканчиваться на ;. Строки могут обрабатывать данные, используя определенный набор операторов. Данные могут быть представлены в виде примитивов (строки, числа, логические значения), массивов или объектов. Для определения массивов и объектов мы используем формат JSON. Доступны следующие операторы:

  1. Вызовы методов. На самом деле, вызывая метод, можно отправить один параметр. Этот параметр может быть строкой, логическим значением, числом, массивом или объектом.
    methodName ~ {"key" : "val"};
  2. Условия. Выполняет подзапрос на основе условия. Подзапросы должны начинаться с @{; и заканчиваться на };
    ? $varName == "value" @{;
    $result = method ~ $varName;
    } : @{;
    $result = method2 ~ $varName;
    };
  3. Назначать. Присвоение значения переменной. Вот как мы можем сохранить данные для дальнейшего использования. Имя каждой переменной должно начинаться с «$».
    $varName = "value"
  4. Возвращаемое значение. Завершает запрос, возвращает значение.
    =>$reuslt;

Операторы внутри строки выполняются в порядке, указанном выше. Мы можем использовать скобки () для изменения порядка. Также мы должны использовать скобки при вызове операторов в определении объекта JSON:
=>{"key" : (method ~ "val")};

Пример:

$result = method ~ {"key" : "val"}; 
? $result.success == true @{;
    =>$result;
} : @{;
    writeErrorLog ~ $result.error;
    => {
        "success" : false, 
        "error" : "Error during calling /"method/""
   };
};

Методы управления и делегирования. Выполнение запросов

Один экземпляр может обрабатывать одни методы и делегировать другие. Если метод делегирован, обратный вызов получит фрагмент на основе делегированного метода. В противном случае обратный вызов получит только параметр, определенный в вызове метода, и имя метода.

делегирование

Чтобы делегировать метод, мы должны вызвать метод «делегата» экземпляра.

делегат(опции : объект) : void
options.regExp : RegExp. Будет делегирован метод, соответствующий регулярному выражению.
options.handle : Function(query : String). Функция обратного вызова.

Умение обращаться

Если мы хотим обработать метод текущим экземпляром, мы должны вызвать метод addHandler экземпляра.

addHandler(options : объект) : void
options.regExp : RegExp. Метод, соответствующий регулярному выражению, будет обработан текущим обработчиком.
options.handle : Function(param, methodName). Функция обратного вызова.

addHandlers(Handlers : Array): void
Добавляет несколько обработчиков.

Выполнение запросов

Чтобы выполнить запрос, мы вызываем метод call экземпляра.

call(query : String) : Promise
query : String. Запрос, который мы хотим выполнить.

Поток выполнения запроса

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

  1. Построить чанк
  2. Выполнить блок локально или отправить в другой экземпляр
  3. Результат процесса
  4. Перейти к пункту 1, если не выполнено

Разделение и назначение резольвера

Во время обработки запросы разбиваются на куски на основе делегированных методов. при разделении мы пытаемся уменьшить количество чанков и, как следствие, потенциальное количество запросов к API. Так как запросы могут содержать условия, может быть вложенный блок и необходимость обрабатывать случаи, когда чанк заканчивается таким вложенным блоком. Чтобы решить эту проблему, мы добавили еще один оператор ->@_bookmark, который возвращает закладку. Этот оператор ставится автоматически во время разбиения чанка. Если текущий чанк возвращает закладку, то дальнейшее выполнение кода должно начинаться со строки, указанной в закладке. Например:

$res1 = resolver1.method1 ~ "string";
? $res1.success == true @{;
    resolver1.method2 ~ "anotherString";
    $res2 = resolver2.method1 ~ $res1;
    ? res2.success @{;
        resolver1.method3 ~ $res2;
    };
    =>resolver2.method2 ~ $res2;
};
=>resolver1.method4 ~ $res1;

Для такого запроса распознаватель1 получит следующее:

$res1 = resolver1.method1 ~ "string";
? $res1.success == true @{;
    resolver1.method2 ~ "anotherString";
    ->@_0;
};
=>resolver1.method3 ~ $res1;

Затем, если $res1.success будет ложным, resolver2 никогда не будет вызываться. В противном случае получается:

$res2 = resolver2.method1 ~ $res1;
? res2.success == true @{;
    ->@_1;
};
=>resolver2.method2 ~ $res2;

А если $res2.success ложно, то решатель1 получит последний вызов:

resolver1.method3 ~ $res2;
=>resolver1.method4 ~ $res1;

$res1 и $res2 будут заменены фактическими данными.

Весь алгоритм разделения запроса и назначения резольвера выглядит так:

Случаи использования

  1. У нас есть клиент, который взаимодействует с несколькими API. Эти API представлены как конечные точки REST и/или GraphQL. Наш клиент в этом случае использует limbo как единую точку для получения данных из этих API и управления ими.

2. У нас есть сервер, который принимает запросы и обрабатывает их.

Эти варианты использования можно комбинировать. Один клиент может вызывать API, которые реализуют limbo, и те, которые этого не делают. В то же время серверы могут вызывать другие API и делегировать им запросы.

Выводы

Такой подход может хорошо подойти для микросервисной архитектуры, поскольку он отделяет бизнес-логику от вызовов API и позволяет выполнять один запрос несколькими экземплярами в зависимости от потребностей. Сами запросы не должны изменяться при наличии сервисов, что позволяет масштабировать решения.

Так как это всего лишь проверка концепции, еще есть место для улучшений, таких как:

  1. Петли. Теперь язык запросов не поддерживает циклы. Это будет исправлено в ближайшее время.
  2. Асинхронный поток выполнения запросов. Поскольку это только доказательство концепции, асинхронный поток не совсем оптимален (строки выполняются одна за другой, даже если они независимы). Мы собираемся разрешить параллельное выполнение нескольких строк или фрагментов.

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