Чтиво получилось больше философское, нежели техническое, но и для любителей технической части здесь будет над чем поразмыслить. Сомневаюсь, что скажу в этой статье что-то принципиально новое, то, о чем вы никогда не слышали, не читали и о чем не думали сами. Просто попытаюсь уложить все в единую систему, в первую очередь в своей собственной голове, а это уже дорогого стоит. Тем не менее, буду рад, если мои измышления будут вам полезны в вашей практике. Итак, поехали.
Приближение первое: Действующие лица
Клиент и сервер
Сервером в данном случае мы считаем абстрактную машину в сети, способную получить HTTP-запрос, обработать его и вернуть корректный ответ. В контексте данной статьи совершенно не важны его физическая суть и внутренняя архитектура, будь то студенческий ноутбук или огромный кластер из промышленных серверов, разбросанных по всему миру. Нам в той же мере совершенно неважно, что у него под капотом, кто встречает запрос у дверей, Apache или Nginx, какой неведомый зверь, PHP, Python или Ruby выполняет его обработку и формирует ответ, какое хранилище данных используется: Postgresql, MySQL или MongoDB. Главное, чтобы сервер отвечал главному правилу – услышать, понять и
Клиентом тоже может быть все, что угодно, что способно сформировать и отправить HTTP-запрос. До определенного момента в этой статье нам также не особо будут интересны цели, которые ставит перед собой клиент, отправляя этот запрос, как и то, что он будет делать с ответом. Клиентом может быть JavaScript-сценарий, работающий в браузере, мобильное приложение, злой (или не очень) демон, запущенный на сервере, или слишком поумневший холодильник (уже есть и такие).
По большей части мы будем говорить о способе общения между выше перечисленными двумя, таком способе, чтобы они друг друга понимали, и ни у одного не оставалось вопросов.
Философия REST
REST (Representational state transfer) изначально был задуман как простой и однозначный интерфейс для управления данными, предполагавший всего несколько базовых операций с непосредственным сетевым хранилищем (сервером): извлечение данных (GET), сохранение (POST), изменение (PUT/PATCH) и удаление (DELETE). Разумеется, этот перечень всегда сопровождался такими опциями, как обработка ошибок в запросе (корректно ли составлен запрос), разграничение доступа к данным (вдруг этого вам знать не следует) и валидация входящих данных (вдруг вы написали ерунду), в общем, всеми возможными проверками, которые сервер выполняет перед тем, как выполнить желание клиента.
Помимо этого REST имеет ряд архитектурных принципов, перечень которых можно найти в любой другой статье о REST. Пробежимся по ним кратко, чтобы они были под рукой, и не пришлось никуда уходить:
Независимость сервера от клиента – серверы и клиенты могут быть мгновенно заменены другими независимо друг от друга, так как интерфейс между ними не меняется. Сервер не хранит состояний клиента.
Уникальность адресов ресурсов – каждая единица данных (любой степени вложенности) имеет свой собственный уникальный URL, который, по сути, целиком является однозначным идентификатором ресурса.
Пример: GET /api/v1/users/25/name
Независимость формата хранения данных от формата их передачи – сервер может поддерживать несколько различных форматов для передачи одних и тех же данных (JSON, XML etc), но хранит данные в своем внутреннем формате, независимо от поддерживаемых.
Присутствие в ответе всех необходимых метаданных – помимо самих данных сервер должен возвращать детали обработки запроса, например, сообщения об ошибках, различные свойства ресурса, необходимые для дальнейшей работы с ним, например, общее число записей в коллекции для правильного отображения постраничной навигации. Мы еще пройдемся по разновидностям ресурсов.
Чего нам не хватает
Классический REST подразумевает работу клиента с сервером как с плоским хранилищем данных, при этом ничего не говорится о связанности и взаимозависимости данных между собой. Все это по умолчанию целиком ложится на плечи клиентского приложения. Однако современные предметные области, для которых разрабатываются системы управления данными, будь то социальные сервисы или системы интернет-маркетинга, подразумевают сложную взаимосвязь между сущностями, хранящимися в базе данных. Поддержка этих связей, т.е. целостности данных, находится в зоне ответственности серверной стороны, в то время, как клиент является только интерфейсом для доступа к этим данным. Так чего же нам не хватает в REST?
Вызовы функций
Необходимость выделения функций как отдельного способа управления данными продиктована тем, что вступают в силу принципы инкапсуляции методов, атомарности транзакций и поддержания целостности данных. Чтобы не менять данные и связи между ними вручную, мы просто вызываем у ресурса (единичного объекта, или коллекции) функцию и “скармливаем” ей в качестве аргумента необходимые параметры. Эта операция не подходит под стандарты REST, для нее не существует особого глагола, как и способа указания имени функции, что заставляет нас, разработчиков, выкручиваться кто во что горазд.
Самый простой пример – авторизация пользователя. Мы вызываем функцию POST /api/v1/auth/login, передаем ей в качестве аргумента объект, содержащий учетные данные, и в ответ получаем ключ доступа. Что творится с данными на серверной стороне – нас не волнует.
Еще вариант – создание и разрыв связей между сущностями. Например, добавление пользователя в группу. Вызываем у сущности группа функцию POST /api/v1/groups/1/addUser, в качестве параметра передаем объект пользователь, получаем результат. Пример, разумеется, притянут за уши, но частенько при создании связей возможны дополнительные операции с данными на серверной стороне.
А еще бывают операции, которые вообще не связаны напрямую с сохранением данных как таковых, например, рассылка уведомлений, подтверждение или отклонение каких-либо операций (завершение отчетного периода etc).
В одной из следующих статей я постараюсь классифицировать эти операции и предложить варианты возможных запросов и ответов, основываясь на том, с какими из них мне приходилось сталкиваться на практике.
Множественные операции
Часто бывает так, и разработчики клиентов поймут о чем я, что клиентскому приложению удобнее создавать/изменять/удалять/ сразу несколько однородных объектов одним запросом, и по каждому объекту возможен свой вердикт серверной стороны. Тут есть как минимум несколько вариантов: либо все изменения выполнены, либо они выполнены частично (для части объектов), либо произошла ошибка. Ну и стратегий тоже несколько: применять изменения только в случае успеха для всех, либо применять частично, либо откатываться в случае любой ошибки, а это уже тянет на полноценный механизм транзакций.
Для web-api, стремящегося к идеалу, тоже хотелось бы как-то привести подобные операции в систему. Постараюсь сделать это в одном из продолжений.
Статистические запросы, агрегаторы, форматирование данных
Частенько бывает так, что на основе хранимых на сервере данных нам нужно получить статистическую выжимку или данные, отформатированные особым образом: например, для построения графика на стороне клиента. По сути это данные, генерируемые по требованию, в той или иной мере на лету, и доступные только для чтения, так что имеет смысл вынести их в отдельную категорию. Одной из отличительных особенностей статистических данных, на мой взгляд, является то, что они не имеют уникального ID.
Уверен, что это далеко не все, с чем можно столкнуться при разработке реальных приложений, и буду рад вашим дополнениям и коррективам.
Разновидности данных
Объекты
Ключевым типом данных в общении между клиентом и сервером выступает объект. По сути, объект – это перечень свойств и соответствующих им значений. Мы можем отправить объект на сервер в запросе и получить в результат запроса в виде объекта. При этом объект не обязательно будет реальной сущностью, хранящейся в базе данных, по крайней мере, в том виде, в котором он отправлен или получен. Например, учетные данные для авторизации передаются в виде объекта, но не являются самостоятельной сущностью. Даже хранимые в БД объекты склонны обрастать дополнительными свойствами внутрисистемного характера, например, датами создания и редактирования, различными системными метками и флагами. Свойства объектов могут быть как собственными скалярными значениями, так и содержать связанные объекты и коллекции объектов, которые не являются частью объекта. Часть свойств объектов может быть редактируемой, часть системной, доступной только для чтения, а часть может носить статистический характер и вычисляться на лету (например, количество лайков). Некоторые свойства объекта могут быть скрыты, в зависимости от прав пользователя.
Коллекции объектов
Говоря о коллекциях, мы подразумеваем разновидность серверного ресурса, позволяющую работать с перечнем однородных объектов, т.е. добавлять, удалять, изменять объекты и осуществлять выборку из них. Помимо этого коллекция теоретически может обладать собственными свойствами (например, максимальное число элементов на страницу) и функциями (тут я в замешательстве, но такое тоже было).
Скалярные значения
В чистом виде скалярные значения как отдельная сущность на моей памяти встречались крайне редко. Обычно они фигурировали как свойства объектов или коллекций, и в этом качестве они могут быть доступны как для чтения, так и для записи. Например, имя пользователя может быть получено и изменено в индивидуальном порядке GET /api/v1/users/1/name. На практике эта возможность пригождается редко, но в случае необходимости хотелось бы, чтобы она была под рукой. Особенно это касается свойств коллекции, например числа записей (с фильтрацией или без нее): GET /api/v1/news/count.
Файлы
Файлы есть файлы — их стоит рассматривать как единую нечленимую единицу. Другой вопрос в том, что в большинстве случаев при сохранении файла в базе может создаваться служебная сущность, содержащая метаданные этого файла: размер, настоящее имя, статус etc.
Продолжение следует...
Комментарии (9)
Accetone
12.10.2016 16:16Почему не подойдет следующее:
Для авторизации:
POST /api/v1/sessions
Для вступления пользователя в группу:
POST /api/v1/groups/42/members
POST /api/v1/groups/42/moderators
marapper
12.10.2016 16:34Семантичнее и в стиле CRUD
POST /api/v1/member/42/groups
{groups: [1, 2, 3]}
Defff
13.10.2016 02:41Как-то достаточно сложная архитектура у Автора
Зачастую удобнее использовать идентификатор пользователя(автора) в каждом из создаваемых ключей(в качестве его части), а в АPI иметь поисковый сборщик по ключам( значение поиска по имени ключа задаём в запросе нужной регуляркой), сборщик отдаёт на клиента все найденные ключи вместе с их значениями, тем самым мы можем избежать конфликтов одновременной попытки изменения-модификации объекта данного ключа разными пользователями, так как каждый ключ пишется только одним юзером. Исчезают проблемы конфликтов одновременного доступа на запись-модификацию.
napa3um
Вы не поняли сути различия REST и RPC. Это равномощные концепции, выразимые друг через друга, но смешивать их вредно. В терминах REST выполнение процедуры из RPC будет созданием нового одиночного ресурса «операция». REST придуман для того, чтобы явно выражать _состояние_ персистентных моделей на клиенте и сервере и безконфликтной синхронизации между ними. REST — это набор ограничений (на виды ресурсов и операции с ними) над RPC, способ упорядочивания «произвольных операций», чтобы они с меньшей вероятностью противоречили друг другу и не приводили к неконсистентным состояниям моделей. RPC — это уличная драка клиента с сервером, REST — этот поединок в стиле джиуджитсу.
napa3um
Ваш пример о включении одного ресурса в другой (пользователя в группу модераторов, например) в концепции REST будет созданием ресурса «членствоПользователяВГруппе», без необходимости придумывать для этого отдельную семантику операции над RPC (например, не придётся заботиться об идемпотентности этой операции или о видах ошибок при её выполнении).
marapper
Странное смешение.
RPC — глаголы-методы-ресурсы, единая точка входа, автодискаверинг методов по ней, обычно точно описанный протокол типа SOAP, xml-rpc, json-rpc
REST — архитектура гипертекстовых операций, CRUD, урлы-ресурсы и глаголы в методах. Все остальное, точки входа, дискаверинг, документация — на совести реализаций, типа HATEOAS, JSON API, oData.
napa3um
Вы смешали архитектурный стиль с конкретными реализациями. В архитектурном смысле RPC (Remote Procedure Call) — вызов _произвольных_ методов, REST (REpresentational State Transfer) — вызов _ограниченного_ набора методов изменения состояния ресурсов. (HATEOAS/HAL в REST/HTTP и WSDL в RPC/SOAP — отдельная тема, которой я не касался в своём комментарии.)