Статьи

Не ненавидят хатео

Или как я научился перестать беспокоиться и любить HATEOAS

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

Однако многие так называемые сервисы RESTful не реализуют HATEOAS (Hypermedia As The Engine Of Application State), что удерживает Роя Филдинга ночью (если вы думаете, что введение плохое, прочитайте раздел комментариев ). Это печальная тенденция, так как включение элементов управления гипермедиа предлагает много преимуществ, особенно в отсоединении клиента от сервера.

В этой статье, первой в серии из двух частей, будут рассмотрены основные детали реализации и проблемы проектирования, которые управляют REST. Мы обсудим, как реализация HATEOAS в вашем сервисе RESTful стоит дополнительных усилий, поскольку ваш сервис сталкивается с изменяющимися бизнес-требованиями.

Вторая часть, которая должна быть выпущена 28 марта, станет примером реального кода реализации службы HATEOAS с использованием Spring-HATEOAS. Вы также можете увидеть некоторые из этих концепций в моем предстоящем выступлении в группе пользователей Kansas City Spring в среду, 2 марта 2016 года, под названием « Как я научился перестать заботиться и начал любить HATEOAS ».

ОТДЫХ, триумфальная история успеха архитектурных ограничений

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

В своей докторской диссертации 2000 года Рой Филдинг определил шесть основных архитектурных стилей, ограничивающих REST. Я буду вдаваться в подробности о пяти из них; шестой, код по запросу, который является необязательным, не рассматривается. Пять счастливых стилевых ограничений: клиент-сервер, без сохранения состояния, кешируемый, унифицированный интерфейс и многоуровневая архитектура.

1. Клиент-сервер

Первое ограничение стиля — разделение клиент-сервер. По иронии судьбы, это ограничение больше всего сказывается, когда разработчики предпочитают не использовать HATEOAS.

Разделение интересов является одним из основополагающих принципов хорошего проектирования системы. В контексте REST и веб-сервисов такое разделение интересов имеет некоторые преимущества в масштабируемости, поскольку новые экземпляры сервиса RESTful также не должны обрабатывать распаковку клиента.

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

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

2. Без гражданства

Если вы спросите разработчика, что является ключевой характеристикой сервиса RESTful, одним из первых ответов, который вы, вероятно, получите, является то, что он не имеет состояния. Это популярный ответ, потому что безгражданство играет центральную роль в двух наиболее привлекательных чертах REST: долговечности и масштабируемости.

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

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

3. Кешируемый

Третье ограничение стиля заключается в том, что запрос может быть кэширован. В этом контексте кешируемость относится к способности клиента кешировать запрос. Это в отличие от размещенного на сервере кэша, такого как Redis , хотя это включено в более позднем ограничении. Кэширование клиентского запроса — это функция, которая реализована в каждом крупном браузере и активируется с помощью заголовков http, как показано на рисунке ниже (контроль кэша).

источник изображения: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en

источник изображения: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en

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

4. Единый интерфейс

Конечные точки службы RESTful — это ресурсы. Изменения в состоянии происходят через манипулирование этими ресурсами. Сообщения, отправляемые на эти ресурсы, являются самоописательными, а гипермедиа является движком состояния приложения (последнее ограничение звучит знакомо).

В следующем разделе модели зрелости Ричардсона мы рассмотрим, как выглядит реализация этих четырех ограничений в службе.

5. Многоуровневая архитектура

Как и у огров и луков, у архитектуры REST есть слои. Многоуровневая архитектура в службе RESTful достигается за счет того, что сообщения, отправляемые через нее, являются самоописательными, а каждый уровень не может видеть за пределами интерфейса следующий.

Когда я отправляю запрос на просмотр фильма на Netflix, любой клиент, которого я использую, отправляет запрос GET. Вероятно, запрос попадет в службу маршрутизации. Видя, что это запрос GET (т. Е. Поиск), эта служба маршрутизации может затем отправить этот запрос в кэш сервера. Этот кэш может проверить, есть ли у него ресурс с истекшим сроком действия, который соответствует запросу запроса. Это может продолжаться для нескольких уровней или даже областей в архитектуре Netflix, прежде чем мой запрос может быть выполнен. Вся эта маршрутизация и перенаправление могут произойти, потому что сообщение REST самоописательно. Пока слой может понимать HTTP, он может понимать полученное сообщение.

Модель зрелости Ричардсона

Итак, мы рассмотрели пять из шести основных ограничений архитектурного стиля, которые управляют REST. Давайте теперь подробнее рассмотрим ограничение четвертого стиля, унифицированный интерфейс, как было обещано ранее. Единый интерфейс — это то, что определяет «внешний вид» службы RESTful, именно там определяется конечная точка, например: GET: / users / bob. Здесь также определен HATEOAS, и в этом весь смысл этой статьи. Чтобы визуализировать влияние этих ограничений, а также увидеть, где многие службы RESTful не работают, я буду следовать полезной модели зрелости Ричардсона (RMM) в качестве руководства.

Болото оспы

Это уровень 0 на RMM. Здесь служба недобросовестно может быть описана как RESTful. Конечные точки, с которыми взаимодействует наш клиент, не являются ресурсами, мы не используем правильные HTTP-глаголы в наших запросах, и сервер не отвечает с помощью элементов управления гипермедиа. Мы все работали над подобным сервисом, действительно возможно, хотя, вероятно, маловероятно, что такой сервис прост в использовании и обслуживании… но независимо от того, что он определенно не является RESTful.

Проходя через RMM, мы будем использовать взаимодействие заказа телевизора через интернет-магазин, такой как Amazon, чтобы наблюдать, как реализация ограничений унифицированного интерфейса в REST влияет на взаимодействие между сервером и клиентом.

Здесь мы видим взаимодействие на уровне 0:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
POST: viewItem
{
    id”: “1234”
}
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00
}
POST: orderItem
{
    id” : 1,
    “items” : [
        “item” : {
            id” : 1234
        }
    ]
}
Response:
HTTP 1.1 200
{
    id” : 1,
    “items” : [
        “item” : {
            id” : 1234
        }
    ]
}

Ресурсы

На этом уровне, уровне 1 в RMM, мы реализуем первые два ограничения единого интерфейса; мы идентифицируем ресурсы, с которыми взаимодействуем через URI (/ items / 1234, / orders / 1), и то, как мы взаимодействуем с сервисом, заключается в манипулировании этими ресурсами.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
POST: /items/1234
{}
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00
}
POST: /orders/1
{
    “item” : {
        id” : 1234
    }
}
Response:
HTTP 1.1 200
{
    id” : 1,
    “items” : [
        “item” : {
            id” : 1234
        }
    ]
}

Поэтому теперь мы обращаемся к конечным точкам ресурсов вместо анонимных конечных точек, через которые будут проходить все запросы. Однако характер нашего взаимодействия с сервисом неясен. Когда мы размещаем сообщение в / items / 1234, мы создаем новый элемент или получаем его? Когда мы помещаем POST в / orders / 1, обновляем ли мы существующую сущность или создаем новую? Эти взаимодействия не понятны клиенту во время отправки запроса.

HTTP

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

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

Вызов GET всегда должен возвращать один и тот же список элементов. Запрос DELETE должен удалить элемент, но последующие запросы DELETE не должны приводить к изменению состояния сервера. Обратите внимание, это не означает, что ответ всегда должен быть одинаковым; во втором примере второй запрос DELETE может вернуть ответ об ошибке. Безопасный означает, что действие не повлияет на состояние сервера. GET только для поиска, он не изменит состояние ресурсов, которые он получает. Однако запрос PUT может привести к изменению состояния и, следовательно, не является безопасным глаголом.

СЕЙФ НЕБЕЗОПАСНЫЙ
идемпотент ПОЛУЧИТЬ, ГОЛОВУ, ТРАССИРОВКУ, ВАРИАНТЫ УДАЛИТЬ, ПОСТАВИТЬ
НЕ ИДЕМПОТЕНТ ПОЧТА

Вот как выглядит наше взаимодействие, когда мы начинаем использовать правильные HTTP-глаголы и коды состояния в наших взаимодействиях:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00
}
PUT: /orders/1
{
    “items” : [
        “item” : {
            id” : 1234
        }
    ]
}
Response:
HTTP 1.1 226
{
    “items” : [
        “item” : {
            id” : 1234
        }
    ]
}

Даже без глубокого понимания спецификации HTTP взаимодействие между клиентом и сервером становится более ясным. Мы получаем элемент с сервера; мы помещаем что-то на сервер. Есть некоторые субтитры, в которых понимание HTTP помогает, зная, что PUT означает, что модификация сообщает разработчику, что заказ уже существует, и мы изменяем его, а не создаем новый заказ (это будет запрос POST).

Понимание кодов состояния HTTP также даст разработчику больше понимания того, как сервер отвечает на запросы от клиента. В то время как наш сервер все еще возвращает соответствующий ответ 200 на наш начальный запрос GET, запрос PUT сервер теперь отправляет код ответа 226 (используется IM), что означает, что возвращается только дельта измененного ресурса. Если вы посмотрите на ответ на добавление товара в заказ в разделе «Ресурсы», сервер вернул идентификатор заказа вместе со списком товаров. В этом ответе возвращается только тот элемент, который был добавлен в заказ. Если бы в заказе уже были другие предметы, они также были бы возвращены в ответе «ресурсы», но в этом ответе опущены.

В качестве альтернативы, если не существует элемента с идентификатором 1234, вместо возврата пустого тела ответа или какого-либо сообщения об ошибке, HTTP уже определил правильный ответ. Можете ли вы угадать это?

1
2
3
GET: /items/1234
Response:
HTTP 1.1 404

Гипермедиа управления

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

Без использования HATEOAS для передачи приложения состояния клиенту клиент должен быть достаточно умен, чтобы определить, есть ли у пользователя открытый заказ, и если да, то идентификатор этого заказа. Это создает дублирование работы, поскольку бизнес-логика, определяющая состояние пользователя, теперь существует как на клиенте, так и на сервере. По мере изменения бизнеса между клиентом и сервером возникает зависимость, определяющая состояние порядка пользователя, изменения как кода клиента, так и сервера, а также необходимость координировать выпуск между ними. HATEOAS решает эту проблему, сообщая клиенту о состоянии через ссылки, которые он возвращает (то есть, что клиент может делать дальше).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “link” : {
            “rel” : “next”,
            “href” : “/orders
        }
    }
}
POST: /orders
{
    id” : 1,
    “items” : [
        {
            id” : 1234
        }
    ]
}
 
Response:
HTTP 1.1 201:
{
    id” : 1,
    “items” : [
    {
            id” : 1234
    }
]
links : [
        {
            “rel” : “next”,
            “href” : “/orders/1/payment
        },
        {
            “rel” : “self”,
            “href” : “/orders/1
        }
    ]
}

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

Смерть, налоги и перемены, случай для HATEOAS

Разработчики знают, что идиома «единственное, что является определенным, это смерть и налоги» ложна, третья уверена: изменение. Любое разработанное приложение претерпит изменения в течение срока его службы; добавлены новые бизнес-требования, изменены существующие бизнес-требования, а некоторые бизнес-требования удалены вместе.

Хотя я не обещаю, что HATEOAS станет серебряной пулей, я верю, что это одна из немногих технологий, чья выгода увеличивается при столкновении с проблемами реального мира. Ниже приведен пример трех вариантов использования, которые, взятые вместе и вместе с другими, которые можно себе представить, дают веские аргументы в пользу того, почему вы должны внедрить HATEOAS в службу RESTful.

Вариант использования 1: администраторы и обычные пользователи взаимодействуют через одного и того же клиента

Как обычные пользователи, так и администраторы используют один и тот же клиент для взаимодействия со службой. В этом случае использования обычный пользователь сможет выполнить GET только для ресурса / items, но администратор также будет иметь привилегии PUT и DELETE. Если бы мы остановились на уровне 2 в модели зрелости Ричардсона (HTTP), нам нужно было бы, чтобы клиент понимал типы привилегий, которыми обладает пользователь, чтобы правильно отобразить интерфейс для пользователя.

С HATEOAS это может быть так же просто, как клиент, отображающий некоторые новые элементы управления, отправленные сервером. Вот как могут выглядеть различия в запросах. Кроме того, мы, вероятно, не хотим, чтобы администраторы размещали заказы на товары:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Request:
[Headers]
user: bob
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “links” : [
            {
                “rel” : “next”,
                “href” : “/orders
            }
        ]  
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Request:
[ Headers ]
user: jim
roles: USER, ADMIN
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “links” : [
            {
                “rel” : “modify”,
                “href” : “/items/1234
            },
            {
                “rel” : “delete”,
                “href” : “/items/1234
            }
        ]  
    }
}

Вариант использования 2: администраторы больше не могут УДАЛИТЬ

Бизнес-требования меняются, и администраторы больше не имеют возможности УДАЛИТЬ элемент. В то время как в предыдущем случае использования было бы довольно сложно сказать, что клиентские изменения не потребуются (например, пользователю-администратору потребуется форма для изменения полей элемента), удаление глагола DELETE определенно может быть выполнено без изменения клиент.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Request:
[Headers]
user: jim
roles: USER, ADMIN
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “links” : [
            {
                “rel” : “modify”,
                “href” : “/items/1234
            }
        ]  
    }
}

Вариант использования 3: пользователи могут продавать свои товары

Бизнес теперь требует, чтобы пользователи имели возможность продавать свои собственные товары. Этот вариант использования, более чем два предыдущих, действительно начинает демонстрировать быстрое увеличение объема и сложности бизнес-логики на клиенте, а также представляет возможную связь между клиентом и сервером.

Пользователи могут продавать свои собственные предметы, но они также должны иметь возможность изменять только те предметы, которые выставляют на продажу самостоятельно. Пользователь Боб не должен иметь возможность изменять предметы пользователя Стива и наоборот. Распространенным решением этой проблемы может быть возвращение нового поля в сущности элемента, в котором указывается владелец, но теперь мы модифицируем элемент только для того, чтобы наш клиент мог правильно отобразить интерфейс для пользователя, не по какой-либо коммерческой причине.

Сейчас мы вводим связь между клиентом и сервером, и граница между ними быстро начинает стираться. С сервисом HATEOAS большая часть этой сложности, по крайней мере для клиента, удаляется, и наша сущность элемента остается неизменной. Ниже приведены примеры запросов с HATEOAS и без него. Обратите внимание, что в примере с HATEOAS ответ выглядит так же, как и ответ из варианта использования 1.

Без HATEOAS:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
Request:
[Headers]
user: jim
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “owner” : “jim”
}

С HATEOAS:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Request:
[Headers]
user: jim
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
    id” : 1234,
    “description” : “FooBar TV”,
    “image” : “fooBarTv.jpg”,
    “price” : 50.00,
    “links” : [
            {
                “rel” : “modify”,
                “href” : “/items/1234
            },
            {
                “rel” : “delete”,
                “href” : “/items/1234
            }
        ]  
    }
}

Резюме

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

Библиография

Ссылка: Не ненавидите HATEOAS от нашего партнера JCG Билли Корандо в блоге