mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-03-17 20:42:26 +02:00
desconstructing REST part 1
This commit is contained in:
parent
4611844902
commit
2893748266
113
src/ru/drafts/The Mythology of REST.md
Normal file
113
src/ru/drafts/The Mythology of REST.md
Normal file
@ -0,0 +1,113 @@
|
||||
### Мифология REST
|
||||
|
||||
#### История и фактология
|
||||
|
||||
Мало какая технология в истории IT вызывала столько ожесточённых споров, как REST. Самое удивительное при этом состоит в том, что спорящие стороны, как правило, совершенно не представляют себе предмет спора.
|
||||
|
||||
Начнём с самого начала. В 2000 году один из авторов спецификаций HTTP и URI Рой Фьелдинг защитил докторскую диссертацию на тему «Архитектурные стили и дизайн архитектуры сетевого программного обеспечения», пятая глава которой была озаглавлена как «Representational State Transfer (REST)». Вот она: https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm.
|
||||
|
||||
Как нетрудно убедиться, прочитав эту главу, она представляет собой довольно абстрактный обзор распределённой сетевой архитектуры, вообще не привязанный ни к HTTP, ни к URL. Более того, глава вообще-то посвящена вовсе не правилам дизайна API; в ней Фьелдинг методично перечисляет ограничения, с которыми приходится сталкиваться разработчику распределённого сетевого программного обеспечения. Вот они:
|
||||
|
||||
* клиент и сервер не знают внутреннего устройства друг друга (клиент-серверная архитектура);
|
||||
* сессия хранится на клиенте (stateless-дизайн);
|
||||
* данные должны размечаться как кэшируемые или некэшируемые;
|
||||
* интерфейсы взаимодействия должны быть стандартизированы;
|
||||
* сетевые системы являются многослойными, т.е. сервер может быть только прокси к другим серверам;
|
||||
* функциональность клиента может быть расширена через поставку кода с сервера.
|
||||
|
||||
Всё, на этом определение REST заканчивается. Дальше Фьелдинг конкретизирует некоторые аспекты имплементации систем в указанных ограничениях, но все они точно так же являются совершенно абстрактными. Буквально: «ключевая информационная абстракция в REST — ресурс; любая информация, которую можно назвать, может быть ресурсом».
|
||||
|
||||
Ключевой вывод, который следует из определения REST по Фьелдингу, вообще-то, таков: *любое сетевое ПО в мире соответствует принципам REST*, за очень-очень редкими исключениями.
|
||||
|
||||
В самом деле, я с большим трудом могу себе представить систему, в которой не было хоть какого-нибудь стандартизованного интерфейса взаимодействия, иначе её просто невозможно будет разрабатывать. Раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнено. Раз можно сделать альтернативную имплементацию сервера — значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером. Поскольку клиент представляет собой вычислительную машину - он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные. Наконец, code-on-demand вообще лукавое требование, поскольку всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
|
||||
|
||||
Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера — клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S («stateless»), то систем, в которых сервер *вообще не хранит никакого контекста клиента* в мире вообще практически нет, поскольку ничего полезного для клиента в такой системе сделать нельзя. (Что, кстати, постулируется в соответствующем разделе прямым текстом: «клиент не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст».)
|
||||
|
||||
Наконец, сам Фьелдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году [разъяснение](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven), что же он имел в виду. В частности, в этой статье утверждается, что:
|
||||
* REST API не должно зависеть от протокола;
|
||||
* разработка REST API должна фокусироваться на описании медиатипов, представляющих ресурсы; при этом клиент вообще ничего про эти медиатипы знать не должен;
|
||||
* в REST API не должно быть фиксированных имён ресурсов и операций над ними, клиент должен извлекать эту информацию из ответов сервера.
|
||||
|
||||
Короче говоря, REST по Фьелдингу подразумевает, что клиент, получив каким-то образом ссылку на точку входа REST API, далее должен быть в состоянии полностью выстроить взаимодействие с этим API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода, чтобы с этим API взаимодействовать.
|
||||
|
||||
Оставляя за скобками тот факт, что Фьелдинг весьма вольно истолковал свою же диссертацию, просто отметим, что ни одна существующая система в мире не удовлетворяет описанию REST по Фьелдингу-2008.
|
||||
|
||||
#### Здравое зерно REST
|
||||
|
||||
Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно диссертация Фьелдинга обрела столь широкую популярность; очевидно другое: теория Фьелдинга, преломившись в умах миллионов программистов (включая самого Фьелдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера «RESTful API», [конкретного смысла которой никто не знает](https://restfulapi.net/).
|
||||
|
||||
Хотим ли мы тем самым сказать, что REST является бессмысленной концепцией? Отнюдь нет. Мы только хотели показать, что она допускает чересчур широкую интерпретацию, в чём одновременно кроется и её сила, и её слабость.
|
||||
|
||||
С одной стороны, благодаря многообразию интерпретаций, разработчики API выстроили какое-то размытое, но всё-таки полезное представление о «правильной» архитектуре API. С другой стороны, если бы Фьелдинг чётко расписал в 2000 году, что же он конкретно имел в виду, вряд ли бы об этой диссертации знало больше пары десятков человек.
|
||||
|
||||
Что же «правильного» в REST-подходе к дизайну API (таком, как он сформировался в коллективном сознании широких масс программистов)? То, что такой дизайн позволяет добиться двух целей: более эффективно использовать время программистов и машинное время.
|
||||
|
||||
Если коротко обобщить все сломанные в попытках определить REST копья, то мы получим следующий принцип: *хорошо*, если агенты распределённой системы могут читать мета-информацию запросов и ответов, проходящих через систему.
|
||||
|
||||
У протокола HTTP есть очень важное достоинство: он предоставляет стороннему наблюдателю довольно подробную информацию о том, что произошло с запросом и ответом, даже если этот наблюдатель ничего не знает о их семантике:
|
||||
|
||||
* URL идентифицирует некоего конечного получателя запроса, какую-то единицу адресации;
|
||||
* по статусу ответа можно понять, выполнена ли операция успешно или произошла ошибка; если имеет место быть ошибка — то можно понять, кто виноват (клиент или сервер), и даже в каких-то случаях понять, что же конкретно произошло;
|
||||
* по методу запроса можно понять, является ли операция модифицирующей; если операция модифицирующая, то можно выяснить, является ли она идемпотентной;
|
||||
* по методу запроса и заголовкам ответа можно понять, кэшируем ли результат операции, и, если да, то какова политика кэширования.
|
||||
|
||||
Немаловажно здесь то, что все эти сведения можно получить, не вычитывая тело ответа целиком — достаточно прочитать служебную информацию из заголовков.
|
||||
|
||||
Почему это *полезно*? Потому что современный стек взаимодействия между клиентом и сервером является довольно сложным. Разработчик пишет код поверх какого-то фреймворка, который отправляет запросы; фреймворк базируется на API языка программирования, которое, в свою очередь, обращается к API операционной системы. Далее запрос (возможно, через промежуточные HTTP-прокси) доходит до сервера, который, в свою очередь, тоже представляет собой несколько слоёв абстракции в виде фреймворка, языка программирования и ОС; к тому же, перед конечным сервером, как правило, находится веб-сервер, проксирующий запрос, а зачастую и не один. В современных облачных архитектурах HTTP-запрос, прежде чем дойти до конечного обработчика, пройдёт через несколько абстракций в виде прокси и gateway-ев. Если бы все эти агенты трактовали мета-информацию о запросе одинаково, это позволило бы трактовать многие ситуации оптимальнее.
|
||||
|
||||
(На самом деле, в отношении многих технических аспектов промежуточные агенты и так позволяют себе разные вольности, не спрашивая разработчиков. Например, свободно менять Accept-Encoding и Content-Length.)
|
||||
|
||||
Каждый из аспектов, перечисленных Фьелдингом в REST-принципах, позволяет лучше организовать работу промежуточного ПО. Ключевым здесь является stateless-принцип: промежуточные прокси могут быть уверены, что метаинформация запроса его однозначно описывает.
|
||||
|
||||
Приведём простой пример. Пусть в нашей системе есть операции получения профиля пользователя и его удаления. Мы можем организовать их разными способами. Например, вот так:
|
||||
|
||||
```
|
||||
// Получение профиля
|
||||
GET /me
|
||||
Cookie: session_id=<идентификатор сессии>
|
||||
// Удаление профиля
|
||||
GET /delete-me
|
||||
Cookie: session_id=<идентификатор сессии>
|
||||
```
|
||||
|
||||
Почему такая система неудачна с точки зрения сервера?
|
||||
|
||||
1. Сервер не может кэшировать ответы; все /me для него одинаковые, поскольку он не умеет получать уникальный идентификатор пользователя из куки; в том числе промежуточные прокси не могут и заранее наполнить кэш, так как не знают идентификаторов сессий.
|
||||
2. На сервере сложно организовать шардирование, т.е. хранение информации о разных пользователях в разных сегментах сети; для этого опять же потребуется уметь обменивать сессию на идентификатор пользователя.
|
||||
|
||||
Первую проблему можно решить, сделав операции более машиночитаемыми, например, перенеся идентификатор сессии в URL:
|
||||
|
||||
```
|
||||
// Получение профиля
|
||||
GET /me?session_id=<идентификатор сессии>
|
||||
// Удаление профиля
|
||||
GET /delete-me?session_id=<идентификатор сессии>
|
||||
```
|
||||
|
||||
Шардирование всё ещё нельзя организовать, но теперь сервер может иметь кэш (в нём будут появляться дубликаты для разных сессий одного и того же пользователя, но хотя бы ответить из кэша неправильно будет невозможно), но возникнут другие проблемы:
|
||||
|
||||
1. URL обращения теперь нельзя сохранять в логах, так как он содержит секретную информацию; более того, появится риск утечки данных со страниц пользователей, т.к. одного URL теперь достаточно для получения данных.
|
||||
2. Ссылку на удаление пользователя клиент обязан держать в секрете. Если её, например, отправить в мессенджер, то робот-префетчер мессенджера удалит профиль пользователя.
|
||||
|
||||
Как же сделать эти операции правильно с точки зрения REST? Вот так:
|
||||
|
||||
```
|
||||
// Получение профиля
|
||||
GET /user/{user_id}
|
||||
Authorization: Bearer <token>
|
||||
// Удаление профиля
|
||||
DELETE /user/{user_id}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Теперь URL запроса в точности идентифицирует ресурс, к которому обращаются; теперь можно, в частности, организовать маршрутизацию запроса в зависимости от идентификатора пользователя. Префетчер мессенджера не пройдёт по DELETE-ссылке; а если он это и сделает, то без заголовка Authorization операция выполнена не будет.
|
||||
|
||||
Наконец, неочевидная польза такого решения заключается в следующем: промежуточный сервер-гейтвей, обрабатывающий запрос, может проверить заголовок Authorization и переслать запрос далее без него (желательно, конечно, по безопасному соединению или хотя бы подписав запрос). Тогда во внутренней среде можно будет свободно организовывать кэширование данных в любых промежуточных узлах. Более того, гейтвей может модифицировать операцию: например, для авторизованных пользователей пересылать запрос дальше как есть, а для неавторизованным показывать публичный профиль, пересылая запрос на специальный URL, ну, скажем, `GET /user/{user_id}/public-profile`.
|
||||
|
||||
**Разумеется** все подобные оптимизации можно выполнить и без опоры на стандартную номенклатуру методов / статусов / заголовков HTTP, или даже вовсе поверх другого протокола. Достаточно разработать одинаковый формат данных, содержащий нужную мета-информацию, и научить промежуточные агенты его читать. В общем-то, именно это Фьелдинг и утверждает в своей диссертации.
|
||||
|
||||
Заметим, что вышесказанное не имеет абсолютно ничего общего с многочисленными советами «как правильно разрабатывать REST API», которые можно найти в интернете:
|
||||
* неважно, есть ли в ваших URL глаголы — важно, являются ли они достаточно машиночитаемыми, чтобы правильно устроить роутинг;
|
||||
* неважно, хорошо ли HTTP-глагол подходит для описания того, что происходит с ресурсом — важно, что он указывает, является ли операция (не)модифицирующей, (не)кэшируемой, (не)идемпотентной; вам может казаться, что `DELETE /list?element_index=3` прекрасно описывает ваше намерение удалить третий элемент списка, но эта операция неидемпотентна, и использовать здесь метод `DELETE` нельзя;
|
||||
* сведение всего API к набору CRUD-операций не имеет ничего общего с парадигмой REST; просто разрабатывайте их сигнатуры, держа в голове принципы stateless и многослойности.
|
||||
|
Loading…
x
Reference in New Issue
Block a user