Использование параметров запроса #
Васильев Андрей Михайлович, 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-сервер, он выполняет следующие проверки:
- Переданы ли параметры в запросе от пользователя
- Являются ли переданные данные технически корректными (в строке записано число)
Реализация обработки #
Подходы к обработке параметров, приходящих в запросах от пользователя:
- Обработать запросы внутри обработчика, используя низкоуровневые интерфейсы
- Реализовать собственную подсистему для обработки параметров для уменьшения дублирования между разными запросами:
- Реализовать подсистему полностью самостоятельно
- Интегрировать стороннюю библиотеку с примитивами 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")
Если не удалить старые, то вместо замены будет добавлена новая пара
Работа с большим объёмом информации #
При работе с большим объёмом информации возникает ряд проблем:
- Весь объём данных для отображения слишком большой
- Сервер будет долго формировать и обрабатывать весь набор данных
- Клиентскому приложению надо будет эти данные получить (и в плохом случае оплатить мобильный трафик)
- Клиентскому приложению надо данные отобразить пользователю удобным образом
- Сервер будет долго формировать и обрабатывать весь набор данных
- Системе в большинстве случаев не выгодно предоставлять все свои данные
Для обеспечения удобного доступа к большому объёму данных пользователю предоставляются инструменты для выполнения:
- фильтрации
- сортировки
- постраничного ввода информации
Фильтрация и сортировка сервером #
- Фильтрация — выбор элементов из списка согласно какому-то свойству
- По конкретному значению (хочу 10k-телевизор)
- По границам возможных значений (до 10 т.р.)
- Сортировка — расположение элементов в списке согласно свойству
- По дате доставки (хочу вчера)
Обработка GET-запроса #
- Сервер определяет список параметров
- Сервер должен информировать клиента, если параметры не содержат нужных данных
- Разработчик клиента должен знать список известных серверу параметров
Постраничный вывод информации #
Даже после выполнения фильтрации объём данных скорее всего слишком большой для передачи пользователю, да и сервер не хочет отдавать все свои данные
Общепринятое решение — передача данных блоками, страницами
- При выполнения запросов без параметров отдаётся первая страница
- В HTTP-запросе могут быть указаны параметры для получения отдельной страницы
Обработчик запроса должен учитывать все параметры: сначала выполнить фильтрацию, затем отобразить нужную страницу
Параметры постраничного вывода обычно являются необязательными со значением по умолчанию
Идентификация элементов в списке #
При выполнении фильтрации и сортировки порядок элементов в массиве скорее всего не будет соответствовать изначальному:
- Данными для отображения будет массив из элементов с порядковыми номерами 64, 44 и 8 в оригинальном массиве
- В выдаче-списке необходимо давать возможность найти полную информацию
- Порядковый номер в массиве не может являться надёжным средством идентификации
- Уникальный идентификационный номер должен являться частью элемента
Обеспечение уникальности #
В приложении появляется потребность в классе-хранилище, элементов, который
- Обеспечит быстрый доступ к элементу по его внутреннему номеру
- Обеспечит уникальность внутренних идентификаторов элементов в системе
class TriangleStorage() {
private val triangles: ...
fun add(triangle: Triangle): Int { ... }
fun get(id: Int): Triangle? { ... }
}
При добавлении нового элемента в хранилище либо
- У него устанавливается новое значение уникального идентификатора
- Запрещается добавление элемента, если у него не уникальный номер
Хранилище может предоставлять функции для получения всех элементов в виде списка, для выполнения операций фильтрации и т.д.
Обработка запроса от пользователя #
Обработку запроса от пользователя можно разделить на три логических этапа, следующих один за другим:
- Техническая обработка данных, пришедших от пользователя. Данные приходят в формате строк, необходимо их преобразовать к типам слоя предметной области
- Выполнение обработки извлечённых данных в слое предметной области: поиск значений, добавление значений, изменение значений и т.д.
- Формирование ответа пользователю в формате, который он ожидает
В рамках обработки HTTP-запроса может быть выполнено несколько обращений к каждому из слоя извлечения данных и слоя предметной области
Рекомендуется каждый этап оформлять внутри отдельного компонента, а задачей HTTP-обработчика становится их логическое объединение для достижения задачи
Проблема большого класса-хранилища #
В рамках HTTP-обработчика необходимо иметь доступ к хранилищу данных, т.к. все действия приложения так или иначе сводятся к выборке или изменению набора данных
При использовании большого класса-хранилища
При написании теста обработчика ему необходимо предоставить тестовый дубль класса-хранилища, а может быть и не одного
Можно добавить промежуточный слой - классов операций
Если классы-операции предоставляют только один публичный метод, то в рамках теста их легко заместить тестовым дублем
Пример операции #
При создании операции можно удобно описать её интерфейс:
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
}