Как спроектировать правильный конечный автомат на REST

Читать в полной версии →

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

Примечание: Это адаптированный перевод статьи Designing a True REST State Machine Билла Доррфельда, технического журналиста и специалиста по API. Повествование ведётся от лица автора оригинала.

История REST и гипермедиа

Концепция гипермедиа сформировалась в 1941 году, когда аргентинский писатель Хорхе Луис Борхес написал «Сад расходящихся тропок» — рассказ, в котором страницы текста ссылаются друг на друга. Вероятно, это первый в истории пример гипертекста (на самом деле впервые гипертекст использовался в романе Джеймса Джойса «Улисс», — прим. редакции). Сейчас в массовой культуре есть множество примеров реляционных связей — от сюжета видеоигры Bioshock до серии детских романов-ужасов Роберта Стайна Goosebumps, — но во времена Борхеса такой прием был беспрецедентным.

Гипермедиа — система организации информации, элементы которой взаимосвязаны и, кроме самого гипертекста, включают в себя видео, картинки, аудио и другие типы контента.

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

Затем произошло несколько событий, которые помогли REST сформироваться в концепцию с четкой структурой:

Что такое REST

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

Заблуждение №1: REST — это просто CRUD

CRUD — акроним, который обозначает четыре базовые функции при работе с персистентными хранилищами данных: «создание, чтение, редактирование, удаление». CRUD соответствует действиям в SQL, но он плохо соотносится с методами HTTP.

Хотя GET и DELETE в REST и CRUD совпадают, POST, PUT и PATCH отвечают за разные операции. Например, в REST POST означает не только «создать» — это универсальный метод. С его помощью туннелируется весь протокол SOAP при использовании с HTTP.

Поскольку HTTP-методы не соответствуют CRUD, теоретик и популяризатор REST Асбьёрн Ульсберг утверждает, что создатели различных API должны подумать, как они могут описывать API иным способом: «Не ограничивайте себя CRUD при разработке REST API. Прочитайте спецификацию и поймите семантику каждого метода, а затем правильно используйте ее».

Если перефразировать, смысл этой цитаты сводится к тому, что REST — это стиль архитектуры, а не протокол. Таким образом, ошибочно называть «RESTful» HTTP API с операциями CRUD.

Заблуждение №2: некоторые конструкции URI «более RESTful», чем другие

Унифицированные идентификаторы ресурсов (URI) — основа концепции REST. Они позволяют определять ресурсы и действия с ними. Однако многие разработчики ошибочно считают, что могут отличить качество построения REST API просто на основе того, как структурирован URI. Проведем эксперимент: сможете ли вы сказать, какой URI «более RESTful»?

  1. http://hexlet.io/authors/contributor?author=doerrfeld
  2. http://api.hexlet.io/blogpost/getPostById?id=47
  3. http://api.hexlet.io/blogpost/47/edit-form
  4. http://api.hexlet.io/blogpost/47
  5. http://api.hexlet.io/128ndoels-8asdf-12d5-39d3

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

Заблуждение №3: API-интерфейсам REST нужны версии

Представим, что у нас есть таблица базы данных под названием «Referer». После нескольких лет использования мы заметили, что имя базы написано с ошибкой и решили изменить его на «Referrer». Клиенты уже взаимодействуют со старым именем таблицы в своих SQL-операторах, поэтому обновить имя базы будет крайне сложно — прежде придется обновить все клиенты.

Читайте также: Как устроен функциональный диалект Лиспа Clojure и почему использующие его программисты восхищаются им

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

Заблуждение №4. Гипермедиа необязательна для REST API

В интервью Майку Амудсену в 2014 году Филдинг сказал следующее: «Гипермедиа как механизм управления состоянием приложения — это ограничение REST. Это не одна из опций и не идеал, к которому нужно стремиться. Гипермедиа — это данность и ограничение. Вы либо принимаете их, либо не занимаетесь REST».

REST состоит из 6 основных ограничений — так называемых ограничений Филдинга. Он выглядит так:

При этом последний пункт — «Единообразие интерфейса» — состоит из еще нескольких подпунктов:

  1. Определение ресурсов
  2. Управление ресурсами через представление
  3. Самодостаточные сообщение
  4. Гипермедиа как механизм управления состоянием приложения (HATEOAS)

Среди них HATEOAS, вероятно, самое важное и уникальное, но наименее понятное условия REST.

По сути, HATEOAS или гипермедиа как механизм управления состоянием, — это подход к описанию ресурсов API. С его помощью можно установить инверсию контроля за ресурсами, которые клиент может вызвать, вместо того, чтобы просто перечислять каждый из них и указывать список функций, которые можно с ними выполнить. Именно благодаря этому сервер может отвечать за состояние ресурса.

Чтобы лучше понять эту идею, Ульсберг советует воспользоваться методом, предложенным Дональдом Норманом в книге «Дизайн повседневных вещей». Так же, как чашка сделана для того, чтобы ее взяли за ручку и подняли, а кнопка — чтобы быть нажатой, гипермедиа хочет сообщить вам, что делать с тем или иным ресурсом. Гипермедиа — это ссылки и метаданные для операций, которые помогают разработчикам или машинам выполнять дополнительные действия.

Читайте также: О релевантности принципов объектно-ориентированного программирования SOLID

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

Пример конечного автомата: IoT-тостер

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

Конечный автомат — это абстрактная модель, которая содержит конечное количество состояний чего-либо. В нашем случае тостер запускается после включения — переходит в состояние нагрева. Затем он достигает верхнего предела температуры и переходит в состояние ожидания, снижая температуру. Этот цикл продолжается до тех пор, пока кусок хлеба не поджарится достаточно хорошо — после этого тостер отключается.

Как управлять тостером с помощью REST и гипермедиа? Филдинг писал, что каждая страница в сети представляет собой отдельное состояние одного ресурса, которое можно получить с помощью вызова GET (этот процесс показан в таблице выше). То есть вы можете получить какие-либо данные с помощью идентификатора в URI.

Попробуем сделать это: для начала вызовем наш тостер с помощью GET:

GET /toaster HTTP/1.1

Ответ будет выглядеть примерно так:

HTTP/1.1 200 OK

{
    "Id": "/toaster",
    "state": "off",
    "operations": [{
        "rel": "on",
        "method": "PUT",
        "href": "/toaster",
        "Expects": { "state": "on"}
}]
}

В ответе содержится идентификатор, который показывает, с каким ресурсом мы работаем. Следом отображается текущее состояние ресурса, а ниже — список возможных операций, которые с ним можно совершить.

Теперь попробуем изменить состояние тостера. Для этого отправим HTTP-запрос PUT:

PUT /toaster HTTP/1.1

{
    "state": "on"
}

Ответ будет выглядеть примерно так:

HTTP/1.1 200 OK

{
    "Id": "/toaster",
    "state": "on",
    "strength": 0,
    "operations": [ {
        "rel": "on",
        "method": "PUT",
        "href": "/toaster",
        "expects": { "state": "off" }
    }, {
    "rel": "strength",
    "method": "PUT",
    "href": /fcesj48fl29304d827434j
    "expects": {
        "strength": [1,2,3,4,5,6]
        }
}]

Хорошо, теперь мы включили тостер. Однако в ответе видно, что его мощность по-прежнему на нуле — он все еще не нагревается. Попробуем вызвать изменение мощности:

PUT /toaster HTTP/1.1

{
    "strength": 3
}

Мы увеличили мощность — теперь в ответе видно, что тостер нагревается.

HTTP/1.1 200 OK

{
    "Id": "/toaster",
    "state": "heating",
    "strength": 3,
    "operations": [ {
        "rel": "on",
        "method": "PUT",
        "href": "/toaster",
        "expects": { "state": "off" }
    }, {
    "rel": "strength",
    "method": "PUT",
    "href": /fcesj48fl29304d827434j
    "expects": {
        "strength": [1,2,3,4,5,6]
        }
}]
}

Теперь мы можем перевести тостер в режим ожидания и снова увеличить мощность. Однако вместо этого отправим еще один вызов GET на идентификатор устройства:

HTTP/1.1 200 OK

{
    "Id": "/toaster",
    "state": "idle",
    "strength": 3,
    "operations": [ {
        "rel": "on",
        "method": "PUT",
        "href": "/toaster",
        "expects": { "state": "off" }
    }, {
    "rel": "strength",
    "method": "PUT",
    "href": /fcesj48fl29304d827434j
    "expects": {
        "strength": [1,2,3,4,5,6]
        }
}]
}

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

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

Заключение

Сеть функционирует как набор идей, связанных друг с другом гипертекстом. Ульсберг считает, что веб-API должны имитировать этот принцип, а важным аспектом в реализации API является гипермедиа. Для тех, кто только начинает работать с REST, создание HATEOAS-совместимого API — это огромный шаг вперед по сравнению с API-интерфейсами в стиле RPC.

«Если вы используете гипермедиа, вы можете добавлять отношения, ссылки и операции к ресурсам, не нарушая работу существующих клиентов, и наделяя новых клиентов новыми функциями», — писал Ульсберг.

Такой подход подразумевает переосмысление традиционного подхода к управлению версиями. Мнения Ульсберга и Филдинга в этом вопросе сходятся: управление версиями в REST — плохая идея. Когда вы в последний раз видели номер версии на веб-сайте? HTML не требует контроля версий, и JSON тоже в нем не нуждается.

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

Учитесь бесплатно: На Хекслете есть много бесплатных курсов по программированию, логике, английскому языку и различным инструментам для разработки