Всем привет! Меня зовут Дмитрий Павлов, в компании Align Technology мы с коллегами занимаемся разработкой Web API для взаимодействия внутренних систем и интеграции нашей компании со сторонними вендорами. Об идеях создания API для веба, а точнее RESTful API я хотел бы рассказать в этой статье.
В последние годы тема Web API стала очень популярна, многие компании занимаются созданием подобных интерфейсов, как открытых, так и для внутреннего пользования. В описании Web API практически всегда можно встретить акроним REST, но что же это обозначает этот термин и правильно ли его используют?
REST или не REST?
Большинство разработчиков, особенно в России, понимает под REST программный интерфейс, работающий с использованием протокола HTTP[S] при условии соблюдения следующих свойств:
Сервер не хранит состояние клиента: никаких сессий, все что требуется для выполнения запроса клиент передаёт с самим запросом.
Человекочитаемые URL, в которых ресурсы идентифицируются отдельно. Никаких больше
/index.php?productId=1
, вместо этого используем/products/1
Более широкое использование HTTP методов: не ограничиваемся GET и POST, добавляем PUT и DELETE. В некоторых API можно встретить еще и PATCH.
- В качестве формата передачи данных используется JSON.
Алгоритм по которому такой API используется обычно стандартен. Для начала нужно пойти на сайт с документацией, найти страничку со списком URL-шаблонов для доступа к ресурсам. Обычно она выглядит так:
Список ресурсов для API "Рецепты печенек"
--- /recipes/cookies - список рецептов печенек,
GET на данный URL возвращает список доступных рецептов
[
{
"name" : "Овсяное печенье с шоколадом",
"rating" : 5,
"shortDescription" : "...."
}
]
POST на данный URL возволит вам создать новый рецепт. В качестве тела запроса ожидается json вида как
{
"name" : "Малиновое печенье",
"shortDescription" : "...."
......
}
--- /recipes/cookies/:name - рецепт печеньки с именем ${name}
{
"name" : "Овсяное печенье с шоколадом",
"rating" : 5,
"shortDescription" : "....",
"description" : "...."
"ingredients" : [
{
"name" : "Овсянка",
.....
},
{
"name" : "Масло",
.....
},
{
"name" : "Шоколад",
.....
}
],
"cookingSteps" : [
....
]
}
// остальные ресурсы гипотетического API с перечсилением HTTP методов и URL шаблонов
и изучив её выполнять запросы к ресурсам (что обычно выражается в написании клиента, который по заданным форматам URL'ов подставляет параметры и обрабатывает ответы).
Примеров таких API на просторах сети предостаточно, до недавнего времени у Яндекса многие API (раз, два) заявленные как REST работали по этой схеме.
Если мы обратимся к первоисточникам, т.е. к диссертации Роя Филдинга (на которую очень часто ссылаются, но гораздо реже читают), мы увидим, что API, созданные таким способом, не могут называться REST, поскольку они нарушают некоторые из принципов, описанных в диссертации, самый главный из которых — использование hypermedia как средства управления состоянием (Hypermedia As The Engine Of Application State ,HATEOAS), косвенно затрагивая вопросы само описываемых сообщений (self-descriptive messages).
Hypermedia в сообщениях
Суть HATEOAS состоит в подходе к описанию ресурсов нашего API. Вместо простого перечисления набора ресурсов, со списком всех возможных операций, которые клиент может вызвать, руководствуясь некоторой внутренней логикой, мы проводим инверсию контроля — теперь за состояние ресурса отвечает сервер и он диктует клиенту, какие операции над ресурсом можно совершить в текущий момент. Эта информация должна присутствовать в самом представлении ресурса, который получает клиент. Таким образом, представление ресурса само себя описывает в достаточной степени, чтобы клиент понял, что с ним можно делать.
Применение такого подхода обычно означает, что клиент знает некоторый конечный набор "точек входа" (можете считать их аналогами стартовых страниц на сайтах), с которых он начинает свое взаимодействие с API, используя предоставленную в представлении ресурса информацию для навигации к другим ресурсам и совершения действий.
Для достижения этой задачи как раз и используются гиперссылки (hypermedia):
- Все ресурсы адресуемы при помощи ссылок, причем ссылки на другие ресурсы присутствуют внутри самих сообщений для их связи между собой. Клиент вместо ориентации на формат URI руководствуется идентификаторами, по которым он выбирает ссылки, располагающиеся прямо в представлении ресурса. Если ранее мы указывали в документации что нужно взять некоторый ID и на его основе построить специальный URL, тем самым делая наборы URL частью нашего API, то теперь детали формирования URL являются просто особенностями реализации сервера и клиента не волнуют. В конце концов, клиенту важно получить доступ к ресурсу, а не генерировать URL по шаблонам из документации.
- Доступные операции над ресурсом тоже представимы в виде ссылок.
Отсутствие ссылки как на связанные ресурсы, так и на доступные действия означает, что данная операция недоступна в текущем состоянии ресурса.
Пример переработки API "Рецепты печенек" в Hypermedia представление
Возвращаясь к примеру с нашим API о каталоге рецептов печенек, преобразуем его в Hypermedia-вид.
Как вы помните, у нас был список рецептов и ресурс, подробно описывающий конкретный рецепт с перечислением ингредиентов и шагов по приготовлению. Вот как они будут выглядеть с использованием hypermedia подхода:
//список рецептов
{
"links": {
"self" "/recipes/cookies"
}
"items": [
{
"name": "Овсяное печенье с шоколадом",
"rating": 5,
"shortDescription": "...."
"links": {
"self": "/recipes/cookies/Овсяное печенье с шоколадом"
}
}
]
}
//конкретный рецепт
{
"links": {
"self": "/recipes/cookies/Овсяное печенье с шоколадом"
}
"name": "Овсяное печенье с шоколадом",
"rating": 5,
"shortDescription": "....",
"description": "...."
"ingredients": [
{
"name": "Овсянка",
.....
},
{
"name": "Масло",
.....
},
{
"name": "Шоколад",
.....
}
],
"cookingSteps": [
....
]
}
Значительное отличие от оригинальной версии заключается в появлении объекта links
внутри каждого ресурса. Ключи этого объекта представляют собой relation'ы (те самые идентификаторы), а значения — ссылки. В результате наши ресурсы не требуют дополнительной информации (вне самого ресурса) о том как же перейти из каталога рецептов к детальному описанию, ссылка встроена в представление ресурса.
Данные подход позволяет легко расширять функциональность нашего API. Предположим, что мы для каждого рецепта хотим предоставить клиенту набор рекомендаций, представимый в виде списка рецептов. Сделать это очень легко, достаточно добавить в наш объект links новый ключ:
"links" : {
"self" : "/recipes/cookies/Овсяное печенье с шоколадом",
"http://acme.com/recipes/rels/you-can-also-like" : "/recipes/cookies?related_to=Овсяное+печенье+с+шоколадом"
}
Аналогично, совершенно не составит труда добавить идентификацию ингредиентов как отдельных ресурсов, если в этом возникнет необходимость.
Содержание URI не играет никакой роли, ведь теперь элементом API является relation, и мы без каких либо изменений на клиенте можем поменять ссылку на /recipes/related-to/Овсяное печенье с шоколадом
или на /recipes/234892skfj45sdlkfjdsa12
Hypermedia на службе перемен
Hypermedia используется не только для навигации, но и для совершения действий, достаточно лишь определить, что некоторые relation отвечают за совершение определенных операций над ресурсами, а также обозначить семантику и детали этих операций.
Для наглядности рассмотрим пример с нашим API, добавив hypermedia-контрол для создания нового рецепта.
//список рецептов
{
"links" : {
"self" : "/recipes/cookies",
"http://acme.com/recipes/rels/add-recipe" : "/recipes/cookies"
}
"items" : [
.....
]
}
Мы лишь добавили ссылку со специальным relation'ом. Основное правило заключается в том, что клиент игнорирует неизвестные ему отношения: "старые" клиенты, не знающие как добавлять новый рецепт, будут работать как раньше, а для тех кто поддерживает создание, это будет сигналом, что есть возможность добавления нового рецепта путем отправки запроса на URI, который указан в отношении http://acme.com/recipes/rels/add-recipe
.
Данный подход позволяет нам не описывать статический набор операций и условия их выполнения в документации, а непосредственно серверу контролировать какие операции клиент может совершать над ресурсом в данный момент времени, а какие — нет. Добавление новых действий тоже не представляет сложности: мы просто объявляем новый relation, и начинаем включать его в представление ресурса, которое формирует сервер.
Разумеется, предоставление ссылок не снимает с сервера ответсвенности за корректное оперирование HTTP методами и соблюдения их семантики :).
А как же relation?
У вас к данному моменту наверняка возник вопрос: какой смысл затевать все это, если для эффективной работы клиент все равно должен понимать смысл relation'ов? По ним-то документация должна иметься.
В самом деле, для эффективной работы клиент действительно должен понимать, что значит каждое отношение. Основная идея, стоящая за заменой интерпретации URI на работу с relation'ами, состоит в большей долговечности последних. URI является деталью реализации и может меняться со временем или от сервера к серверу. Relation же представляет собой семантическое описание связи и не завязан на детали хранения.
Предположим, я хочу сделать совместимый API для хранения рецептов, но из-за особенностей хранения хочу идентифицировать каждый рецепт по UUID, а не по названию. В случае с оригинальным API сделать это невозможно, а для hypermedia API это совершенно незаметно для клиента.
В результате появляется возможность создания более универсальных клиентов менее подверженных изменениям в случае модификаций на сервере.
Hypermedia types или почему application/json нам не подходит
Решив воспользоваться преимуществами Hypermedia подхода, мы модифицировали наш API указанным выше способом, и теперь у нас ресурсы связаны друг с другом по ссылке. С первого взгляда может показаться, что с нашим API все в порядке, но перед тем как заявить, что у нас Hypermedia API, посмотрим на заголовок Content-Type, возвращаемый нами в ответах. Если там стоит application/json
или даже text/plain
, то нам еще предстоит потрудиться.
Ресурсы глазами машины
Глядя на получившиеся у нас ресурсы, человек сразу выделяют ссылки, что создает впечатление о корректном формате нашего сообщения. Мы делаем вывод об этом, анализируя содержимое сообщения, тогда так стандарт предписывает смотреть на Content-Type заголовок ответа.
Рассмотрим следующий ответ сервера:
200 OK
Content-Type: text/plain
<?xml version="1.0"?>
<hello>world</hello>
Нам очевидно, что в ответе содержится xml-документ, но Content-Type предписывает воспринимать содержимое как простой текст, поэтому то, что он похож на xml-документ может быть просто совпадением или частным случаем. Именно поэтому верный Content-Type так важен.
Давайте разбираться, чем же для нашей задачи не подойдет application/json
? Дело в том, что стандарт, описывающий этот тип, не предусматривает никакого места или механизма для определения ссылок в нем. И даже если сформированное нами сообщения содержит ссылки, то машина не может отличить их от строки, в которой содержится текст по формату напоминающий ссылку. Нам же нужно однозначно определить, где в сообщении ссылка, а где нет, поэтому нам нужен другой тип.
Vendor specific типы
Одним из способов решить проблему корректности Content-Type'а — использовать свой собственный. В документации мы явно укажем, где у нас в сообщении расположены ссылки. Если клиент получил от сервера ответ с нашем личным Content-Type'ом, ему не нужно будет динамически угадывать, что ссылка а что нет, если конечно он понимает наш Content-Type. Стоит отметить, что зачастую документация с описанием типа содержит не только подробности самого формата (т.е. где расположены ссылки, а где свойства), но и другую информацию:
- семантическое описание свойств, т.е. что они обозначают с точки зрения бизнес логики;
- детали взаимодействия клиента с сервером, такие как HTTP методы необходимые для отправки запроса.
Такие типы называются vendor specific, поскольку часто создаются под конкретную задачу и конкретной организацией. Их нет необходимости регистрировать в IANA. Рекомендуется давать им название вида application/vnd.${vendor}+${base_format}
, где ${vendor}
— это перевернутый домен компании, ${base_format}
— тип который мы взяли за основу. Если компания имеет домен acme.com и для представления наших ресурсов мы используем json, то для нашего API рецептов название типа будет выглядеть как application/vnd.com.acme.recipes+json
.
Hypermedia типы общего назначения
На первый взгляд, vendor specific типы решают возникшую проблему со ссылками, но у них есть и свои проблемы:
- типы не совместимы между собой, поэтому клиентам приходится поддерживать много разных реализаций, в случае если они взаимодействуют не с одним API, т.е. требуется поддержка отдельной библиотеки для разбора формата каждого типа, выделения в нем свойств, ссылок и прочего;
- создание vendor specific типа под каждую задачу ведет к очень заметному росту их общего числа.
В качестве альтернативы не заставил себя ждать новый подход, который принесли типы общего назначения. Если подумать, то все что нам нужно от формата сообщений — это спецификация, отвечающая на вопросы:
- как найти свойства наших ресурсов,
- как найти hypermedia контролы внутри ресурса.
Именно эта задача и решается: тип общего назначения не пытается подстроиться под конкретную доменную область, ими можно описать большинство ресурсов, с которыми мы имеем дело.
Важной особенностью всех типов общего назначения является то, что они не ставят задачу семантического описания документа, т.е. они не говорят, что же это за ресурс — описание рецепта или комментарий в блоге, это не их задача. Они отвечают больше за детали формата, оставляя семантическую спецификацию за рамками. Предполагается, что семантика будет заключена в так называемом профиле — отдельном документе, описывающим семантику свойств и отношений (relations).
На данный момент существует уже достаточно большое количество подобных форматов, поэтому перечислим лишь некоторые из них:
application/hal+json
— один из первых появившихся и наиболее популярный формат в наши дни;application/vnd.siren+json
;application/mason+json
.
В описании всех таких форматов вы найдете, как и куда помещать свойства ресурса, в каком виде оформлять ссылки на другие ресурсы.
Различаются они форматом и возможностями, которые содержатся в самом типе.
Разница в форматах или принципиальность создателей
Большинство типов общего назначения отличается незначительными деталями, например, как форматировать ссылки. Так, в HAL ссылки выглядят следующим образом:
"_links" : {
"self" : ....
"relToResource": .....
}
Тогда как Siren представляет их так:
"links" : [
{"rel" : ["self"], "href" : "...."},
{"rel" : ["relToResource"], "href" : "...."}
]
Основное отличие здесь в представлении relation значений. Создатель HAL стремился сделать формат более лаконичным, в то время как создатель Siren — более полным: relation у ссылки действительно может быть сложным (поэтому в Siren это массив значений), но это не всегда используется (поэтому в HAL это скаляр, да еще и ключ в объекте).
Такие вот разные взгляды и привели к созданию разных форматов, об одном формате договориться не смогли.
Различие в возможностях
Не будем здесь перечислять все различия в форматах, обозначим только основные, на примере уже упомянутых типов:
- В HAL не существует понятия действий как отдельной сущности, только ссылки и метод, который нужно использовать для отправки запроса тоже не включен в сообщение. В Siren и Mason в наличии инструменты по описанию форм: сообщение содержит параметры, которые требуется для ввода клиентом, метод, которым нужно все это отправить и Content-Type.
- HAL имеет отдельное понятие формы поиска — безопасного, идемпотентного действия для получения информации с сервера. Siren и Mason, как уже говорилось выше, обладает возможностью описать любое действие.
- HAL и Siren не содержат в спецификации деталей по описанию ошибок, это остаётся на усмотрение пользователя формата (можно использовать
application/vnd.error+json
), тогда как в Mason этот аспект включен в формат.
Generic vs vendor specific
Какой же вариант предпочтительнее: специально созданный тип или один из существующих вариантов? Как обычно бывает с подобными вопросами, однозначного ответа на него нет, все зависит от обстоятельств использования, поэтому попробуем выделить преимущества и недостатки каждого из них.
Одно из главных преимуществ hypermedia-типа общего назначения — экономия времени вам и клиентам вашего API. Вот за счет чего она достигается:
- Формат сообщений уже придуман за вас. Вы можете сосредоточиться на решении вашей проблемы создавая API в уже установленных ограничениях выбранного формата.
- Для создания и разбора сообщений уже написаны библиотеки под разные языки программирования. Согласитесь, удобно подключить готовую библиотеку и работать с высокоуровневыми понятиями вида ссылка, действие, свойства объекта, а не с деталями путей в JSON.
- Для многих generic-типов существуют так называемые браузеры. Это простые клиенты, которые понимают элементы определенного типа, и динамически формируют веб интерфейс, позволяя вам сразу демонстрировать созданный API, не потратив и минуты на создание клиента.
- Вашим клиентам потребуется только изучать документацию по вашему API, т.е. они смогут сосредоточиться только на бизнес-смысле, не тратя время на чтение деталей формата.
В то же время часть преимуществ такого подхода могут для кого-то выглядеть недостатками. В силу того, что тип не завязан под какую-либо доменную область и задачу, представление ресурсов получается более "раздутым", по сравнению со специальным типом, который мы могли бы создать.
В итоге для большинства задач можно рекомендовать использовать один из имеющихся Hypermedia-форматов общего назначения по умолчанию и делать выбор в пользу vendor-форматов в сложных или специфических случаях (если вы, конечно, не ставите целью vendor lock-in).
Насколько мне нужно все это?
Описанный подход не является очередной серебряной пулей, призванной решить все проблемы при разработке API.
Можно отметить, что концепция точек входа способна привести к росту числа запросов, чтобы "добраться" до нужного ресурса и что включая ссылки мы делаем сообщение более объемным по сравнению с голыми данными.
На эти недостатки можно возразить, что эти проблемы решаются продуманной структурой ресурсов (кто мешает сделать операции поиска ресурса в точке входа для быстрой навигации?), кэшированием, которое тоже отмечено Филдингом как важная компонента этого архитектурного подхода, и банальным включением компрессии на веб серверах.
Основной плюс REST-подхода (здесь я имею ввиду полноценный REST) в гибкости и расширяемости, который он предоставляет, позволяя нам добавлять новые возможности или просто менять организацию ресурсов у себя на сервере без нарушения работы существующих клиентов.
Даже если вы решите не использовать hypermedia в вашем API, теперь вы знаете, что без нее REST — это не REST, а просто Web API. Это не делает API плохим или хорошим, я просто констатирую факт. Главное не забывать, что API мы делаем не ради самого API, а для решения задач, стоящих перед нами :).
Примеры на просторах сети
- API Яндекс Диска — пример hypermedia API, использующий HAL.
- Github — использует свой формат (на основе JSON), который тем не менее является hypermedia.
- Paypal тоже использует свой формат сообщений.
- Foxycart — пример API имеющего представление в нескольких форматах — HAL и Siren. Также имеет замечательное
представление документации (к сожалению, технология генерирования таких документов не opensource)
Комментарии (81)
aratak
08.04.2016 12:31+2Три вопроса:
Клиенту все еще нужно догадываться, что запрос на
add-recipe
нужно делать постом а не путом или патчем? Почему бы не указывать метод явно? Например:
"links": { "add-recipe": { "href": "http://example.com/recipes", "method": "POST" }, "recent-recipe": { "href": "http://example.com/recipes/my-recipe", "method": "GET" } }
Почему бы значение 'href' не указывать абсолютной ссылкой? Таким образом можно абстрагироваться от синглтон-домена и мы легко можем заменить домен в случае разнесения функциональности по разным доменам-субдоменам.
А можем и не заменить.
"add-recipe": { "href": "http://example.com/recipes", "method": "POST" }
"add-recipe": { "href": "http://mycompany.example.com/recipes", "method": "POST" }
- Совершенно не понял почему ключом выступает ссылка? (в ваших примерах это "http://acme.com/recipes/rels/you-can-also-like"). Невнимательно прочитал?
Спасибо.
zeldigas
08.04.2016 14:33- Тут вопрос дизайна и предпочтений. Этот пример показывал как можно было бы сделать свой, ни на что не похожий hypermedia тип (
application/vnd.com.acme.recipes+json
). У авторов такого типа, в его документации было бы описано как создавать рецепты, какой метод нужен. Дизайн странный, но используя свой Content-Type они вполне это могут сделать, хотя ваш вариант мне нравится больше :).
Даже у авторов generic-типов нет консенсуса на этот счет. В HAL методов нет, в Siren есть. Альтернативой документирования методов, или указания их в ресурсе может являться ссылка на профиль, такой ресурс с мета-информацией. - Вполне допустимы оба варианта, дело вкуса. У нас в проектах для "локальных" ресурсов мы опускаем домен, а для связи нескольких микросервисов уже указываем полный линк. Клиенты поддерживают оба формата, так что для них все прозрачно. Все как со ссылками на веб страницах.
Скорее я не раскрыл этот момент. Если надеть шляпу формалиста и бюрократа, то все "простые" link relation'ы нужно регистрировать в IANA. Эта организация ведет список общеупотребимых идентификаторов, таких как
self
,prev
,next
. Поэтому используя url, мы вводим namespace чтобы точно не пересечься семантически с другими доменами на просторах сети. Следовать этому подходу или нет — зависит от обстоятельств. Есть варианты когда создатели забивают на это, и используют короткие имена, еще один подход использование префиксов, напримерacme:add-recipe
. Тот же HAL подерживает curies раздел, где задается описания этих сокращений, и ресолв до полного url'a. Вот например прим с сайта HAL:
"_links": { "curies": [ { "name": "doc", "href": "http://haltalk.herokuapp.com/docs/{rel}", "templated": true } ], "doc:latest-posts": { "href": "/posts/latest" } }
- Тут вопрос дизайна и предпочтений. Этот пример показывал как можно было бы сделать свой, ни на что не похожий hypermedia тип (
el777
08.04.2016 12:51+5Оно, конечно, интересно.
Но почему-то, чем дальше тем больше это начинает напоминать SOAP.JC_Piligrim
08.04.2016 18:34Вот-вот. Помню как с десяток лет назад вся эта движуха за REST проходила под флагами «долой RPC, клиент должен быть простым». А сейчас, похоже, к тому самому RPC+WSDL+толстый клиент всё и возвращается. :)
el777
10.04.2016 21:43+2Да, все тоже самое, только в более кривом исполнении в виде 10 несовместимых стандартов:
XPath — уже придумали аналог JsonPath
XMLSchema — Json Schema
валидация, ссылки друг на друга, метаданные и пр.
Даже JOLT вместо XSLT придумали.
После этого хочется спросить — «ну и чем вам угловые скобочки не понравились»?
Мы сейчас получим все это же, только не в виде стройной структуры, которая была в XML-технологиях, а в виде самодеятельного несовместимого зоопарка?napa3um
11.04.2016 00:25REST — это ресурсы и отношения вместо объектов и методов на RPC. Мы получим стандартизованный протокол, поддерживающий стаднартизованную архитектуру (т.е., нужно будет просто понять REST, и принимать решений и делиться ими в команде при проектировании/реализации придётся гораздо меньше). Выбор между XML и JSON в REST вообще никак не принципиален.
vaxXxa
08.04.2016 14:23+2Могу посоветовать еще глянуть в сторону JSON API http://jsonapi.org/.
zeldigas
08.04.2016 14:40-3Спасибо за ссылку, посмотрим.
Это кстати характерный пример, когда человек делает обзор имеющихся форматов, ему ничего не нравится и он создает свой. Отличная иллюстрация — https://xkcd.com/927/
saDam
08.04.2016 14:25+1Оставлю здесь для любимых .net разрабов и не только:
API implementation guidance
Одна из лучших статей.
mikhailt
08.04.2016 14:41+1Как уже отметили, для всяких сложностей, например последовательных запросов по доморощенному протоколу, дуплекс, дозвон — callback и т.п. — есть SOAP.
zeldigas
08.04.2016 14:45Я не думаю, что вопрос выбора инструмента SOAP или REST определяется сложностью задачи. Кстати, сравнивать конкретную технологию с архитектурным принципом не вполне корректно.
Кстати в отличной книге Майка Амундсена Restful Web APIs упоминается CoAP — протокол для простых электронных устройств, использующий REST принципы. Я не буду развивать эту тему, так как не специалист в этой области, но стоит отметить. что не все упирается в http.
SBKarr
08.04.2016 15:54Как возможный формат можно рассмотреть, например, CBOR. С использованием тегов клиент без проблем сможет определять ссылки и прочую метаинформацию. Другой вопрос, что он машинный, а не человекочитаемый. Но ведь проблема именно в том, чтобы научить машину понимать, а человек и сам из JSON ссылки выделит.
ZerGer
09.04.2016 09:26"… Предположим, что мы для каждого рецепта хотим предоставить клиенту набор рекомендаций … "
Мне кажется, что обычно клиент решает свою задачу, и стороннее API подключается по принципу «подключил и забыл…». Странно ожидать от него, что он будет следить за рекомендациями.zeldigas
09.04.2016 13:24+1API бывают разные. Те которые вы, скорее всего, имели ввиду — делаются для решения определенной задачи. Такому клиенту действительно ничего не нужно его захардкодили и все.
Но этим же мир клиентов не ограничивается. Можно заморочиться и сделать более умного клиента или сделать клиента который используется интерактивно. Самый наглядный пример — HAL или Siren браузер который может работать с любым API. Еще пример — робот гугла который ходит по сайтам и распознаёт микроформаты в html разметке.
Если не залезать так высоко в абстракции, вот вам более простой пример: оформление заказа. Инвойс к заказу вы получите только после оплаты. Не гипермедиа вариант — сразу все ресурсы описать в документации, но тогда вам нужно клиенту как-то дать понять есть уже инвойс или нет. А так есть линка
invoice
в ресурсе "Заказ" есть и инвойс, нет значит еще не готов.
vintage
09.04.2016 11:45-1За использование PUT и DELETE нужно руки отрывать :-)
DELETE нарушает связность, когда пройдя по ссылке можно получить 404.
PUT не дружит с совместным редактированием. Либо дружит, но через дополнительные методы LOCK и UNLOCK.
Так что хороший апи должен поддерживать следующие методы: GET, HEAD, POST, PATCH.
А в некоторых случаях (когда идентификаторы выбираются клиентом) и POST не нужен. Яркий пример — вики.zeldigas
09.04.2016 13:05+2Пожалейте руки создателей многих API, включая Amazon S3, а также кучи других :)
Если серьезно, то конечно мнение крайне субъективное. Зачем винить молоток что вы ударили себя по пальцам? Если вам нужна параллельная работа и ваши ресурсы не позволяют это сделать, то вопрос скорее к дизайну ресурсов, чем к методам из спецификации.
За использование PUT и DELETE нужно руки отрывать :-)
А в некоторых случаях (когда идентификаторы выбираются клиентом) и POST не нужен
Налицо логическое противоречие: и это вам не нравится и то вам неправильно. Надеюсь вы не GET'ом собираетесь ресурсы создавать?
vintage
09.04.2016 14:10Если серьезно, то конечно мнение крайне субъективное.
Я привёл аргументы.
Зачем винить молоток что вы ударили себя по пальцам?
Я использую саморезы и не имею такой проблемы.
Если вам нужна параллельная работа и ваши ресурсы не позволяют это сделать, то вопрос скорее к дизайну ресурсов, чем к методам из спецификации.
Правильный дизайн ресурсов не позволяет работать с ними через метод PUT по вышеозначенной причине.
Налицо логическое противоречие: и это вам не нравится и то вам неправильно.
Тут нет противоречия.
Касательно POST — попробуйте реализовать API без него и поймёте, как это удобно. Если вкратце:
1. Идентификатор ресурса у вас есть самого начала, что избавляет от необходимости вводить временные идентификаторы до первого сохранения, от которых очень много проблем с клиентской стороны.
2. Все запросы у вас идемпотентные и вы можете без опаски любой из них повторять.
Но это весьма не «традиционный» путь.
Надеюсь вы не GET'ом собираетесь ресурсы создавать?
Нет.zeldigas
09.04.2016 17:16Касательно POST — попробуйте реализовать API без него и поймёте, как это удобно.
В своем первом комментарии вы заявили, что PUT и DELETE не использовать (отрывание рук — сомнительное руководство к дейстивю), сейчас заявляете что POST тоже не использовать.
Внимание вопрос — какой метод протокола HTTP вы используете для создания ресурса?
vintage
09.04.2016 18:46Я это в первом же комментарии и заявил.
PATCH, очевидно. Например, создание статьи про апи:
PATCH /user=jin/article=api
{ «title»: «API», «description»: "...", «content»: "..." }
200 OK
{ «author»: "/user=jin", «created»: «2016-04-09T18:45:00Z», «updated»: «2016-04-09T18:45:00Z» }zeldigas
09.04.2016 20:23PATCH не идемпотентный метод в широком смысле, в RFC посвященном ему об этом говорится страница 3, второй абзац
В любом случае, удачи вам. Если ваш API решает поставленные перед ним задачи, это прекрасно!
napa3um
09.04.2016 14:24+1HATEOAS — один из принципов REST, предписывающий ресурсу нести в себе информацию об отношениях с другими ресурсами. HAL — язык для описания таких отношений. Помимо этого принципа REST ограничивает архитектуру в количестве действий, которые в принципе возможны над любым ресурсом — у GET, POST, PUT, PATCH, DELETE строго определена семантика и способ их обработки сервером (и стратегия кеширования результатов). Именно потому в HAL не указываются методы запроса, этот не перечень RPC-действий, которые можно совершить с ресурсом, а перечень REST-ресурсов, имеющих отношение к данному. А действия над всеми ресурсами всегда одинаковые.
Отличие RPC/SOAP от REST не в сложности/простоте реализации, а в семантике. RPC — это когда прикладная задача моделируется объектами и методами, а REST — это когда задача моделируется ресурсами и отношениями между ними. Эти способы взаимовыразимы друг через друга (REST можно построить на базе RPC и наоборот), но не одно и то же.zeldigas
09.04.2016 17:24Посмотрите RFC 7231, в частности метод POST. Там четко написано, что семантику определяет сам ресурс, который обрабатывает запрос. В связи с чем если у ресурса сложное поведение, через POST можно сделать разные действия. Понимаю что можно разное поведение можно реализовать и через PUT, но у него более четкая семантика с точки зрения HTTP.
Вы еще забыли важный момент в отличии RPC от REST — в RPC все методы определны заранее, это действительно похоже на сигнатуру класса. В REST при помощи гипермедиа контролов (их наличия и отсутсвия) сервер как владелец ресурса определяет что может делать клиент в текущий момент.
napa3um
09.04.2016 17:36Семантику определяет сам ресурс, верно. Определяет семантику в рамках ограничений, накладываемых парадигмой REST, в которых POST — это именно конкретный глагол, создающий ресурс.
Не забыл. Этот момент непринципиален в том различии REST и RPC, о котором я говорил. Но можно его считать целью этих различий (некой практической выгодой архитектуры).zeldigas
09.04.2016 20:20Почитайте пожалуйста первый параграф по ссылке выше. Там простым английским языком написано, что в общем случае семантика POST'а не определена и ресурс может такой запрос обрабатывать как хочет не нарушая спецификацию.
Вы правы что в большинстве своём он применяется для создания чего-либо, но не нужно воспринимать частый, но все же конкретный пример использования за семантику определенную в стандарте.
zeldigas
09.04.2016 20:24Ссылочки выше нет :). Вот здесь актуальная спецификация: https://tools.ietf.org/html/rfc7231#section-4.3.3. Вам первый параграф.
napa3um
09.04.2016 20:42REST возник позже HTTP, хотя и является формализацией его целей. HTTP != REST.
zeldigas
10.04.2016 18:56REST — это архитектурный принцип, HTTP — протокол. Почитайте внимательно диссертацию Филдинга.
napa3um
10.04.2016 19:24А вы почитайте, пожалуйста, мои комментарии. Я где-то пытался назвать REST протоколом? Сформулируйте почётче, чему хотите возразить в моих словах.
zeldigas
13.04.2016 11:43Я согласен что REST это про ресурсы, а не про набор заранее определенных вызовов, как в случае с RPC.
Но я не совсем согласен со следующими моментами:
Семантика методов. В первом комментарии вы сказали что "GET, POST, PUT, PATCH, DELETE строго определена семантика".
- Это так для всего кроме POST. Как я указывал ранее, в актуальной спецификации HTTP у post'а семантику определяет сам ресурс и REST тут ничего не меняет, потому что REST не меняет ограничений протокола.
Я не спорю что много задач можно решить чисто CRUD подходом, вводя самоограничение, что POST мы используем только для создания ресурса, но не стоит обобщать это на все API и на протокол HTTP как таковой. REST, как принцип, нас совсем не ограничивает — он предписывает серверу сообщать клиенту состояние ресурса (и что с ним можно делать) и не нарушать семантику протокола который используется между ними используя его возможности по назначению. Указывать явно или не указывать метод для выполнения действия уже вопрос реализации.
"HAL — язык описания отношений".
- Вообще это формат представления ресурса у которого в спеке четко прописано где находятся ссылки. И форматов подобных ему достаточно много. Отношения описывают линки, семантика которых задаётся relation'ом. Тут эти форматы ничего не изобретают, используя уже известные конструкции из html'я и atom фидов.
- То что там нет методов не истина в последней инстанции, а просто виденье его создателя — об этом Майк Келли лично говорил на конфе API Craft в 2014 году.
napa3um
13.04.2016 11:511. Нет, REST нас ограничивает, вы просто не поняли этих ограничений, похоже. Если мы хотим в духе RPC ввести новую операцию над объектом, мы её реализовываем новым ресурсом со стандартными операциями над ним. Например, мы хотим реализовать у объекта «Ракета» метод «Запустить», тогда в концепции REST мы, например, создадим у ресурса «Ракета» вложенный ресурс «Запуск». Т.е., методом POST создавая экземпляр ресурса «Ракета/:id/Запуск» мы и производим запуск ракеты.
2. Не могу оспорить вашего мнения. Оно отвалится самостоятельно, когда (если) примите то, что я описал в первом пункте.vintage
13.04.2016 15:40Почему вы меняете состояние ресурса созданием нового виртуального ресурса?
REST — он не о действиях, а о состояниях.
PATCH /ракета(123) target name =London 200 OK target name =London pos 35.2213 12.4367 state =flying
napa3um
13.04.2016 15:58Не понял вопроса. Я просто с потолка привёл пример, как семантика «действие над объектом, выходящее за пределы сематики HTTP-глаголов» может переводиться на семантику «действие над ресурсом в рамках сематики HTTP-глаголов». Да, ваш вариант тоже возможен, его отличие от моего примера уже не упирается в различие RPC и REST, а упирается в другие критерии реализации приложения. В пределе можно вообще всю структуру БД спроецировать в один ресурс REST-API (чтобы избавиться от всех вложенностей ресурсов, которую вы называете виртуальностью) с которым клиент будет работать исключительно патчами. Усложнив себе жизнь с управлением доступами ко всему этому добру и почти полностью отказавшись от возможности кеширования.
vintage
13.04.2016 16:09Что-то вас бросает из одной крайности в другую :-)
napa3um
13.04.2016 16:20Чему вы хотите возразить, кроме моей манеры пояснять свои мысли утрированными примерами?
Да, возможно, слово «например» в моих словах недостаточно чётко указывало, что я привожу лишь один из вариантов реализации. Т.е., если пришла мысль добавить новое действие к ресурсу, то это в REST будет передачей нового состояния — либо «виртуальный» вложенный ресурс, либо «виртуальное» дополнительное поле в ресурсе, но никак не «виртуальная» семантика глагола. Да, ему нужно думать состояниями, а не действиями. Может, такая формулировка кому-то действительно будет удобнее, чтобы испытать просветление. Но, мне казалось, что эту сторону вопроса уже обсудили выше, и затруднения вызывает именно проблема отсутствия глаголов в HAL, отчего и начал развиваться диалог. Безусловно, всегда можно найти, чем мои слова можно дополнить. Не обязательно это делать в форме возражения.vintage
13.04.2016 16:44Что примеры нужно приводить корректные, чтобы у читателей не возникало неправильного представления об обсуждаемом вопросе. Типичное заблуждение — переносить глаголы из хттп-метода в урл и использование метода post. Это рест формально, но не по духу.
napa3um
13.04.2016 16:51Это не заблуждение, а пример. Исключительно в контексте диалога. Представьте, что ракета в прикладной задаче многоразовая, запуски логируются, и к этим логам разграничивается доступ по ролям. Теперь мой пример стал красивее вашего? Ваше «заблуждение» я тоже показал, утрировав ваш пример.
Не нужно вообще переносить глаголы куда-то, нужно сразу проектировать в духе REST. Декомпозиция на ресурсы зависит от прикладной области и ограничений реализации (что, как и где будет обрабатываться, как контролироваться). И не нужно спорить ради спора (либо сформулируйте, пожалуйста, компактным тезисом суть вашего возражения, т.к. я не понимаю предмета спора).vintage
13.04.2016 17:16К сожалению, это массовое заблуждение. Из-за таких вот примеров.
Какое отношение внутренняя реализация (логирование, проверка прав) имеет ко внешнему api? Каким образом ваш клиент должен догадаться, что для изменения состояния ресурса "ракета", необходимо создать ресурс "запустить"? И чем это знание принципиально отличается от знания глагола "запустить"?
napa3um
13.04.2016 17:22К сожалению, вы не желаете меня понимать.
Это не внутренняя реализация, а исключительно прикладная задача. Представьте журнал полёта ракеты и необходимость доступа к нему сержантов, но не рядовых. Это не системное логирование поведения программы для программистов или системных администраторов.vintage
13.04.2016 17:34Вы хотите сказать, что для запуска ракеты мне необходимо создать ресурс "журнал полёта"? :-)
napa3um
13.04.2016 17:39Нет, такого я не хотел сказать. И вам я вообще ничего не хочу сказать. Всё ещё не вижу предмета разговора с вами.
Bronx
16.04.2016 10:31Почему бы благородному дону и не создать такой журнал — только не полёта, а ракеты? Нормальный такой CQRS, что вас смущает? Очень удобно: посылаем команды, выполняем, записываем. Полная история ракеты от изготовления до уничтожения доступна во всех деталях. Состояние ракеты на любой момент времени можно воспроизвести элементарно. Бортовые журналы не вчера придуманы.
Попробуйте получить то же самое манипуляциями со «state=flying».vintage
16.04.2016 11:15Вы программы на компе тоже запускаете путём создания лог-файлов?
Bronx
16.04.2016 11:30Я запускаю их подачей («созданием») команды. Например, «start notepad.exe». Могу сохранить несколько последовательных команд в файле (что говорит о том, что команды — это тоже сущности), и могу проиграть этот файл несколько раз, воспроизводя одно и то же состояние. Можно назвать этот файл «логом команд», а можно «командным файлом», по сути он и то, и другое, чисто терминологический вопрос (логи обычно read-only)
А вы как это делаете — путём изменения у программы статуса «запущено»? Что-то типа «patch notepad.exe status=running»?vintage
16.04.2016 11:57Можно назвать этот файл «логом команд», а можно «командным файлом», по сути он и то, и другое, чисто терминологический вопрос (логи обычно read-only)
Ок, вы "командный файл" запускаете созданием "журнала работы"?
Bronx
16.04.2016 12:14Файл с набором команд — это и есть «журнал работы», он уже создан. Его можно изучать, можно проигрывать, но операции над самим журналом уже не связаны с операциями над программами.
vintage
16.04.2016 12:41Журнал работы выглядит как-то так:
Bronx
16.04.2016 13:31И? Видите там строчки «GET http://blablabla»? Это команды, небольшие текстовые «документы», созданные клиентом и посланные на веб-сервер. Веб-сервер прочитал эти документы, сохранил (в лог, или в таблицу), что-то сделал, отдал ответ. Ваш журнал можно не только читать как лог, но и проиграть заново (после тривиальной трансформации). Это лишь вопрос представления информации либо в виде данных, либо в виде инструкций. Команды — это тоже данные, если уметь смотреть на них не только с точки зрения императивного программирования. Нет ничего зазорного считать их «ресурсом» и создавать/хранить в БД наравне с другими ресурсами.
vintage
16.04.2016 14:05Это содержимое лога. Его создаёт программа (ракета) во время работы сама. А не мы создаём лог файл, чтобы запустить программу (ракету).
Проиграть заново можно лишь идемпотентные запросы.
Если вам до сих пор это не понятно, то мне больше нечего добавить.
Bronx
16.04.2016 15:04> А не мы создаём лог файл, чтобы запустить программу (ракету).
Ничто не мешает создать файл с командами, которые были логгированы при предыдущем запуске, и прокрутить эти команды ракете. Или всем ракетам. С воспроизводимым результатом. Лог команд превращается в скрипт одним движением руки, это по сути одно и то же — последовательность команд, прошлых (лог) либо будущих (скрипт). Скрипт можно создать, даже не создавая лога. И мы таки создаём скрипты, чтобы запустить программу. Мне непонятно, что вам _тут_ непонятного и о чём вы спорите?
> Проиграть заново можно лишь идемпотентные запросы.
Я вообще намекал на CQRS и event sourcing, при чём тут идемпотентность?
napa3um
13.04.2016 17:34Вообще, остановитесь, пожалуйста, и сформулируйте сначала ваше возражение в виде тезиса. Не опирайтесь на мой пример — в нём не было никаких исходных данных о прикладной задаче, его можно понимать очень по-разному, он был приведён не для полного объяснения принципа REST, а лишь для пояснения частного, конкретного аспекта, всплывшего в диалоге — почему в HAL указываются связанные ресурсы, а не связанные дейсвтия. Т.е., отсюда у меня и появился связанный ресурс, мне нужно было придумать именно связанный ресурс, а не дополнительный стейт, чтобы показать, что указывается в HAL.
Клиент догадываться ни о чём не должен, он должен знать, как работать с конкретным API. REST не избавляет от необходимости понимать прикладную область и ресурсы, на которые её декомпозировали в API. Он лишь избавляется от головной боли, связанной с распределением, кешированием, разграничением доступов, интерпретацией ошибок и статусов — ограничения REST этому способствуют (как раз потому, что не позволяют как попало обращаться с глаголами, и потому, что заставляют думать ресурсами, а не действиями).vintage
13.04.2016 17:51-1Используя http метод LAUNCH у вас точно также не будет "головной боли, связанной с распределением, кешированием, разграничением доступов, интерпретацией ошибок и статусов".
Bronx
16.04.2016 11:07Допустим, вы решили запустить ракету — что будете делать? Выставите «state=flying»? Допустим, разрешение на запуск есть, и поле благополучно изменилось. Означает ли это, что ракета уже летит? Фигушки. От команды на старт и до полёта ещё куча всего должно случиться, и несмотря на статус «flying» ваша ракета пока ещё на земле, т.е. статус не правдив. А если мы сделаем его правдивым (поле state не изменится, пока ракета не оторвётся от земли), то получится, что мы посылаем PUT/PATCH, а состояние всё не меняется и не меняется — тоже неудобно.
А всё потому, что тут смешаны истинное состояние сущности, поле её состояния (state="..") и подача команды на изменение состояния. Это и есть типичное заблуждение — считать, что изменение поля состояния эквивалентно изменению истинного состояния сущности. Такое бывает только для самых тривиальных случаев, например, если сущность — это просто какая-то тупая конфигурация без поведения (пользовательский профиль, настройки и т.п.). В менее тривиальных случаях мир становится асинхронным, и эта простая схема перестаёт работать.vintage
16.04.2016 11:51Допустим, вы решили запустить ракету — что будете делать? Выставите «state=flying»?
мы посылаем PUT/PATCH, а состояние всё не меняется и не меняется — тоже неудобно.
Вполне нормальная ситуация, когда фактическое изменение состояния отличается от запрошенного. В описанном вами случае будет так:
PATCH /ракета(123)
state =activated
200 OK
state =fuel-loading
А сама ракета — конечный автомат со следующими состояниями: staying => activated -> fuel-loading -> ready -> flying => detonated
Жирные стрелки — переходы, управляемые клиентом. Тонкие — автоматические переходы.
А всё потому, что тут смешаны истинное состояние сущности, поле её состояния (state="..") и подача команды на изменение состояния.
В dataflow архитектурах нет понятия "команды" — все процессы определяются текущим состоянием. Это очень гибкий подход, рекомендую с ним ознакомиться.
В менее тривиальных случаях мир становится асинхронным, и эта простая схема перестаёт работать.
Всё замечательно работает, если мыслить не в терминах событий и действий, а в терминах состояний и их синхронизации.
Bronx
16.04.2016 14:47> Вполне нормальная ситуация, когда фактическое изменение состояния отличается от запрошенного.
> В описанном вами случае будет так:
> PATCH /ракета(123)
> state =activated
Почему ваш вариант лучше чем:
POST /missiles/123/military_activity/
command=arm&confirmationCode=CEJBCWJNSD&approvedBy=Pupkin
POST /missiles/123/military_activity/
command=lock&targetId=UWYEG&confirmationCode=JCBEIWIEJNV&approvedBy=Pupkin,Ivanov
POST /missiles/123/military_activity/
command=launch&confirmationCode=IUWIEFBCAMN&approvedBy=Pupkin,Ivanov,Putin
Или, вариант попроще (если не нужны подтверждения каждого шага):
POST /launches/
missileId=123&launchpadId=RVMOW&targetId=UWYEG&confirmationCode=IUWIEFBCAMN&approvedBy=Pupkin,Ivanov,Putin
HTTP/1.1 201 Created
Location: /launches/293848
GET /launches/293848
HTTP/1.1 200 OK
{ возвращается состояние ракеты, пусковой площадки, цели, и т.п. }
Как в вашем варианте добавить всю ту пачку дополнительной информации, требующейся для каждого шага запуска? Делать её частью состояния ракеты? Но коды подтверждения пуска, ответственные лица, цели, расчёты, результаты, и т.п. не относятся к ракете, это именно что атрибуты _пуска_. Вполне логично выделить боевую активность в отдельную сущность, и запускать ракеты созданием «пусков». Ракета пусть меняет своё состояние «реактивно».
И это ещё не упомянуты прочие действия с ракетой, влияющие на её состояние — ТО, испытания, учения, транспортировка и т.п. Запихивать всё это в конечный автомат «ракета» — это умаяться можно. Пусть лучше она будет пассивной железякой, которая на земле не имеет своего поведения (тем более управляемого её собственным состоянием), а за переходы пусть отвечают более компетентные сущности.vintage
16.04.2016 15:08POST /missiles/123/military_activity/
command=arm&confirmationCode=CEJBCWJNSD&approvedBy=Pupkin
А чем ваш вариант лучше, чем:
ARM /missiles/123/
confirmationCode=CEJBCWJNSD&approvedBy=Pupkin
Оба варианта — RPC.
POST /launches/
missileId=123&launchpadId=RVMOW&targetId=UWYEG&confirmationCode=IUWIEFBCAMN&approvedBy=Pupkin,Ivanov,Putin
А вот тут у вас, наконец, получился REST :-)
Как в вашем варианте добавить всю ту пачку дополнительной информации, требующейся для каждого шага запуска?
Для этого есть http-headers.
Вполне логично выделить боевую активность в отдельную сущность, и запускать ракеты созданием «пусков».
Боевые операции — да. Журналы полёта — нет.
Пусть лучше она будет пассивной железякой, которая на земле не имеет своего поведения (тем более управляемого её собственным состоянием), а за переходы пусть отвечают более компетентные сущности.
Что ж вас так из крайности в крайность-то бросает? Ракета — тоже вполне конкретная сущность, которая что-то умеет, а что-то не умеет. И изменение других сущностей "реактивно" может влиять и на её состояния. И декомпозиция на ресурсы должна происходить в соответствии с предметной областью, а не выдумыванием RPC over REST.
Bronx
16.04.2016 16:13> Оба варианта — RPC.
POST /missiles/123/military_activity/ — это нормальный REST. Обычная коллекция сущностей, её можно читать GETом, фильтровать, выбирать отдельные активности и т.п. У каждой активности есть свой постоянный URL, состояние, связанные сущности, и т.п. Они выглядят, как «глаголы», но на самом деле это «существительные» (на самом-самом же деле эта граница весьма условна). Банковские операции — другой классический пример.
> А вот тут у вас, наконец, получился REST :-)
Те же яйца. Просто теперь активности типа «launch» выделены в отдельную коллекцию, с абсолютно тем же интерфейсом. Схема данных построже стала (military_activity полиморфная коллекция).vintage
16.04.2016 21:02Разница в том, что launch представляет из себя отдельный бизнес-процесс, имеющий продолжительность и промежуточные состояния, а military_activity — просто операции (зачастую атомарные) над бизнес-объектом и их введение продиктовано ни чем иным как желанием сделать RPC средствами REST.
Я ещё раз подчеркну, что REST — он о состояниях, а не действиях выраженных в форме существительных. И если у вашей ракеты есть состояние state, то и изменять его надо редактируя ракету. А если по бизнесу у вас есть отдельный процесс "launch", то и состояния state у ракеты быть не должно. Зато у "launch будет состояние "stage", которое опять же можно изменять редактированием "launch".
Bronx
16.04.2016 21:51Все активити в моём примере (активация, ввод целей, пуск) были бизнес-процессами. Некоторые из них могут проходить параллельно (активация и ввод целей, например).
RPC это бы было бы, если бы там были команды типа «POST /missile/123/change_state?state=flying».
> И если у вашей ракеты есть состояние state, то и изменять его надо редактируя ракету.
Как я уже писал, это хорошо работает лишь с простыми состояниями без зависимостей. Как только появляются сложные зависимости, становится выгодным вынести «изменение состояния и его зависимостей» в отдельные workflows, представляя их отдельными ресурсами.
> А если по бизнесу у вас есть отдельный процесс «launch», то и состояния state у ракеты быть не должно.
Это почему же? У ракеты вполне может быть состояние «state=flying» (равно как другие состояния, скажем, «mass», «active_stage», «altitude» и т.п.), зависимое от состояния процесса «launch», и ведомое этим процессом. Делать «mass» состоянием процесса как-то глупо, потому что на массу ракеты могут влиять разные процессы (заправка, например).vintage
16.04.2016 22:38О да, ввод целей — это целый процесс.
POST /missile/123/change_state?state=flying
|
POST /missiles/123/military_activity/
command=arm
не вижу принципиальной разницы.
Как я уже писал, это работает и со сложными зависимостями, лаконично инкапсулируя их, и позволяя клиенту лишь декларировать намерения, а не расписывать конкретные действия. Если нам нужно перевезти и перекрасить ракету, то в терминах состояний достаточно указать целевое состояния, а автоматика сама разберётся что в какой последовательности делать, а в терминах действий необходимо последовательно выполнять действия, дожидаясь завершения предыдущего.
Потому что если одни и те же данные у вас будут доступны из разных мест, то периодически вы будете сталкиваться с их рассинхронизацией. Например, запросили launch, там было состояние fuel-loading, потом запросили ракету, а там состояние flying. В результате, в зависимости от того, через какую модель вы обратитесь, вы получите разные состояния.
Bronx
16.04.2016 23:02> О да, ввод целей — это целый процесс.
Вообще-то, да, процесс со множеством контролей.
> не вижу принципиальной разницы.
А если бы было «POST /missiles/123/military_activities/arming», то увидели бы?
> лаконично инкапсулируя их, и позволяя клиенту лишь декларировать намерения, а не расписывать конкретные действия
Я как раз и инкапсулировал намерения и связанные с ними зависимости в отдельные сущности. Это не конкретные действия типа «установить переменную state в значение flying», это высокоуровневые задачи, описанные на предметном языке, бизнес-логика которых спрятана от пользователя, а наружу торчат лишь CRUD-операции «создать задачу; проверить статус; изменить задачу; отменить задачу; получить историю задач», прекрасно ложащиеся на стандартный REST. Вас, похоже, смутило то, что я назвал их «командами». Хотите, называйте из «задачами» или «намерениями» — суть та же.
Bronx
16.04.2016 11:22> А действия над всеми ресурсами всегда одинаковые.
Есть ли в HATEOAS каноничный способ сообщить о _доступности_ этих стандартных действий, передавая некую авторизационную информацию вместе с отношениями? Конечно, сервер всегда может дать отлуп на неавторизованную операцию, но что если хочется уберечь пользователя и заранее скрыть в UI запрещённые контролы (кнопку «Удалить», например)?vintage
16.04.2016 12:00Для этого добавляется ссылка на ресурс с описанием привилегий. Например: http://nin-jin.github.io/harp=1/article=12345.xml
Bronx
16.04.2016 12:52Если клиент получил коллекцию из 1000 элементов, и нужно отобразить в UI, какие элементы можно удалять, а какие нет, то для каждого из них делать запрос о привилегиях? Можно, но неохота. Вопрос в том, чтобы получить привилегии вместе с данными и метаданными, одним и тем же запросом, и сделать это стандартным путём (если он есть).
vintage
16.04.2016 13:18Получение привилегий — частный случай получения связных ресурсов. Есть разные подходы, которые можно комбинировать:
- Грузить данные лишь для тех элементов, что попадают в видимую область.
- Использовать http2/websockets для осуществления множества легковесных запросов.
- Использовать пакетные запросы.
- Использовать специальные языки запросов для выгребания связанных данных.
Ну а конкретно в примере права делятся на группы. Например, permission=article-my — права пользователя на созданные им статьи. Независимо от пользователя и статьи. Таких наборов прав весьма ограниченное число и они замечательно кешируются.
napa3um
16.04.2016 13:30Стандартного способа нет, это уже уровень бизнес-логики. Добавьте дополнительное поле в ресурс, сообщающее о доступности операций с ним для запросившего пользователя, или набор разрешений, необходимых пользователю для их выполнения, например. Зависит от прикладной задачи и выбранной схемы разграничения прав.
Bronx
16.04.2016 15:33Бывает, что URL связанного ресурса есть, а его состояния ещё нету. Скажем, есть ресурс /item/123, и в его гипермедии есть линк на коллекцию /item/123/children/ — а что с этой коллекцией разрешено делать? Ну, «GET /item/123/children» наверное можно — а POST? Или есть линк на "/item/123/sibling" — а можно на нём сделать DELETE? Хранить эти разрешения в самом /item/123 кажется неестественным, логичнее хранить права доступа рядом с гиперссылками. В принципе, никто не мешает расширить линки своими атрибутами, но я надеялся что где-то уже лежит стандарт, просто я о нём не знаю. Ну нет так нет… :)
napa3um
16.04.2016 15:54Если приложение большое и растущее, то «в духе REST» лучше использовать отдельный сервис авторизации/аутентификации (стандарт OAuth 2.0), это было бы «в духе микросервисности и масштабируемости». А в качестве стандартов для хранения и обработки правил доступа предложил бы https://m.habrahabr.ru/company/custis/blog/258861/
Это всё максимально мощные штуки, покрывающие все возможные варианты использования. Если проект не требует такой мощи, можно взять только идеи или отдельные спецификации (скажем, ограничиться ролями).
vintage
16.04.2016 21:07Вам не кажется странным передавать информацию и правах не в самом ресурсе, а копипастить вместе с каждой ссылкой на него, а в случае изменения прав доступа к ресурсу — предлагать перевыкачать все ресурсы, на него ссылающиеся?
Bronx
16.04.2016 22:21Ну я же написал, что бывают случаи, когда ресурса на руках ещё нет, есть только ссылка на него. Если есть ссылка — значит УЖЕ доступны операции над ресурсом. А раз операции становятся доступны вместе со ссылкой, то и права имеет смысл привязывать к ссылке.
Что касается «протухающих» прав, то с правами, передаваемыми в состоянии та же история: они точно так же могут протухнуть. Ничего страшного: будет ошибка на сервере и последующий рефреш.
zeldigas
17.04.2016 21:41Стандарта нет, все зависит от дизайна API и задач стоящих перед API. С точки зрения стандартных операций, есть OPTIONS метод, ответ на который должен содержать список доступных HTTP методов которые можно выполнить над ресурсом. В некоторых случаях это может помочь, но если у вас логика сложная то это может быть слишком "грубо" — например вы хотите позволить клиенту делать PUT запрос с определенными данными, но запрещать передавать другие данные. Как вы понимаете, через OPTIONS этого будет сложно достичь.
Могу сказать о своем опыте: если речь идет о "единичных" ресурсах, то отсутсвие ресурса выражается в отсутсвии ссылки на него. С коллекциями мы считаем что они всегда есть, но в случае чего пустые.
Action'ы мы выставляем в основном в тех ресурсах, к которым они относятся, но бывают и исключения. Из последних примеров — есть ресурс asset — содержащий мета-информацию и у него есть action для загрузки бинарных данных для него, естественно указывающий на другой URI. Так же и с созданием элементов для коллекций — action всегда присутсвует в самом ресурсе-коллекции, но в некоторых случаях мы выставляем его в другом ресурсе, если это лучше соотносится с задачами (производительность, логичность с точки зрения семантики ресурса и т.д.).
khaletskiy
11.04.2016 15:35После прочтения у меня возник следующий вопрос: если уходить от проталкивания знания клиента о действиях над ресурсами, то как будет данная схема работать, предположим страница профайла, есть ресурс Account, мне в любом случае нужно будет сходить за списком линок для данного ресурса, а затем уже по нужному релейшну сходить и получить то что мне нужно? Или это какое-то начальное знание с сервера при отрисовке темплейта? И как быть, если «типа микросервисы», т.е. темплейт рисует FE, а за данными ходим в сервисы BE?
Спасибо.napa3um
11.04.2016 19:54+1HATEOAS/HAL добавляет в ресурс знания не о действиях над этим ресурсом (действия для любого REST-ресурса всегда CRUD), а знания о связанных ресурсах. Изначальный список корневых ресурсов получается из entry-point (пример: https://morethancoding.com/2011/09/07/uri-construction-give-it-a-rest/ ). Не пытайтесь сделать RPC из REST, это не просто способ связи клиента с сервером, это именно архитектура приложения (парадигма декомпозиции прикладной задачи на ресурсы и отношения между ними).
napa3um
11.04.2016 20:04> Или это какое-то начальное знание с сервера при отрисовке темплейта? И как быть, если «типа микросервисы», т.е. темплейт рисует FE, а за данными ходим в сервисы BE?
Композиция ресурсов — на совести клиента. Т.е., в общем случае REST-приложение — это набор RESTfull API и отдельно хостящиеся клиенты (SPA, мобильное приложение, т.д.), каждый из которых сам реализует композицию данных по-своему. Микросерверность в REST — это возможность клиента работать сразу с кучей разнородных API на разных урлах. Темплейты на сервере не рендерятся, это задача клиента, сервисы предоставляют только данные (ресурсы). Т.е., да, теймплейтами рулит FE, а API рулит BE.zeldigas
13.04.2016 11:48Не надо пожалуйста в таком категоричном тоне про темплейты. В API Craft мейл листе люди обсуждают реализацию API с использованием html, ведь он имеет все для этого необходимое.
Нестандартно, но не запрещено :). Другое дело, что клиентов под это дело мало, но если кто реализует такое API оно будет полностью соответсвовать REST подходу.
napa3um
13.04.2016 12:08Всегда есть те, кто не понимает, всегда есть те, кто спрашивает, всегда есть те, кто уверен в том, что отвечает. Нормальный процесс, не переживайте сильно.
kekekeks
Проблема не в росте числа запросов, проблема в росте числа последовательных запросов. То есть даже мультиплексирование в SPDY/HTTP2 от тормозов не спасает. Если хочется "самодокументированных" API, лучше выдайте наружу RAML-спеку, по которой можно сгенерить клиент.
zeldigas
Генерация клиентов, вопрос достаточно дискуссионный. За это в частности критиковали SOAP.
Что же касается числа последовательных запросов, то все как с сайтом — если вам нужен быстрый переход на спец страницу — на home ресурсе выставите линк/action который подскажет клиенту как это сделать.