Всем привет! Ни для кого не секрет, что в мире программирования есть много приемов, практик и шаблонов программирования (проектирования), но зачастую, узнав что-то новое, совершенно не понятно, куда и как это новое применить.
Сегодня на примере создания небольшого модуля-обертки для работы с http запросами разберем реальную пользу каррирования — приема функционального программирования.
Всем новичкам и интересующимся применением функционального программирования на практике — welcome, те, кто прекрасно понимают что такое каррирование — жду ваших комментариев по коду, ибо как говорится — нет предела совершенству.
Итак, начнем
Но не с понятия каррирования, а с постановки задачи, где его мы сможем применить.
У нас есть некое API блога, работающее по следующему принципу (все совпадения с реальными API являются случайностью):
- запрос к
/api/v1/index/
вернет данные для главной страницы - запрос к
/api/v1/news/
вернет данные для страницы новостей - запрос к
/api/v1/articles/
вернет данные для списка статей - запрос к
/api/v1/article/222/
вернет страницу статьи с id 222 - запрос к
/api/v1/article/edit/222/
вернет форму редактирования статьи с id 222
… и так далее, далее, далее
Как видите, чтобы обратиться к API нам нужно обратиться к api определенной версии v1 (мало-ли оно разрастется и выйдет новая версия) и после этого дальше конструировать запрос данных.
Следовательно, в js коде, для получения данных, например одной статьи с id 222 мы должны написать (для максимального упрощения примера, используем нативный js метод fetch):
fetch('/api/v1/article/222/')
.then(/* success */)
.catch(/* error */)
Чтобы отредактировать эту же статью, запросим так:
fetch('/api/v1/article/edit/222/')
.then(/* success */)
.catch(/* error */)
Наверняка вы уже заметили, что в наших запросах очень много повторяющихся путей. Например путь и версия до нашего API /api/v1/
, и работа с одной статьей /api/v1/article/
и /api/v1/article/edit/
.
Следуя нашему любимому правилу DRY (Don't Repeat Yourself), как оптимизировать код запросов к API?
Мы можем добавить части запросов в константы, например:
const API = '/api'
const VERSION = '/v1'
const ARTICLE = `${API}${VERSION}/article`
И теперь можем переписать примеры выше таким образом:
Запрос статьи
fetch(`${ARTICLE}/222/`)
Запрос редактирования статьи
fetch(`${ARTICLE}/edit/222/`)
Кода стало вроде бы меньше, появились константы, относящиеся к API, но мы с вами знаем, что можно сделать намного удобнее.
Верю, что есть еще варианты решения задачи, но наша задача рассмотреть решение при помощи каррирования.
Принцип построения запросов на основе http сервисов
Стратегия заключается в том, чтобы создать некую функцию, вызывая которую мы будем конструировать запросы к API.
Как это должно работать
Мы конструируем запрос, вызывая функцию-обертку над нативным fetch (назовем ее http. Ниже выложен полный код этой функции), в аргументах которой передаем параметры запроса:
cosnt httpToArticleId222 = http({
url: '/api/v1/article/222/',
method: 'POST'
})
Обратите внимание, результатом выполнения данной функции http будет функция, внутри которой содержатся настройки запроса url и method.
Теперь, вызвав httpToArticleId222()
мы собственно отправляем запрос к API.
Можно поступить хитрее, и поэтапно конструировать запросы. Тем самым мы можем создать набор из готовых функций с "зашитыми" путями к API. Их мы и назовем http сервисами.
Итак, первое, конструируем сервис обращения к API (попутно добавляя параметры запроса, неизменные для всех последующих запросов, например метод)
const httpAPI = http({
url: '/api',
method: 'POST'
})
Теперь создаем сервис обращения к API именно первой версии. В дальнейшем мы сможем от сервиса httpAPI создать отдельную ветку запросов к другой версии API.
const httpAPIv1 = httpAPI({
url: '/v1'
})
Сервис обращения к API первой версии готов. Теперь от него создадим сервисы для остальных данных (вспомним импровизированный список в начале статьи)
Данные главной страницы
const httpAPIv1Main = httpAPIv1({
url: '/index'
})
Данные страницы новостей
const httpAPIv1News = httpAPIv1({
url: '/news'
})
Данные списка статей
const httpAPIv1Articles = httpAPIv1({
url: '/articles'
})
Наконец подходим к нашему основному примеру, данные для материала
const httpAPIv1Article = httpAPIv1({
url: '/article'
})
Как получить путь до редактирования статьи? Вы конечно догадались, догружаем данные, созданной ранее функции httpAPIv1Article
const httpAPIv1ArticleEdit = httpAPIv1({
url: '/edit'
})
Небольшой логический итог
Итак, у нас есть красивый список сервисов, которые, например лежат в каком-то отдельном файле, который нас совершенно не беспокоит. Если что-то нужно будет изменить в запросе, я точно знаю, где править.
export {
httpAPIv1Main,
httpAPIv1News,
httpAPIv1Articles,
httpAPIv1Article,
httpAPIv1ArticleEdit
}
Делаю импорт сервиса с определенной функцией
import { httpAPIv1Article } from 'services'
И выполняю запрос, сначала доконструируя его, добавлением id материала, и тут же вызываю функцию для отправки запроса (как говориться: "изи")
httpAPIv1Article({
url: ArticleID // id получен где-то в коде
})()
.then(/* success */)
.catch(/* error */)
Чисто, красиво, понятно (не реклама)
Как это работает
"Догружать" данными функцию мы можем именно благодаря каррированию.
Немного теории.
Каррирование — способ конструирования функции с возможностью постепенного применения ее аргументов. Достигается путем возвращения функции, после ее вызова.
Классический пример — сложение.
У нас есть функция sum, первый раз вызывая которую, мы передаем первое число для последующего складывания. После ее вызова мы получаем новую функцию, ожидающую второе число для вычисления суммы. Вот ее код (ES6 синтаксис)
const sum = a => b => a + b
Вызываем первый раз (частичное применение) и сохраняем результат в переменную, например sum13
const sum13 = sum(13)
Теперь sum13 мы можем так же вызвать с недостающим числом, в аргументе, результатом вызова которой будет 13 + второй аргумент
sum13(7) // => результат 20
Хорошо, как это применить к нашей задаче?
Создаем функцию http, которая и будет оберткой над fetch
function http (paramUser) {}
где paramUser — параметры запроса, передаваемые в момент вызова функции
Начнем дополнять нашу функцию логикой
Добавим параметры запроса, заданные по-умолчанию.
function http (paramUser) {
/**
* Параметры по-умолчанию, при первом запуска
* @type {string}
*/
let param = {
method: 'GET',
credentials: 'same-origin'
}
}
И следом функцию paramGen, генерирующую параметры запроса из тех, что заданы по-умолчанию и пользовательский (по факту просто мержинг двух объектов)
function http (paramUser) {
/**
* Параметры по-умолчанию, при первом запуска
* @type {string}
*/
let param = {
method: 'GET',
credentials: 'same-origin'
}
/**
* Генератор параметров запроса,
* поле url он суммирует, таким образом мы можем дозаполнять его при разных вызовах
*
* @param {object} param начальные параметры
* @param {object} paramUser параметры, которыми догружаем начальные
*
* @return {object} возвращаем новый объект параметров
*/
function paramGen (param, paramUser) {
let url = param.url || ''
let newParam = Object.assign({}, param, paramUser)
url += paramUser.url || ''
newParam.url = url
return newParam
}
}
Переходим к самому главному, описываем каррирование
Поможет нам в этом функция, названная, к примеру, fabric и возвращаемая функцией http
function http (paramUser) {
/**
* Параметры по-умолчанию, при первом запуска
* @type {string}
*/
let param = {
method: 'GET',
credentials: 'same-origin'
}
/**
* Генератор параметров запроса,
* поле url он суммирует, таким образом мы можем дозаполнять его при разных вызовах
*
* @param {object} param начальные параметры
* @param {object} paramUser параметры, которыми догружаем начальные
*
* @return {object} возвращаем новый объект параметров
*/
function paramGen (param, paramUser) {
let url = param.url || ''
url += paramUser.url || ''
let newParam = Object.assign({}, param, paramUser);
newParam.url = url
return newParam
}
/**
* Фабричная функция, возвращающая новый инстанс модуля
* вызывая ее, можно тем самым конфигурировать запросы
*
* Обратите внимание:
*
* - если не передать аргументы, то функция начнет запрос данных по тем параметрам, что у нее есть
* - если передать строку, то это будет автоматически интерпритироваться как добавление в строку запроса
* - если передать объект, то он будет конфигурировать запрос
*
* @param {object} param параметры, сохраненные ранее и переданные через каррирование
* @param {object} paramUser параметры, переданные пользователем
*
* @return {function || promise} возвращаем каррированную функцию, либо промис запроса (fetch), в зависимости от аргумента
*/
function fabric (param, paramUser) {
if (paramUser) {
if (typeof paramUser === 'string') {
return fabric.bind(null, paramGen(param, {
url: paramUser
}))
}
return fabric.bind(null, paramGen(param, paramUser))
} else {
// я знаю, что вы скажете, что в param уходит и бесполезный url,
// пусть решение будет вашим домашним заданием :)
return fetch(param.url, param)
}
}
return fabric.bind(null, paramGen(param, paramUser))
}
При первом вызове функции http возвращается функция fabric, с переданными в нее (и сконфигурированными функцией paramGen) параметрами param, которая будет ожидать своего часа вызова в дальнейшем.
Например конфигурируем запрос
let httpGift = http({
url: '//omozon.ru/givemegift/'
})
И вызывая httpGift, применяются переданные параметры, в результате возвращаем fetch, если же мы хотим доконфигурировать запрос, то просто передаем новые параметры в сформированную функцию httpGift и ожидаем ее вызова без аргументов
httpGift()
.then(/* success */)
.catch(/* error */)
Итоги
Благодаря применению каррирования при разработке различных модулей, мы можем достигать высокой гибкости в применении модулей и удобству тестирования. Как, например, при организации архитиктуры сервисов для работы с API.
Мы словно создаем мини-библиотеку, используя инструменты которой создаем единую инфраструктуру нашего приложения.
Надеюсь информация была полезной, сильно не бейте, это моя первая статья в жизни :)
Всем компилируемого кода, до встречи!
Комментарии (11)
Yeah
08.09.2018 14:03+2import { httpAPIv1Article } from 'services'
И тут выходит API v2 и мы меняем все вызовы по всему проекту?
Serge78rus
08.09.2018 15:36Смена мажорной версии API подразумевает отсутствие обратной совместимости и, если не замену, то просмотр всех вызовов к нему. В случае же смены лишь минорной версии, замена будет представлять из себя что-то вроде замены строки
наimport { httpAPIv1Article } from 'services_v1_0'
import { httpAPIv1Article } from 'services_v1_1'
nanomen Автор
10.09.2018 10:15Да, в этом-то и дело, что изменяя версию API, мы можем подключить обновленный модуль, не меняя при этом обработчики данных (если конечно структура API кардинально не меняется)
Bhudh
08.09.2018 15:27+2вызывая httpGift, применяются переданные параметры
Что такое «вызывая применяются»?
«Подъезжая к сией станцыи и глядя на природу в окно, у меня слетела шляпа»?
Aries_ua
08.09.2018 21:04+1А создать слой API с объектами и методами в них для получения/отправки данных, и в settings вынести «настройкой» версию API и списки url уже считается плохим тоном?
Не поймите не правильно, но сопровождать запутанный код всегда сложно. Я когда разрабатываю приложение, то стараюсь к минимуму свести связанность. И конкретно для API делаю так, что бы можно было что либо поменять в настройках, не затрагивая приложение.VolCh
09.09.2018 13:52Это решение выглядит как реализация слоя API и не исключает конфигурирования дополнительного.
nanomen Автор
10.09.2018 10:21Я бы сказал, что это лишь наглядный пример пользы каррирования на примере работы с API. Тут как всегда, под каждую задачу (команду) могут быть найдены разные оптимальные решения, это одно из.
koldyr
Опять смешаны кадрирование и частичное применение.
koldyr
Упс, видимо с первой попытки не так понял, возражение снимается.