diff --git a/src/ru/drafts/The Mythology of REST.md b/src/ru/drafts/The Mythology of REST.md new file mode 100644 index 0000000..b0be6f8 --- /dev/null +++ b/src/ru/drafts/The Mythology of REST.md @@ -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 +// Удаление профиля +DELETE /user/{user_id} +Authorization: Bearer +``` + +Теперь 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 и многослойности. +