Pull to refresh

Comments 85

Проблема не в росте числа запросов, проблема в росте числа последовательных запросов. То есть даже мультиплексирование в SPDY/HTTP2 от тормозов не спасает. Если хочется "самодокументированных" API, лучше выдайте наружу RAML-спеку, по которой можно сгенерить клиент.

Генерация клиентов, вопрос достаточно дискуссионный. За это в частности критиковали SOAP.


Что же касается числа последовательных запросов, то все как с сайтом — если вам нужен быстрый переход на спец страницу — на home ресурсе выставите линк/action который подскажет клиенту как это сделать.

Три вопроса:


  1. Клиенту все еще нужно догадываться, что запрос на add-recipe нужно делать постом а не путом или патчем? Почему бы не указывать метод явно? Например:


    "links": {
    "add-recipe": {
      "href": "http://example.com/recipes",
      "method": "POST"
    },
    "recent-recipe": {
      "href": "http://example.com/recipes/my-recipe",
      "method": "GET"
    }
    }

  2. Почему бы значение 'href' не указывать абсолютной ссылкой? Таким образом можно абстрагироваться от синглтон-домена и мы легко можем заменить домен в случае разнесения функциональности по разным доменам-субдоменам.
    А можем и не заменить.


    "add-recipe": {
      "href": "http://example.com/recipes",
      "method": "POST"
    }

    "add-recipe": {
      "href": "http://mycompany.example.com/recipes",
      "method": "POST"
    }

  3. Совершенно не понял почему ключом выступает ссылка? (в ваших примерах это "http://acme.com/recipes/rels/you-can-also-like"). Невнимательно прочитал?

Спасибо.

  1. Тут вопрос дизайна и предпочтений. Этот пример показывал как можно было бы сделать свой, ни на что не похожий hypermedia тип (application/vnd.com.acme.recipes+json). У авторов такого типа, в его документации было бы описано как создавать рецепты, какой метод нужен. Дизайн странный, но используя свой Content-Type они вполне это могут сделать, хотя ваш вариант мне нравится больше :).
    Даже у авторов generic-типов нет консенсуса на этот счет. В HAL методов нет, в Siren есть. Альтернативой документирования методов, или указания их в ресурсе может являться ссылка на профиль, такой ресурс с мета-информацией.
  2. Вполне допустимы оба варианта, дело вкуса. У нас в проектах для "локальных" ресурсов мы опускаем домен, а для связи нескольких микросервисов уже указываем полный линк. Клиенты поддерживают оба формата, так что для них все прозрачно. Все как со ссылками на веб страницах.
  3. Скорее я не раскрыл этот момент. Если надеть шляпу формалиста и бюрократа, то все "простые" 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"
    }
    }

Оно, конечно, интересно.
Но почему-то, чем дальше тем больше это начинает напоминать SOAP.
Вот-вот. Помню как с десяток лет назад вся эта движуха за REST проходила под флагами «долой RPC, клиент должен быть простым». А сейчас, похоже, к тому самому RPC+WSDL+толстый клиент всё и возвращается. :)
Да, все тоже самое, только в более кривом исполнении в виде 10 несовместимых стандартов:
XPath — уже придумали аналог JsonPath
XMLSchema — Json Schema
валидация, ссылки друг на друга, метаданные и пр.
Даже JOLT вместо XSLT придумали.
После этого хочется спросить — «ну и чем вам угловые скобочки не понравились»?
Мы сейчас получим все это же, только не в виде стройной структуры, которая была в XML-технологиях, а в виде самодеятельного несовместимого зоопарка?
REST — это ресурсы и отношения вместо объектов и методов на RPC. Мы получим стандартизованный протокол, поддерживающий стаднартизованную архитектуру (т.е., нужно будет просто понять REST, и принимать решений и делиться ими в команде при проектировании/реализации придётся гораздо меньше). Выбор между XML и JSON в REST вообще никак не принципиален.

Спасибо за ссылку, посмотрим.


Это кстати характерный пример, когда человек делает обзор имеющихся форматов, ему ничего не нравится и он создает свой. Отличная иллюстрация — https://xkcd.com/927/

Как уже отметили, для всяких сложностей, например последовательных запросов по доморощенному протоколу, дуплекс, дозвон — callback и т.п. — есть SOAP.

Я не думаю, что вопрос выбора инструмента SOAP или REST определяется сложностью задачи. Кстати, сравнивать конкретную технологию с архитектурным принципом не вполне корректно.


Кстати в отличной книге Майка Амундсена Restful Web APIs упоминается CoAP — протокол для простых электронных устройств, использующий REST принципы. Я не буду развивать эту тему, так как не специалист в этой области, но стоит отметить. что не все упирается в http.

Как возможный формат можно рассмотреть, например, CBOR. С использованием тегов клиент без проблем сможет определять ссылки и прочую метаинформацию. Другой вопрос, что он машинный, а не человекочитаемый. Но ведь проблема именно в том, чтобы научить машину понимать, а человек и сам из JSON ссылки выделит.
"… Предположим, что мы для каждого рецепта хотим предоставить клиенту набор рекомендаций … "

Мне кажется, что обычно клиент решает свою задачу, и стороннее API подключается по принципу «подключил и забыл…». Странно ожидать от него, что он будет следить за рекомендациями.

API бывают разные. Те которые вы, скорее всего, имели ввиду — делаются для решения определенной задачи. Такому клиенту действительно ничего не нужно его захардкодили и все.


Но этим же мир клиентов не ограничивается. Можно заморочиться и сделать более умного клиента или сделать клиента который используется интерактивно. Самый наглядный пример — HAL или Siren браузер который может работать с любым API. Еще пример — робот гугла который ходит по сайтам и распознаёт микроформаты в html разметке.


Если не залезать так высоко в абстракции, вот вам более простой пример: оформление заказа. Инвойс к заказу вы получите только после оплаты. Не гипермедиа вариант — сразу все ресурсы описать в документации, но тогда вам нужно клиенту как-то дать понять есть уже инвойс или нет. А так есть линка invoice в ресурсе "Заказ" есть и инвойс, нет значит еще не готов.

За использование PUT и DELETE нужно руки отрывать :-)
DELETE нарушает связность, когда пройдя по ссылке можно получить 404.
PUT не дружит с совместным редактированием. Либо дружит, но через дополнительные методы LOCK и UNLOCK.
Так что хороший апи должен поддерживать следующие методы: GET, HEAD, POST, PATCH.
А в некоторых случаях (когда идентификаторы выбираются клиентом) и POST не нужен. Яркий пример — вики.

Пожалейте руки создателей многих API, включая Amazon S3, а также кучи других :)


Если серьезно, то конечно мнение крайне субъективное. Зачем винить молоток что вы ударили себя по пальцам? Если вам нужна параллельная работа и ваши ресурсы не позволяют это сделать, то вопрос скорее к дизайну ресурсов, чем к методам из спецификации.


За использование PUT и DELETE нужно руки отрывать :-)
А в некоторых случаях (когда идентификаторы выбираются клиентом) и POST не нужен

Налицо логическое противоречие: и это вам не нравится и то вам неправильно. Надеюсь вы не GET'ом собираетесь ресурсы создавать?

Если серьезно, то конечно мнение крайне субъективное.
Я привёл аргументы.

Зачем винить молоток что вы ударили себя по пальцам?
Я использую саморезы и не имею такой проблемы.

Если вам нужна параллельная работа и ваши ресурсы не позволяют это сделать, то вопрос скорее к дизайну ресурсов, чем к методам из спецификации.
Правильный дизайн ресурсов не позволяет работать с ними через метод PUT по вышеозначенной причине.

Налицо логическое противоречие: и это вам не нравится и то вам неправильно.
Тут нет противоречия.

Касательно POST — попробуйте реализовать API без него и поймёте, как это удобно. Если вкратце:
1. Идентификатор ресурса у вас есть самого начала, что избавляет от необходимости вводить временные идентификаторы до первого сохранения, от которых очень много проблем с клиентской стороны.
2. Все запросы у вас идемпотентные и вы можете без опаски любой из них повторять.
Но это весьма не «традиционный» путь.

Надеюсь вы не GET'ом собираетесь ресурсы создавать?
Нет.
Касательно POST — попробуйте реализовать API без него и поймёте, как это удобно.

В своем первом комментарии вы заявили, что PUT и DELETE не использовать (отрывание рук — сомнительное руководство к дейстивю), сейчас заявляете что POST тоже не использовать.


Внимание вопрос — какой метод протокола HTTP вы используете для создания ресурса?

Я это в первом же комментарии и заявил.

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» }

PATCH не идемпотентный метод в широком смысле, в RFC посвященном ему об этом говорится страница 3, второй абзац


В любом случае, удачи вам. Если ваш API решает поставленные перед ним задачи, это прекрасно!

HATEOAS — один из принципов REST, предписывающий ресурсу нести в себе информацию об отношениях с другими ресурсами. HAL — язык для описания таких отношений. Помимо этого принципа REST ограничивает архитектуру в количестве действий, которые в принципе возможны над любым ресурсом — у GET, POST, PUT, PATCH, DELETE строго определена семантика и способ их обработки сервером (и стратегия кеширования результатов). Именно потому в HAL не указываются методы запроса, этот не перечень RPC-действий, которые можно совершить с ресурсом, а перечень REST-ресурсов, имеющих отношение к данному. А действия над всеми ресурсами всегда одинаковые.

Отличие RPC/SOAP от REST не в сложности/простоте реализации, а в семантике. RPC — это когда прикладная задача моделируется объектами и методами, а REST — это когда задача моделируется ресурсами и отношениями между ними. Эти способы взаимовыразимы друг через друга (REST можно построить на базе RPC и наоборот), но не одно и то же.

Посмотрите RFC 7231, в частности метод POST. Там четко написано, что семантику определяет сам ресурс, который обрабатывает запрос. В связи с чем если у ресурса сложное поведение, через POST можно сделать разные действия. Понимаю что можно разное поведение можно реализовать и через PUT, но у него более четкая семантика с точки зрения HTTP.


Вы еще забыли важный момент в отличии RPC от REST — в RPC все методы определны заранее, это действительно похоже на сигнатуру класса. В REST при помощи гипермедиа контролов (их наличия и отсутсвия) сервер как владелец ресурса определяет что может делать клиент в текущий момент.

Семантику определяет сам ресурс, верно. Определяет семантику в рамках ограничений, накладываемых парадигмой REST, в которых POST — это именно конкретный глагол, создающий ресурс.

Не забыл. Этот момент непринципиален в том различии REST и RPC, о котором я говорил. Но можно его считать целью этих различий (некой практической выгодой архитектуры).

Почитайте пожалуйста первый параграф по ссылке выше. Там простым английским языком написано, что в общем случае семантика POST'а не определена и ресурс может такой запрос обрабатывать как хочет не нарушая спецификацию.


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

REST возник позже HTTP, хотя и является формализацией его целей. HTTP != REST.

REST — это архитектурный принцип, HTTP — протокол. Почитайте внимательно диссертацию Филдинга.

А вы почитайте, пожалуйста, мои комментарии. Я где-то пытался назвать REST протоколом? Сформулируйте почётче, чему хотите возразить в моих словах.

Я согласен что REST это про ресурсы, а не про набор заранее определенных вызовов, как в случае с RPC.


Но я не совсем согласен со следующими моментами:


  1. Семантика методов. В первом комментарии вы сказали что "GET, POST, PUT, PATCH, DELETE строго определена семантика".


    1. Это так для всего кроме POST. Как я указывал ранее, в актуальной спецификации HTTP у post'а семантику определяет сам ресурс и REST тут ничего не меняет, потому что REST не меняет ограничений протокола.

    Я не спорю что много задач можно решить чисто CRUD подходом, вводя самоограничение, что POST мы используем только для создания ресурса, но не стоит обобщать это на все API и на протокол HTTP как таковой. REST, как принцип, нас совсем не ограничивает — он предписывает серверу сообщать клиенту состояние ресурса (и что с ним можно делать) и не нарушать семантику протокола который используется между ними используя его возможности по назначению. Указывать явно или не указывать метод для выполнения действия уже вопрос реализации.


  2. "HAL — язык описания отношений".


    1. Вообще это формат представления ресурса у которого в спеке четко прописано где находятся ссылки. И форматов подобных ему достаточно много. Отношения описывают линки, семантика которых задаётся relation'ом. Тут эти форматы ничего не изобретают, используя уже известные конструкции из html'я и atom фидов.
    2. То что там нет методов не истина в последней инстанции, а просто виденье его создателя — об этом Майк Келли лично говорил на конфе API Craft в 2014 году.

1. Нет, REST нас ограничивает, вы просто не поняли этих ограничений, похоже. Если мы хотим в духе RPC ввести новую операцию над объектом, мы её реализовываем новым ресурсом со стандартными операциями над ним. Например, мы хотим реализовать у объекта «Ракета» метод «Запустить», тогда в концепции REST мы, например, создадим у ресурса «Ракета» вложенный ресурс «Запуск». Т.е., методом POST создавая экземпляр ресурса «Ракета/:id/Запуск» мы и производим запуск ракеты.
2. Не могу оспорить вашего мнения. Оно отвалится самостоятельно, когда (если) примите то, что я описал в первом пункте.

Цитату из диссертации приведете?

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


REST — он не о действиях, а о состояниях.


PATCH /ракета(123) 
target name =London

200 OK
target
    name =London
    pos
        35.2213
        12.4367
state =flying
Не понял вопроса. Я просто с потолка привёл пример, как семантика «действие над объектом, выходящее за пределы сематики HTTP-глаголов» может переводиться на семантику «действие над ресурсом в рамках сематики HTTP-глаголов». Да, ваш вариант тоже возможен, его отличие от моего примера уже не упирается в различие RPC и REST, а упирается в другие критерии реализации приложения. В пределе можно вообще всю структуру БД спроецировать в один ресурс REST-API (чтобы избавиться от всех вложенностей ресурсов, которую вы называете виртуальностью) с которым клиент будет работать исключительно патчами. Усложнив себе жизнь с управлением доступами ко всему этому добру и почти полностью отказавшись от возможности кеширования.

Что-то вас бросает из одной крайности в другую :-)

Чему вы хотите возразить, кроме моей манеры пояснять свои мысли утрированными примерами?

Да, возможно, слово «например» в моих словах недостаточно чётко указывало, что я привожу лишь один из вариантов реализации. Т.е., если пришла мысль добавить новое действие к ресурсу, то это в REST будет передачей нового состояния — либо «виртуальный» вложенный ресурс, либо «виртуальное» дополнительное поле в ресурсе, но никак не «виртуальная» семантика глагола. Да, ему нужно думать состояниями, а не действиями. Может, такая формулировка кому-то действительно будет удобнее, чтобы испытать просветление. Но, мне казалось, что эту сторону вопроса уже обсудили выше, и затруднения вызывает именно проблема отсутствия глаголов в HAL, отчего и начал развиваться диалог. Безусловно, всегда можно найти, чем мои слова можно дополнить. Не обязательно это делать в форме возражения.

Что примеры нужно приводить корректные, чтобы у читателей не возникало неправильного представления об обсуждаемом вопросе. Типичное заблуждение — переносить глаголы из хттп-метода в урл и использование метода post. Это рест формально, но не по духу.

Это не заблуждение, а пример. Исключительно в контексте диалога. Представьте, что ракета в прикладной задаче многоразовая, запуски логируются, и к этим логам разграничивается доступ по ролям. Теперь мой пример стал красивее вашего? Ваше «заблуждение» я тоже показал, утрировав ваш пример.

Не нужно вообще переносить глаголы куда-то, нужно сразу проектировать в духе REST. Декомпозиция на ресурсы зависит от прикладной области и ограничений реализации (что, как и где будет обрабатываться, как контролироваться). И не нужно спорить ради спора (либо сформулируйте, пожалуйста, компактным тезисом суть вашего возражения, т.к. я не понимаю предмета спора).

К сожалению, это массовое заблуждение. Из-за таких вот примеров.


Какое отношение внутренняя реализация (логирование, проверка прав) имеет ко внешнему api? Каким образом ваш клиент должен догадаться, что для изменения состояния ресурса "ракета", необходимо создать ресурс "запустить"? И чем это знание принципиально отличается от знания глагола "запустить"?

К сожалению, вы не желаете меня понимать.

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

Вы хотите сказать, что для запуска ракеты мне необходимо создать ресурс "журнал полёта"? :-)

Нет, такого я не хотел сказать. И вам я вообще ничего не хочу сказать. Всё ещё не вижу предмета разговора с вами.
Почему бы благородному дону и не создать такой журнал — только не полёта, а ракеты? Нормальный такой CQRS, что вас смущает? Очень удобно: посылаем команды, выполняем, записываем. Полная история ракеты от изготовления до уничтожения доступна во всех деталях. Состояние ракеты на любой момент времени можно воспроизвести элементарно. Бортовые журналы не вчера придуманы.

Попробуйте получить то же самое манипуляциями со «state=flying».

Вы программы на компе тоже запускаете путём создания лог-файлов?

Я запускаю их подачей («созданием») команды. Например, «start notepad.exe». Могу сохранить несколько последовательных команд в файле (что говорит о том, что команды — это тоже сущности), и могу проиграть этот файл несколько раз, воспроизводя одно и то же состояние. Можно назвать этот файл «логом команд», а можно «командным файлом», по сути он и то, и другое, чисто терминологический вопрос (логи обычно read-only)

А вы как это делаете — путём изменения у программы статуса «запущено»? Что-то типа «patch notepad.exe status=running»?
Можно назвать этот файл «логом команд», а можно «командным файлом», по сути он и то, и другое, чисто терминологический вопрос (логи обычно read-only)

Ок, вы "командный файл" запускаете созданием "журнала работы"?

Файл с набором команд — это и есть «журнал работы», он уже создан. Его можно изучать, можно проигрывать, но операции над самим журналом уже не связаны с операциями над программами.

Журнал работы выглядит как-то так:


image

И? Видите там строчки «GET http://blablabla»? Это команды, небольшие текстовые «документы», созданные клиентом и посланные на веб-сервер. Веб-сервер прочитал эти документы, сохранил (в лог, или в таблицу), что-то сделал, отдал ответ. Ваш журнал можно не только читать как лог, но и проиграть заново (после тривиальной трансформации). Это лишь вопрос представления информации либо в виде данных, либо в виде инструкций. Команды — это тоже данные, если уметь смотреть на них не только с точки зрения императивного программирования. Нет ничего зазорного считать их «ресурсом» и создавать/хранить в БД наравне с другими ресурсами.

Это содержимое лога. Его создаёт программа (ракета) во время работы сама. А не мы создаём лог файл, чтобы запустить программу (ракету).


Проиграть заново можно лишь идемпотентные запросы.


Если вам до сих пор это не понятно, то мне больше нечего добавить.

> А не мы создаём лог файл, чтобы запустить программу (ракету).

Ничто не мешает создать файл с командами, которые были логгированы при предыдущем запуске, и прокрутить эти команды ракете. Или всем ракетам. С воспроизводимым результатом. Лог команд превращается в скрипт одним движением руки, это по сути одно и то же — последовательность команд, прошлых (лог) либо будущих (скрипт). Скрипт можно создать, даже не создавая лога. И мы таки создаём скрипты, чтобы запустить программу. Мне непонятно, что вам _тут_ непонятного и о чём вы спорите?

> Проиграть заново можно лишь идемпотентные запросы.

Я вообще намекал на CQRS и event sourcing, при чём тут идемпотентность?
Вообще, остановитесь, пожалуйста, и сформулируйте сначала ваше возражение в виде тезиса. Не опирайтесь на мой пример — в нём не было никаких исходных данных о прикладной задаче, его можно понимать очень по-разному, он был приведён не для полного объяснения принципа REST, а лишь для пояснения частного, конкретного аспекта, всплывшего в диалоге — почему в HAL указываются связанные ресурсы, а не связанные дейсвтия. Т.е., отсюда у меня и появился связанный ресурс, мне нужно было придумать именно связанный ресурс, а не дополнительный стейт, чтобы показать, что указывается в HAL.

Клиент догадываться ни о чём не должен, он должен знать, как работать с конкретным API. REST не избавляет от необходимости понимать прикладную область и ресурсы, на которые её декомпозировали в API. Он лишь избавляется от головной боли, связанной с распределением, кешированием, разграничением доступов, интерпретацией ошибок и статусов — ограничения REST этому способствуют (как раз потому, что не позволяют как попало обращаться с глаголами, и потому, что заставляют думать ресурсами, а не действиями).

Используя http метод LAUNCH у вас точно также не будет "головной боли, связанной с распределением, кешированием, разграничением доступов, интерпретацией ошибок и статусов".

Допустим, вы решили запустить ракету — что будете делать? Выставите «state=flying»? Допустим, разрешение на запуск есть, и поле благополучно изменилось. Означает ли это, что ракета уже летит? Фигушки. От команды на старт и до полёта ещё куча всего должно случиться, и несмотря на статус «flying» ваша ракета пока ещё на земле, т.е. статус не правдив. А если мы сделаем его правдивым (поле state не изменится, пока ракета не оторвётся от земли), то получится, что мы посылаем PUT/PATCH, а состояние всё не меняется и не меняется — тоже неудобно.

А всё потому, что тут смешаны истинное состояние сущности, поле её состояния (state="..") и подача команды на изменение состояния. Это и есть типичное заблуждение — считать, что изменение поля состояния эквивалентно изменению истинного состояния сущности. Такое бывает только для самых тривиальных случаев, например, если сущность — это просто какая-то тупая конфигурация без поведения (пользовательский профиль, настройки и т.п.). В менее тривиальных случаях мир становится асинхронным, и эта простая схема перестаёт работать.
Допустим, вы решили запустить ракету — что будете делать? Выставите «state=flying»?

Я же привёл пример.


мы посылаем PUT/PATCH, а состояние всё не меняется и не меняется — тоже неудобно.

Вполне нормальная ситуация, когда фактическое изменение состояния отличается от запрошенного. В описанном вами случае будет так:


PATCH /ракета(123)
state =activated


200 OK
state =fuel-loading


А сама ракета — конечный автомат со следующими состояниями: staying => activated -> fuel-loading -> ready -> flying => detonated


Жирные стрелки — переходы, управляемые клиентом. Тонкие — автоматические переходы.


А всё потому, что тут смешаны истинное состояние сущности, поле её состояния (state="..") и подача команды на изменение состояния.

В dataflow архитектурах нет понятия "команды" — все процессы определяются текущим состоянием. Это очень гибкий подход, рекомендую с ним ознакомиться.


В менее тривиальных случаях мир становится асинхронным, и эта простая схема перестаёт работать.

Всё замечательно работает, если мыслить не в терминах событий и действий, а в терминах состояний и их синхронизации.

> Вполне нормальная ситуация, когда фактическое изменение состояния отличается от запрошенного.
> В описанном вами случае будет так:
> 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
{ возвращается состояние ракеты, пусковой площадки, цели, и т.п. }

Как в вашем варианте добавить всю ту пачку дополнительной информации, требующейся для каждого шага запуска? Делать её частью состояния ракеты? Но коды подтверждения пуска, ответственные лица, цели, расчёты, результаты, и т.п. не относятся к ракете, это именно что атрибуты _пуска_. Вполне логично выделить боевую активность в отдельную сущность, и запускать ракеты созданием «пусков». Ракета пусть меняет своё состояние «реактивно».

И это ещё не упомянуты прочие действия с ракетой, влияющие на её состояние — ТО, испытания, учения, транспортировка и т.п. Запихивать всё это в конечный автомат «ракета» — это умаяться можно. Пусть лучше она будет пассивной железякой, которая на земле не имеет своего поведения (тем более управляемого её собственным состоянием), а за переходы пусть отвечают более компетентные сущности.
POST /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.

> Оба варианта — RPC.

POST /missiles/123/military_activity/ — это нормальный REST. Обычная коллекция сущностей, её можно читать GETом, фильтровать, выбирать отдельные активности и т.п. У каждой активности есть свой постоянный URL, состояние, связанные сущности, и т.п. Они выглядят, как «глаголы», но на самом деле это «существительные» (на самом-самом же деле эта граница весьма условна). Банковские операции — другой классический пример.

> А вот тут у вас, наконец, получился REST :-)

Те же яйца. Просто теперь активности типа «launch» выделены в отдельную коллекцию, с абсолютно тем же интерфейсом. Схема данных построже стала (military_activity полиморфная коллекция).

Разница в том, что launch представляет из себя отдельный бизнес-процесс, имеющий продолжительность и промежуточные состояния, а military_activity — просто операции (зачастую атомарные) над бизнес-объектом и их введение продиктовано ни чем иным как желанием сделать RPC средствами REST.


Я ещё раз подчеркну, что REST — он о состояниях, а не действиях выраженных в форме существительных. И если у вашей ракеты есть состояние state, то и изменять его надо редактируя ракету. А если по бизнесу у вас есть отдельный процесс "launch", то и состояния state у ракеты быть не должно. Зато у "launch будет состояние "stage", которое опять же можно изменять редактированием "launch".

Все активити в моём примере (активация, ввод целей, пуск) были бизнес-процессами. Некоторые из них могут проходить параллельно (активация и ввод целей, например).

RPC это бы было бы, если бы там были команды типа «POST /missile/123/change_state?state=flying».

> И если у вашей ракеты есть состояние state, то и изменять его надо редактируя ракету.

Как я уже писал, это хорошо работает лишь с простыми состояниями без зависимостей. Как только появляются сложные зависимости, становится выгодным вынести «изменение состояния и его зависимостей» в отдельные workflows, представляя их отдельными ресурсами.

> А если по бизнесу у вас есть отдельный процесс «launch», то и состояния state у ракеты быть не должно.

Это почему же? У ракеты вполне может быть состояние «state=flying» (равно как другие состояния, скажем, «mass», «active_stage», «altitude» и т.п.), зависимое от состояния процесса «launch», и ведомое этим процессом. Делать «mass» состоянием процесса как-то глупо, потому что на массу ракеты могут влиять разные процессы (заправка, например).

О да, ввод целей — это целый процесс.


POST /missile/123/change_state?state=flying
|
POST /missiles/123/military_activity/
command=arm

не вижу принципиальной разницы.


Как я уже писал, это работает и со сложными зависимостями, лаконично инкапсулируя их, и позволяя клиенту лишь декларировать намерения, а не расписывать конкретные действия. Если нам нужно перевезти и перекрасить ракету, то в терминах состояний достаточно указать целевое состояния, а автоматика сама разберётся что в какой последовательности делать, а в терминах действий необходимо последовательно выполнять действия, дожидаясь завершения предыдущего.


Потому что если одни и те же данные у вас будут доступны из разных мест, то периодически вы будете сталкиваться с их рассинхронизацией. Например, запросили launch, там было состояние fuel-loading, потом запросили ракету, а там состояние flying. В результате, в зависимости от того, через какую модель вы обратитесь, вы получите разные состояния.

> О да, ввод целей — это целый процесс.

Вообще-то, да, процесс со множеством контролей.

> не вижу принципиальной разницы.

А если бы было «POST /missiles/123/military_activities/arming», то увидели бы?

> лаконично инкапсулируя их, и позволяя клиенту лишь декларировать намерения, а не расписывать конкретные действия

Я как раз и инкапсулировал намерения и связанные с ними зависимости в отдельные сущности. Это не конкретные действия типа «установить переменную state в значение flying», это высокоуровневые задачи, описанные на предметном языке, бизнес-логика которых спрятана от пользователя, а наружу торчат лишь CRUD-операции «создать задачу; проверить статус; изменить задачу; отменить задачу; получить историю задач», прекрасно ложащиеся на стандартный REST. Вас, похоже, смутило то, что я назвал их «командами». Хотите, называйте из «задачами» или «намерениями» — суть та же.
> А действия над всеми ресурсами всегда одинаковые.

Есть ли в HATEOAS каноничный способ сообщить о _доступности_ этих стандартных действий, передавая некую авторизационную информацию вместе с отношениями? Конечно, сервер всегда может дать отлуп на неавторизованную операцию, но что если хочется уберечь пользователя и заранее скрыть в UI запрещённые контролы (кнопку «Удалить», например)?
Если клиент получил коллекцию из 1000 элементов, и нужно отобразить в UI, какие элементы можно удалять, а какие нет, то для каждого из них делать запрос о привилегиях? Можно, но неохота. Вопрос в том, чтобы получить привилегии вместе с данными и метаданными, одним и тем же запросом, и сделать это стандартным путём (если он есть).

Получение привилегий — частный случай получения связных ресурсов. Есть разные подходы, которые можно комбинировать:


  1. Грузить данные лишь для тех элементов, что попадают в видимую область.
  2. Использовать http2/websockets для осуществления множества легковесных запросов.
  3. Использовать пакетные запросы.
  4. Использовать специальные языки запросов для выгребания связанных данных.

Ну а конкретно в примере права делятся на группы. Например, permission=article-my — права пользователя на созданные им статьи. Независимо от пользователя и статьи. Таких наборов прав весьма ограниченное число и они замечательно кешируются.

Стандартного способа нет, это уже уровень бизнес-логики. Добавьте дополнительное поле в ресурс, сообщающее о доступности операций с ним для запросившего пользователя, или набор разрешений, необходимых пользователю для их выполнения, например. Зависит от прикладной задачи и выбранной схемы разграничения прав.
Бывает, что URL связанного ресурса есть, а его состояния ещё нету. Скажем, есть ресурс /item/123, и в его гипермедии есть линк на коллекцию /item/123/children/ — а что с этой коллекцией разрешено делать? Ну, «GET /item/123/children» наверное можно — а POST? Или есть линк на "/item/123/sibling" — а можно на нём сделать DELETE? Хранить эти разрешения в самом /item/123 кажется неестественным, логичнее хранить права доступа рядом с гиперссылками. В принципе, никто не мешает расширить линки своими атрибутами, но я надеялся что где-то уже лежит стандарт, просто я о нём не знаю. Ну нет так нет… :)
Если приложение большое и растущее, то «в духе REST» лучше использовать отдельный сервис авторизации/аутентификации (стандарт OAuth 2.0), это было бы «в духе микросервисности и масштабируемости». А в качестве стандартов для хранения и обработки правил доступа предложил бы https://m.habrahabr.ru/company/custis/blog/258861/

Это всё максимально мощные штуки, покрывающие все возможные варианты использования. Если проект не требует такой мощи, можно взять только идеи или отдельные спецификации (скажем, ограничиться ролями).

Вам не кажется странным передавать информацию и правах не в самом ресурсе, а копипастить вместе с каждой ссылкой на него, а в случае изменения прав доступа к ресурсу — предлагать перевыкачать все ресурсы, на него ссылающиеся?

Ну я же написал, что бывают случаи, когда ресурса на руках ещё нет, есть только ссылка на него. Если есть ссылка — значит УЖЕ доступны операции над ресурсом. А раз операции становятся доступны вместе со ссылкой, то и права имеет смысл привязывать к ссылке.

Что касается «протухающих» прав, то с правами, передаваемыми в состоянии та же история: они точно так же могут протухнуть. Ничего страшного: будет ошибка на сервере и последующий рефреш.

То есть ответ на мой вопрос — да?


Я ничего не говорил про протухание. Изменение прав — это далеко не только протухание.

Ответ «да, пока не кажется странным».

Под «протуханием» имелась в виду рассинхронизация информации о правах на клиенте и на сервере.

Стандарта нет, все зависит от дизайна API и задач стоящих перед API. С точки зрения стандартных операций, есть OPTIONS метод, ответ на который должен содержать список доступных HTTP методов которые можно выполнить над ресурсом. В некоторых случаях это может помочь, но если у вас логика сложная то это может быть слишком "грубо" — например вы хотите позволить клиенту делать PUT запрос с определенными данными, но запрещать передавать другие данные. Как вы понимаете, через OPTIONS этого будет сложно достичь.


Могу сказать о своем опыте: если речь идет о "единичных" ресурсах, то отсутсвие ресурса выражается в отсутсвии ссылки на него. С коллекциями мы считаем что они всегда есть, но в случае чего пустые.


Action'ы мы выставляем в основном в тех ресурсах, к которым они относятся, но бывают и исключения. Из последних примеров — есть ресурс asset — содержащий мета-информацию и у него есть action для загрузки бинарных данных для него, естественно указывающий на другой URI. Так же и с созданием элементов для коллекций — action всегда присутсвует в самом ресурсе-коллекции, но в некоторых случаях мы выставляем его в другом ресурсе, если это лучше соотносится с задачами (производительность, логичность с точки зрения семантики ресурса и т.д.).

После прочтения у меня возник следующий вопрос: если уходить от проталкивания знания клиента о действиях над ресурсами, то как будет данная схема работать, предположим страница профайла, есть ресурс Account, мне в любом случае нужно будет сходить за списком линок для данного ресурса, а затем уже по нужному релейшну сходить и получить то что мне нужно? Или это какое-то начальное знание с сервера при отрисовке темплейта? И как быть, если «типа микросервисы», т.е. темплейт рисует FE, а за данными ходим в сервисы BE?

Спасибо.
HATEOAS/HAL добавляет в ресурс знания не о действиях над этим ресурсом (действия для любого REST-ресурса всегда CRUD), а знания о связанных ресурсах. Изначальный список корневых ресурсов получается из entry-point (пример: https://morethancoding.com/2011/09/07/uri-construction-give-it-a-rest/ ). Не пытайтесь сделать RPC из REST, это не просто способ связи клиента с сервером, это именно архитектура приложения (парадигма декомпозиции прикладной задачи на ресурсы и отношения между ними).
> Или это какое-то начальное знание с сервера при отрисовке темплейта? И как быть, если «типа микросервисы», т.е. темплейт рисует FE, а за данными ходим в сервисы BE?
Композиция ресурсов — на совести клиента. Т.е., в общем случае REST-приложение — это набор RESTfull API и отдельно хостящиеся клиенты (SPA, мобильное приложение, т.д.), каждый из которых сам реализует композицию данных по-своему. Микросерверность в REST — это возможность клиента работать сразу с кучей разнородных API на разных урлах. Темплейты на сервере не рендерятся, это задача клиента, сервисы предоставляют только данные (ресурсы). Т.е., да, теймплейтами рулит FE, а API рулит BE.

Не надо пожалуйста в таком категоричном тоне про темплейты. В API Craft мейл листе люди обсуждают реализацию API с использованием html, ведь он имеет все для этого необходимое.


Нестандартно, но не запрещено :). Другое дело, что клиентов под это дело мало, но если кто реализует такое API оно будет полностью соответсвовать REST подходу.

Всегда есть те, кто не понимает, всегда есть те, кто спрашивает, всегда есть те, кто уверен в том, что отвечает. Нормальный процесс, не переживайте сильно.
В теории это очень красиво и – спору нет – на самом деле решает многие проблемы, связанные с потенциальным изменением Uri на сервере.

Но на практике, мне кажется, это будет выглядеть не так радужно.

Предположим, передо мной стоит задача: получить ресурс по известному идентификатору. Что для меня, как разработчика, проще: сформировать Uri books/123 или полезть в документацию, прочитать, как организован поиск и построить Uri books?searchField=Id&searchText=123? Это ещё хорошо, если поиск реализован. А если нет?

Боюсь, большинство разработчиков предпочтут первый вариант.
Вы полезете в документацию и выясните имена связей от entry point до интересующего вас ресурса (вместо выяснения урла). Это очень радужно на практике, и сильно проще конструкторов урла (при готовой библиотечной обвязке на клиенте для работы с гипермедией, конечно). Сквозной поиск полей по всем ресурсам, как вы его написали, вряд ли в каком-нибудь API кто-то станет делать.
Кажется, где-то выше было замечено, что в общем случае это приводит к увеличению количества запросов. Так что, как ни крути, приходится признать, что буквальное следование принципам HATEOAS приводит к некоторому снижению отказоустойчивости в архитектуре микросервисов.
В общем случае увеличится на один единственный запрос при старте приложения (чтение релейшнов точки входа). Если это «правильное» PWA, то вообще лишь при первой загрузке (кешировании) приложения. И подарит кучу бонусов взамен.
Sign up to leave a comment.