Использование параметров запроса #

Васильев Андрей Михайлович, 2024

Версии презентации

Параметры в URI-запросах #

Структура URI

[ схема ":" ] [ // источник ] путь [ "?" запрос ] [ "#" фрагмент ]
  • Запрос начинается с обязательного символа ?, знака вопроса
  • В запросе данные могут быть отформатированы любым образом, однако обычно применяется схема с передачей набора параметров
    • параметр — это пара ключ-значение, разделённые знаком =
    • параметры отделяются друг от друга знаком &
http://some.domain/some/path?key1=value1&key2=value2&key3=value3
  • Передаётся 3 пары параметров с именами key1, key2 и key3
  • Значения параметра key1 — это value1, key2 — это value2, key3 — value3

Особенности параметров #

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

Однако есть важное отличие от словаря: клиент может передать несколько параметров с одинаковыми ключами и сервер должен обработать их все

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

Обработка параметров #

Логические типы параметров #

  • Обязательные. При их отсутствии сервер не может выполнить обработку запроса
  • Не обязательные. Их наличие или отсутствие не влияет на обработку запроса
  • Со значением по умолчанию. Если клиент не передал параметр, то сервер использует значение по умолчанию

При наличии ошибки в значении параметра сервер должен сообщить об ошибке

Дополнительные параметры, переданные от клиента, обычно игнорируются

Последовательность обработки #

Для всех параметров, которые ожидает HTTP-сервер, он выполняет следующие проверки:

  1. Переданы ли параметры в запросе от пользователя
  2. Являются ли переданные данные технически корректными (в строке записано число)

Реализация обработки #

Подходы к обработке параметров, приходящих в запросах от пользователя:

  • Обработать запросы внутри обработчика, используя низкоуровневые интерфейсы
  • Реализовать собственную подсистему для обработки параметров для уменьшения дублирования между разными запросами:
    • Реализовать подсистему полностью самостоятельно
    • Интегрировать стороннюю библиотеку с примитивами http4k
  • Использовать механизмы проверки данных, предоставляемые библиотекой http4k

Низкоуровневый доступ к параметрам #

  • В объекте запроса (Request) предоставляется поле uri, содержащее объект класса Uri
  • Класс Uri предоставляет следующие методы для работы с параметрами:
    • Свойство query содержит строку запроса полностью
    • Функция fun query(query: String): Uri позволяет создать новый объект Uri с новым значением запроса
    • Функция fun queries(): Parameters возвращает набор пар ключ-значения
    • Функция-расширение fun Uri.query(name: String, value: String?): Uri позволяет добавить новый параметр к новому Uri-объекту
    • Функция-расширение fun Uri.removeQuery(name: String): Uri позволяет удалить все параметры с указанным ключом, новое состояние сохраняется в возвращаемом объекте типа Uri
  • Класс Parameters представляет собой список параметров ключ-значение
    • Функция-расширение fun Parameters.findSingle(name: String): String? позволяет найти первый параметр с указанным именем
    • Функция-расширение fun Parameters.findMultiple(name: String): List<String?> позволяет найти все значения для параметра с указанным именем

Работа с классом URI #

Получение параметров из запроса #

val queryParameters: Parameters = request.uri.queries()
val maxPrise: String? = queryParameters.findSingle("maxPrise")
// Каждый вызов queries() приводит к разбору строки
val page: String? = request.uri.queries().findSingle("maxPrice")
  • Данные приходят (или не приходят) в строковом виде
  • Из строки-значения надо извлечь данные

Установление новых значений параметр запроса #

Для установки нового значения для параметра надо сначала удалить старые, а после добавить один новый

val newUri = request.uri.removeQuery("data").query("data", "value")

Если не удалить старые, то вместо замены будет добавлена новая пара

Работа с большим объёмом информации #

При работе с большим объёмом информации возникает ряд проблем:

  • Весь объём данных для отображения слишком большой
    • Сервер будет долго формировать и обрабатывать весь набор данных
      • Клиентскому приложению надо будет эти данные получить (и в плохом случае оплатить мобильный трафик)
    • Клиентскому приложению надо данные отобразить пользователю удобным образом
  • Системе в большинстве случаев не выгодно предоставлять все свои данные

Для обеспечения удобного доступа к большому объёму данных пользователю предоставляются инструменты для выполнения:

  • фильтрации
  • сортировки
  • постраничного ввода информации

Фильтрация и сортировка сервером #

diagram

  • Фильтрация — выбор элементов из списка согласно какому-то свойству
    • По конкретному значению (хочу 10k-телевизор)
    • По границам возможных значений (до 10 т.р.)
  • Сортировка — расположение элементов в списке согласно свойству
    • По дате доставки (хочу вчера)

Обработка GET-запроса #

diagram

  • Сервер определяет список параметров
  • Сервер должен информировать клиента, если параметры не содержат нужных данных
  • Разработчик клиента должен знать список известных серверу параметров

Постраничный вывод информации #

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

Общепринятое решение — передача данных блоками, страницами

  • При выполнения запросов без параметров отдаётся первая страница
  • В HTTP-запросе могут быть указаны параметры для получения отдельной страницы

Обработчик запроса должен учитывать все параметры: сначала выполнить фильтрацию, затем отобразить нужную страницу

Параметры постраничного вывода обычно являются необязательными со значением по умолчанию

Идентификация элементов в списке #

При выполнении фильтрации и сортировки порядок элементов в массиве скорее всего не будет соответствовать изначальному:

diagram

  • Данными для отображения будет массив из элементов с порядковыми номерами 64, 44 и 8 в оригинальном массиве
  • В выдаче-списке необходимо давать возможность найти полную информацию
  • Порядковый номер в массиве не может являться надёжным средством идентификации
  • Уникальный идентификационный номер должен являться частью элемента

Обеспечение уникальности #

В приложении появляется потребность в классе-хранилище, элементов, который

  • Обеспечит быстрый доступ к элементу по его внутреннему номеру
  • Обеспечит уникальность внутренних идентификаторов элементов в системе
class TriangleStorage() {
    private val triangles: ...

    fun add(triangle: Triangle): Int { ... }
    fun get(id: Int): Triangle? { ... }
}

При добавлении нового элемента в хранилище либо

  • У него устанавливается новое значение уникального идентификатора
  • Запрещается добавление элемента, если у него не уникальный номер

Хранилище может предоставлять функции для получения всех элементов в виде списка, для выполнения операций фильтрации и т.д.

Обработка запроса от пользователя #

diagram

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

  1. Техническая обработка данных, пришедших от пользователя. Данные приходят в формате строк, необходимо их преобразовать к типам слоя предметной области
  2. Выполнение обработки извлечённых данных в слое предметной области: поиск значений, добавление значений, изменение значений и т.д.
  3. Формирование ответа пользователю в формате, который он ожидает

В рамках обработки HTTP-запроса может быть выполнено несколько обращений к каждому из слоя извлечения данных и слоя предметной области

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

Проблема большого класса-хранилища #

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

При использовании большого класса-хранилища

diagram

При написании теста обработчика ему необходимо предоставить тестовый дубль класса-хранилища, а может быть и не одного

Можно добавить промежуточный слой - классов операций

diagram

Если классы-операции предоставляют только один публичный метод, то в рамках теста их легко заместить тестовым дублем

Пример операции #

При создании операции можно удобно описать её интерфейс:

interface GetTriangleOperation {
    fun get(id: Int): Triangle?
}
class GetTriangleOperationImpl(
    private val storage: TriangleStorage
) : GetTriangleOperation {
    override fun get(id: Int) = storage.get(id)
}

Внутри HTTP-обработчика реализовать получение объекта, реализующего интерфейс:

class ShowTriangleHandler(
    getTriangleOperation: GetTriangleOperation
) : HttpHandler { ... }

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

class ReturnTriangle(private val triangle: Triangle?)
        : GetTriangleOperation {
    override fun get(id: Int) = triangle
}