Васильев Андрей Михайлович, 2022
Версии презентации
Процесс проектирования архитектуры приложения может быть как выделеным, когда происходит разработка новых подходов, так и постоянным, когда создаются отдельные файлы, функции и классы
Архитектура приложения — это комбинация выбранных подходов к формированию приложения, а также результирующая структура исходного кода
Обычно архитектура проявляется в подходе к разделению зон ответственности приложения на части
Приложение разделяется по сильно связанным функциям, для каждой функции выделяются отдельные части приложения
Любое достаточно сложное приложение содержит нетривиальную логику по модификации данных, эту логику логично отделить от технической части приложения, которая содержит сложные компоненты
Можно предложить следующий подход к разделению небольшого приложения на части:
.domain
.domain.storage
или .domain.database
.domain.operations
.public
.web
.web.filters
.web.handlers
.web.models
Вход — запрос от клиента, клиентом может выступать:
Выход — ответ сервера
Пользователь должен всегда получать корректный ответ, даже если составил некорректный запрос
Для каждого случая необходимо подготовить разумную стратегию обработки ошибок
fun interface Filter : (HttpHandler) -> HttpHandler
Фильтры позволяют выполнить действия, независимые от конкретного запроса:
Фильтр принимает в качестве аргумента HttpHandler и должен вернуть HttpHandler
val handler = { _: Request -> Response(OK) }
val myFilter = Filter {
next: HttpHandler -> {
request: Request ->
val start = System.currentTimeMillis()
val response = next(request)
val latency = System.currentTimeMillis() - start
println("I took $latency ms")
response
}
}
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth(
"my realm", "user", "password")
.then(myFilter)
val app: HttpHandler = latencyAndBasicAuth.then(handler)
Пакет org.http4k.filter содержит описание фильтров, поставляемых с помощью http4k
Подход к реализации:
Принципиальный подход к реализации
val errorFilter: Filter = { next: HttpHandler ->
{ request: Request ->
val response = next(request)
if (response.successfull) {
response
} else {
response.body(renderer(ErrorMessage(request)))
}
}
}
val system = errorFilter.then(app)
Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:
Подходы к обработке данной ситуации:
Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение сообщения, что искомый элемент не был найден
fun handler(renderer: TemplateRenderer): HttpHandler = handler@{ request ->
val paramemter: String? = request.path("parameter")?.toIntOrNull()
if (parameter == null) {
return@handler Response(UNSATISFIABLE_PARAMETERS).body(
renderer(EmptyParameterRequestFoundVM())
)
}
val entity: Entity? = getEntityByData(parameter)
if (entity == null) {
return@handler Response(NOT_FOUND).body(
renderer(EntityNotFoundVM())
)
}
Response(OK).body(renderer(ShowEntityVM(entity)))
}
Или с помощью составления цепочки по обработке данных, которые могут
стать null
:
fun handler(renderer: Templaterenderer): HttpHandler = { request ->
request
.path("parameter")
?.let { parameter ->
getEntityByParameter(parameter)
}?.let { entity ->
Response(OK).body(renderer(ShowEntityVM(entity)))
} ?: Response(NOT_FOUND).body(renderer(EntityNotFoundVM()))
}
Или путём обработки данной ситуации на уровне шаблона Pebble
Разрешаем null-состояние для модели
data class ShowEntityVM(entity: Entity?)
Передаём неопределённое состояние в модель
fun handler(renderer: Templaterenderer): HttpHandler = { request ->
val entity request
.path("parameter")
?.let { parameter ->
getEntityByParameter(parameter)
}
Response(OK).body(renderer(ShowEntityVM(entity)))
}
Проверка на null в шаблоне Pebble:
{% if model.entity is null %}
...
{% else %}
...
{% endif %}
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
val structure = listOf(
mapOf(
"lang" to "kotlin",
"name" to "Веб-программирование",
)
)
Для редактирования структуры необходимо сначала получить доступ к элементу массива, а затем уже к свойству словаря
Для сильно вложенной структуры данных цепочка может быть гораздо длиннее. Это несёт следующие проблемы:
Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства
Предположим, что мы создали линзу для получения переменной из пути:
val parameterLens = Path.of("parameter")
Для получения параметра:
val parameter:String = parameterLens(request)
Потенциально для установки значения можно было бы написать:
val newRequest = parameterLens("newData", request)
Но смысла в операции записи на запросе нет
Объект | Начальный тип данных | Применимо для объектов | Несколько | Ограничитель требований |
---|---|---|---|---|
Параметры запроса | String | Request | Один или много | Обязательный или необязательный |
Заголовок | String | Request / Response | Один или много | Обязательный или необязательный |
Переменная пути | String | Request | Один | Обязательный |
Поле формы | String | WebForm | Один или много | Обязательный или необязательный |
Тело | String | Request / Response | Один | Обязательный |
При создании линзы необходимо указать:
Данный этап можно пропустить, если необходима строка, однако лучше всегда явно указывать тип для преобразования
http4k предлагает большой набор функций расширения, с помощью которых можно настраивать преобразование данных. Рассмотрим список для типа Query
val request = Request(GET, Uri.of("http://server.ru"))
val requestWithQuery = request.query("data", "42")
val queryDataLens = Query.int().defaulted("data", 15)
val data = queryDataLens(requestWithQuery) // 42
Уровни необходимости параметра описываются в рамках класса LensSpec
Ключевое отличие — поведение при отсутствии целевого значения
Предыдущий рассмотренный сценарий обработки форм не удовлетворяет требованиям по обработке данных:
HTML форма представляет собой набор полей, которые пользователь заполняет
В рамках http4k используются следующие типы данных:
val ageField = FormField.int().required("age")
val nameField = FormField.map(::Name, Name::value).optional("name")
val feedbackFormBody = Body.webForm(Validator.Feedback, nameField, ageField).toLens()
val invalidRequest = Request(GET, "/")
.with(Header.CONTENT_TYPE of ContentType.APPLICATION_FORM_URLENCODED)
val invalidForm = feedbackFormBody(invalidRequest)
println(invalidForm.errors)
val validForm = strictFormBody(validRequest)
val age = ageField(validForm)
Удобно использовать объекты класса WebForm в обоих случаях
data class WebForm constructor(
val fields: Map<String, List<String>> = emptyMap(),
val errors: List<Failure> = emptyList()
)
Для проверки наличия данных в форме можно использовать следующие конструкции:
{% if model.form.errors is not empty %}
Присутствуют ошибки
{% endif %}
{% if model.form.fields contains "field" %}
Поле присутствует: {{ model.form.fields["field"] | first }}
{% endif %}
Для уменьшения количества повторений внутри шаблонов Pebble можно воспользоваться макросами, которые можно несколько раз вызывать внутри шаблона
{% macro input(type="text", name, placeholder="", form) %}
<input
type="{{ type }}"
name="{{ name }}"
placeholder="{{ placeholder }}"
{% if form.fields contains name %}
value="{{ form.fields[name] | first }}"
{% endif %}
>
{% endmacro %}
{{
input(name="count", type="number", placeholder="Число работников",
form=model.form)
}}
Согласно протоколу HTTP серверу желательно сообщать клиенту тип документа с помощью MIME типов. Также поступает и клиент
Для указания типа документа используется заголовок Content-Type
В http4k для решения этой задачи можно либо воспользоваться фильтрами:
val SetHtmlContentType = Filter { next ->
{ next(it).with(CONTENT_TYPE of TEXT_HTML) }
}
val app = routes(
return "/create" bind POST to SetHtmlContentType.then someHandler
)
Либо использовать линзы:
val renderer = PebbleTemplates().HotReload("src/main/resources")
val htmlView = Body.viewModel(renderer, TEXT_HTML).toLens()
val handler: HttpHandler {
Response(OK).with(htmlView of SomeViewModel(42))
}