mirror of
https://github.com/twirl/The-API-Book.git
synced 2024-11-24 08:02:25 +02:00
refactoring
This commit is contained in:
parent
8c4b7f8ba8
commit
ec34795923
476
docs/API.ru.html
476
docs/API.ru.html
File diff suppressed because one or more lines are too long
@ -226,7 +226,7 @@ ul.table-of-contents {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul.table-of-contents li {
|
||||
ul.table-of-contents > li > a {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@ -234,8 +234,7 @@ ul.table-of-contents ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
ul.table-of-contents ul li {
|
||||
text-transform: none;
|
||||
ul.table-of-contents li {
|
||||
font-family: local-sans;
|
||||
}
|
||||
|
||||
|
@ -12,14 +12,14 @@
|
||||
|
||||
**API — это обязательство**. Формальное обязательство связывать между собой различные программируемые контексты.
|
||||
|
||||
Когда меня просят привести пример хорошего API, я обычно показываю фотографию древнеримского акведука:
|
||||
Когда автора этой книги просят привести пример хорошего API, он обычно показывает фотографию древнеримского акведука:
|
||||
|
||||
[![igorelick @ pixabay](/img/pont-du-gard.jpg "Древнеримский акведук Пон-дю-Гар. Построен в I веке н.э.")](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
[![igorelick @ pixabay](/img/pont-du-gard.jpg "Древнеримский акведук Пон-дю-Гар. Построен в I веке н.э")](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
|
||||
* он связывает между собой две области
|
||||
* он связывает между собой две области;
|
||||
* обратная совместимость нарушена ноль раз за последние две тысячи лет.
|
||||
|
||||
Отличие древнеримского акведука от хорошего API состоит лишь в том, что API предлагает _программный_ контракт. Для связывания двух областей необходимо написать некоторый _код_. Цель этой книги: помочь вам разработать API, так же хорошо выполняющий свою задачу, как и древнеримский акведук.
|
||||
Отличие древнеримского акведука от хорошего API состоит лишь в том, что API предлагает _программный_ контракт. Для связывания двух областей необходимо написать некоторый _код_. Цель настоящей книги состоит в том, чтобы помочь вам разработать API, так же хорошо выполняющий свою задачу, как и древнеримский акведук.
|
||||
|
||||
Акведук хорошо иллюстрирует и другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не можете контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.
|
||||
|
||||
|
@ -1,36 +1,17 @@
|
||||
### [Обзор существующих решений в области разработки API][intro-api-solutions-overview]
|
||||
### [Критерии качества API][intro-api-quality]
|
||||
|
||||
Цель первых трёх разделов настоящей книги — поговорить о дизайне API вне привязки к какой-либо конкретной технологии. Изложенные нами принципы равно применимы как к веб-сервисам, так и, например, к интерфейсам операционных систем.
|
||||
Прежде чем переходить к изложению рекомендаций по проектированию архитектуры API, нам следует определиться с тем, что мы считаем качественным API, и какую пользу мы получаем от того, что наш API «качественный». Достаточно очевидно, что качество API определяется в первую очередь тем, насколько хорошо он помогает разработчикам решать стоящие перед ними задачи. (Оставим за скобками ситуации, когда вендор API преследует какие-то свои неявные цели, отличные от предоставления полезного продукта.)
|
||||
|
||||
Тем не менее, два больших сценария использования явно доминируют, когда мы говорим о разработке API:
|
||||
* разработка клиент-серверных приложений;
|
||||
* разработка клиентских SDK.
|
||||
Как же дизайн API может помочь разработчику? Очень просто: API должен решать задачи *максимально удобно и понятно*. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:
|
||||
|
||||
В первом случае мы говорим практически исключительно об API, работающих поверх протокола HTTP. В настоящий момент клиент-серверное взаимодействие, не опирающееся на HTTP, можно встретить разве что в протоколах WebSocket (хотя и он может быть использован совместно с HTTP, что часто и происходит) и MQTT, а также в различных форматах потокового вещания и других узкоспециализированных интерфейсах.
|
||||
* из структуры вашего API должно быть максимально очевидно, как решить ту или иную задачу; в идеале разработчику должно быть достаточно одного взгляда на документацию, чтобы понять, с помощью каких сущностей следует решать поставленную задачу;
|
||||
* API должен быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!);
|
||||
* немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;
|
||||
* API должен быть консистентен: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать;
|
||||
* желательно при этом, чтобы API соответствовал принципам и правилам платформы и используемого фреймворка.
|
||||
|
||||
#### HTTP API
|
||||
Однако статическое удобство и понятность API — это относительно простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.
|
||||
|
||||
Несмотря на кажущуюся гомогенность технологии ввиду использования одного протокола прикладного уровня, в действительности же наблюдается значительное разнообразие подходов к имплементации HTTP API.
|
||||
Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простой и понятный API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
|
||||
|
||||
**Во-первых**, существующие реализации различаются подходом к утилизации собственно протокола HTTP:
|
||||
* либо клиент-серверное взаимодействие опирается на описанные в стандарте HTTP возможности (точнее было бы сказать, «стандартах HTTP», поскольку функциональность протокола описана во множестве разных RFC);
|
||||
* либо HTTP утилизируется как транспорт, и поверх него выстроен дополнительный уровень абстракции (т.е. возможности HTTP, такие как номенклатура ошибок или заголовков, сознательно редуцируются до минимального уровня, а вся мета-информация переносится на уровень вышестоящего протокола).
|
||||
|
||||
К первой категории относятся API, которые принято обозначать словом «REST» или «RESTful». Вторая категория, в основном, представлена разными видами RPC-протоколов.
|
||||
|
||||
**Во-вторых**, реализации HTTP API опираются на разные форматы передаваемых данных:
|
||||
* REST API и некоторые RPC (JSON-RPC[ref JSON-RPC](https://www.jsonrpc.org/), GraphQL[ref GraphQL](https://graphql.org/)) полагаются в основном на формат JSON[ref JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) (опционально дополненный передачей бинарных файлов);
|
||||
* gRPC[ref gRPC](https://grpc.io/), а также Apache Avro[ref Apache Avro](https://avro.apache.org/docs/) и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как Protocol Buffers[ref Protocol Buffers](https://protobuf.dev/), FlatBuffers[ref FlatBuffers](https://flatbuffers.dev/) и собственный формат Apache Avro);
|
||||
* наконец, некоторые RPC-протоколы (SOAP[ref SOAP](https://www.w3.org/TR/soap12/), XML-RPC[ref XML-RPC](http://xmlrpc.com/)) используют для передачи данных формат XML[ref Extensible Markup Language (XML)](https://www.w3.org/TR/xml/) (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
|
||||
|
||||
Все перечисленные технологии оперируют существенно разными парадигмами — и вызывают естественным образом большое количество холиваров — хотя на момент написания этой книги можно констатировать, что для API общего назначения выбор практически сводится к триаде «REST API (фактически, JSON over HTTP) против gRPC против GraphQL».
|
||||
|
||||
HTTP API будет посвящён раздел IV; мы также отдельно и подробно рассмотрим концепцию «REST API», поскольку, в отличие от GRPC и GraphQL, она является более гибкой и низкоуровневой — но и приводит к большему непониманию по той же причине.
|
||||
|
||||
#### SDK
|
||||
|
||||
Понятие SDK (Software Development Kit, «набор для разработки программного обеспечения»), вообще говоря, вовсе не относится к API: это просто термин для некоторого набора программных инструментов. Однако, как и за «REST», за ним закрепилось некоторое определённое толкование — как клиентского фреймворка для работы с некоторым API. Это может быть как обёртка над клиент-серверным API, так и UI-библиотека в рамках какой-то платформы. Существенным отличием от вышеперечисленных API является то, что «SDK» реализован для какого-то конкретного языка программирования и предоставляет возможность работать с низкоуровневым API нижележащей платформы.
|
||||
|
||||
В отличие от клиент-серверных API, обобщить такие SDK не представляется возможным, т.к. каждый из них написан под конкретное сочетание язык программирования-платформа. Из интероперабельных технологий в мире SDK можно привести в пример кросс-платформенные мобильные (React Native[ref React Native](https://reactnative.dev/), Flutter[ref Flutter](https://flutter.dev/), Xamarin[ref Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin)) и десктопные фреймворки (JavaFX[ref JavaFX](https://openjfx.io/), QT[ref QT](https://www.qt.io/)) и некоторые узкоспециализированные решения (Unity[ref Unity](https://docs.unity3d.com/Manual/index.html)), однако все они направлены на работу с конкретными технологиями и весьма специфичны.
|
||||
|
||||
Тем не менее, SDK обладают общностью *на уровне задач*, которые они решают, и именно этому (решению проблем трансляции и предоставления UI-компонент) будет посвящён раздел V настоящей книги.
|
||||
Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивался во времени и не превращался в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. **Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг**.
|
||||
|
@ -1,17 +1,11 @@
|
||||
### [Критерии качества API][intro-api-quality]
|
||||
### [Выбор подхода к разработке API][intro-api-choosing-solutions]
|
||||
|
||||
Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наш API «хороший».
|
||||
Вернёмся к нашей аналогии API как акведука, соединяющего два различных контекста. Пытаясь сделать подключение к нашему сооружению удобным для потребителей, мы также сталкиваемся и с другой стороной вопроса: а как им (потребителям) было бы удобно? Как они привыкли? Нет ли в нашей предметной области каких-то распространённых стандартов подключения к водопроводу?
|
||||
|
||||
Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой, предлагая разработчикам API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее предполагается, что API существует в первую очередь для того, чтобы разработчики решали с его помощью свои задачи, а не ради каких-то завуалированных целей).
|
||||
В абсолютном большинстве случаев такие стандарты есть: кто-то уже разрабатывал похожие API раньше. Чем дальше отстоят друг от друга два контекста, тем большее количество разнообразных абстракций и фреймворков работы с ними будет существовать.
|
||||
|
||||
Как же дизайн API может помочь разработчику? Очень просто: API должен решать задачи *максимально удобно и понятно*. Путь разработчика от формулирования своей задачи до написания работающего кода должен быть максимально коротким. Это, в том числе, означает, что:
|
||||
Использование привычных механизмов — важная составляющая качества API. Там, где есть устоявшийся стандарт взаимодействия (скажем, протокол TCP/IP), изобретать свой собственный стандарт рекомендуется только в тех случаях, когда вы стопроцентно уверены, что преимущества нового подхода будут настолько очевидны, что вам простят необходимость изучить новую технологию для работы с API.
|
||||
|
||||
* из структуры вашего API должно быть максимально очевидно, как решить ту или иную задачу; в идеале разработчику должно быть достаточно одного взгляда на документацию, чтобы понять, с помощью каких сущностей следует решать поставленную задачу;
|
||||
* API должен быть читаемым: в идеале разработчик, просто глядя в номенклатуру методов, сразу пишет правильный код, не углубляясь в детали (особенно — детали реализации!); немаловажно уточнить, что из интерфейсов объектов должно быть понятно не только решение задачи, но и возможные ошибки и исключения;
|
||||
* API должен быть консистентен: при разработке новой функциональности, т.е. при обращении к каким-то незнакомым сущностям в API, разработчик может действовать по аналогии с уже известными ему концепциями API, и его код будет работать.
|
||||
|
||||
Однако статическое удобство и понятность API — это простая часть. В конце концов, никто не стремится специально сделать API нелогичным и нечитаемым — всегда при разработке мы начинаем с каких-то понятных базовых концепций. При минимальном опыте проектирования сложно сделать ядро API, не удовлетворяющее критериям очевидности, читаемости и консистентности.
|
||||
|
||||
Проблемы возникают, когда мы начинаем API развивать. Добавление новой функциональности рано или поздно приводит к тому, что некогда простой и понятный API становится наслоением разных концепций, а попытки сохранить обратную совместимость приводят к нелогичным, неочевидным и попросту плохим решениям. Отчасти это связано так же и с тем, что невозможно обладать полным знанием о будущем: ваше понимание о «правильном» API тоже будет меняться со временем, как в объективной части (какие задачи и каким образом решает API), так и в субъективной — что такое очевидность, читабельность и консистентность для вашего API.
|
||||
|
||||
Принципы, которые мы будем излагать ниже, во многом ориентированы именно на то, чтобы API правильно развивался во времени и не превращался в нагромождение разнородных неконсистентных интерфейсов. Важно понимать, что такой подход тоже не бесплатен: необходимость держать в голове варианты развития событий и закладывать возможность изменений в API означает избыточность интерфейсов и возможно излишнее абстрагирование. И то, и другое, помимо прочего, усложняет и работу программиста, пользующегося вашим API. **Закладывание перспектив «на будущее» имеет смысл, только если это будущее у API есть, иначе это попросту оверинжиниринг**.
|
||||
Во многих областях, однако, подобной определённости нет; напротив, существуют многочисленные конкурирующие друг с другом парадигмы разработки API, и вам придётся сделать выбор в пользу одной из них (или в пользу разработки собственного подхода). Две такие области мы рассмотрим в разделах IV и V настоящей книги:
|
||||
* выбор парадигмы организации клиент-серверного взаимодействия (REST API, RPC, GraphQL) — в главе «[Преимущества и недостатки HTTP API в сравнении с альтернативными технологиями](#http-api-pros-and-cons)»;
|
||||
* выбор подхода к написанию UI компонентов — в главе «[Терминология. Обзор технологий разработки SDK](#sdk-toc)».
|
@ -1,22 +1,45 @@
|
||||
### [О концепции HTTP API и терминологии][http-api-concepts]
|
||||
### [О концепции HTTP API. Парадигмы разработки клиент-серверного взаимодействия][http-api-concepts]
|
||||
|
||||
Вопросы организации HTTP API — к большому сожалению, одни из самых «холиварных». Будучи одной из самых популярных и притом весьма непростых в понимании технологий (ввиду большого по объёму и фрагментированного на отдельные RFC стандарта), спецификация HTTP обречена быть плохо понятой и превратно истолкованной миллионами разработчиков и многими тысячами учебных пособий. Поэтому, прежде чем переходить непосредственно к полезной части настоящего раздела, мы обязаны дать уточнения, о чём же всё-таки пойдёт речь.
|
||||
|
||||
Так сложилось, что в настоящий момент сетевой стек, используемый для разработки клиент-серверных API, практически полностью унифицирован в двух точках. Одна из них — это Internet protocol suite, состоящий из базового протокола IP и надстройки в виде TCP или UDP над ним. На сегодняшний день альтернативы TCP/IP используются в чрезвычайно ограниченном спектре задач, и средний разработчик практически не сталкивается ни с каким другим сетевым стеком. Однако у TCP/IP с прикладной точки зрения есть существенный недостаток — он оперирует поверх системы IP-адресов, которые плохо подходят для организации распределённых систем:
|
||||
Начнём с небольшой исторической справки. Выполнение запросов пользователя на удалённом сервере — одна из базовых задач в программировании ещё со времён мейнфреймов, естественным образом получившая новый импульс развития с появлением ARPANET. Хотя первые высокоуровневые протоколы сетевого взаимодействия работали в терминах отправки по сети сообщений (см., например, протокол DEL предложенный в одном из самых первых RFC — RFC-5 от 1969 года[ref RFC-5. DEL](https://datatracker.ietf.org/doc/html/rfc5)), довольно быстро теоретики пришли к мысли, что было бы гораздо удобнее, если бы обращение к удалённому узлу сети и использование удалённых ресурсов *с точки зрения сигнатуры вызова* ничем не отличались от обращения к локальной памяти и локальным ресурсам. Эту концепцию строго сформулировал под названием Remote Procedure Call (RPC) в 1981 году сотрудник знаменитой лаборатории XEROX в Пало-Альто Брюс Нельсон[ref Nelson, B. J. (1981) Remote procedure call](https://www.semanticscholar.org/paper/Remote-procedure-call-Nelson/c860de40a88090055948b72d04dd79b02195e06b) и он же был соавтором первой практической имплементации предложенной парадигмы — Sun RPC[ref Birrell, A. D., Nelson, B. J. (1984) Implementing remote procedure calls](https://dl.acm.org/doi/10.1145/2080.357392)[ref RPC: Remote Procedure Call Protocol Specification](https://datatracker.ietf.org/doc/html/rfc1050), существующей и сегодня под названием ONC RPC.
|
||||
|
||||
Первые широко распространённые сетевые протоколы — такие как упомянутый Sun RPC, Java RMI[ref Remote Method Invocation (RMI)](https://www.oracle.com/java/technologies/javase/remote-method-invocation-home.html), CORBA[ref CORBA](https://www.corba.org/) — чётко следовали парадигме RPC. Технология позволила сделать именно то, о чём писал Нельсон — не различать локальное и удалённое исполнение кода. Вся «магия» скрыта внутри обвязки, которая генерирует имплементации удалённых функций, а с форматом передачи данных разработчик и вовсе не сталкивается.
|
||||
|
||||
Удобство использования технологии, однако, оказалось её же Ахиллесовой пятой:
|
||||
* требование работы с удалёнными вызовами как с локальными приводит к высокой сложности протокола в силу необходимости поддерживать разнообразные возможности высокоуровневых языков программирования;
|
||||
* RPC первого поколения диктуют выбор языка и платформы и для клиента, и для сервера;
|
||||
* Sun RPC не работал под Windows, Java RMI требовал виртуальную машину Java для работы;
|
||||
* некоторые протоколы (CORBA, в частности) декларировали возможность разработки адаптеров для любого языка программирования, однако фактическая имплементация поддержки произвольных языков оказалась весьма сложной;
|
||||
* возможность адресовать объекты в памяти удалённого сервера так же, как и локальные накладывает огромные ограничения на масштабирование такой системы;
|
||||
* любопытно, что ни один заметный RPC-протокол управление разделяемой памятью не реализовал, но сама *возможность* в протоколы (в частности, Sun RPC) была заложена.
|
||||
|
||||
Одновременно с идеологическим кризисом RPC-подходов первого поколения, который стал особенно заметен с появлением массовых клиент-серверных приложений, где производительность намного важнее удобства разработчиков, происходит и другой процесс — стандартизации сетевых протоколов. Ещё в начале 90-х наблюдалось огромное разнообразие форматов взаимодействия, но постепенно сетевой стек в двух точках практических полностью унифицировался. Одна из них — это Internet protocol suite, состоящий из базового протокола IP и надстройки в виде TCP или UDP над ним. На сегодняшний день альтернативы TCP/IP используются в чрезвычайно ограниченном спектре задач, и средний разработчик практически не сталкивается ни с каким другим сетевым стеком. Однако у TCP/IP с прикладной точки зрения есть существенный недостаток — он оперирует поверх системы IP-адресов, которые плохо подходят для организации распределённых систем:
|
||||
* во-первых, люди не запоминают IP-адреса и предпочитают оперировать «говорящими» именами;
|
||||
* во-вторых, IP-адрес является технической сущностью, связанной с узлом сети, а разработчики хотели бы иметь возможность добавлять и изменять узлы, не нарушая работы своих приложений.
|
||||
|
||||
Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы.
|
||||
|
||||
Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9[ref The Original HTTP as defined in 1991](https://www.w3.org/Protocols/HTTP/AsImplemented.html), разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
|
||||
Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы. Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9[ref The Original HTTP as defined in 1991](https://www.w3.org/Protocols/HTTP/AsImplemented.html), разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле.
|
||||
|
||||
Протокол был очень прост и всего лишь описывал способ получить документ, открыв TCP/IP соединение с сервером и передав строку вида `GET адрес_документа`. Позднее протокол был дополнен стандартом URL, позволяющим детализировать адрес документа, и далее протокол начал развиваться стремительно: появились новые глаголы помимо `GET`, статусы ответов, заголовки, типы данных и так далее.
|
||||
|
||||
HTTP появился изначально для передачи размеченного гипертекста, что для программных интерфейсов подходит слабо. Однако HTML со временем эволюционировал в более строгий и машиночитаемый XML, который быстро стал одним из общепринятых форматов описания вызовов API. С начала 2000-х XML начал вытесняться более простым и интероперабельным JSON, и сегодня, говоря об HTTP API, чаще всего имеют в виду такие интерфейсы, в которых данные передаются в формате JSON по протоколу HTTP.
|
||||
HTTP появился изначально для передачи размеченного гипертекста, что для программных интерфейсов подходит слабо. Однако HTML со временем эволюционировал в более строгий и машиночитаемый XML, который быстро стал одним из общепринятых форматов описания вызовов API. (Забегая вперёд, скажем, что в начале 2000-х XML был быстро вытеснен ещё более простым и интероперабельным JSON.)
|
||||
|
||||
Поскольку, с одной стороны, HTTP был простым и понятным протоколом, позволяющим осуществлять произвольные запросы к удаленным серверам по их доменным именам, и, с другой стороны, быстро оброс почти бесконечным количеством разнообразных расширений над базовой функциональностью, он стал второй точкой, к которой сходятся сетевые технологии: практически все запросы к API внутри TCP/IP-сетей осуществляются по протоколу HTTP (и даже если используется альтернативный протокол, запросы в нём всё равно зачастую оформлены в виде HTTP-пакетов просто ради удобства). При этом, однако, в отличие от TCP/IP-уровня, каждый разработчик сам для себя решает, какой объём функциональности, предоставляемой протоколом и многочисленными расширениями к нему, он готов применить. В частности, gRPC и GraphQL работают поверх HTTP, но используют крайне ограниченное подмножество его возможностей.
|
||||
Поскольку, с одной стороны, HTTP был простым и понятным протоколом, позволяющим осуществлять произвольные запросы к удаленным серверам по их доменным именам, и, с другой стороны, быстро оброс почти бесконечным количеством разнообразных расширений над базовой функциональностью, он стал второй точкой, к которой сходятся сетевые технологии: практически все запросы к API внутри TCP/IP-сетей осуществляются по протоколу HTTP (и даже если используется альтернативный протокол, запросы в нём всё равно зачастую оформлены в виде HTTP-пакетов просто ради удобства). HTTP, однако, идеологически совершенно противоположен RPC, поскольку не предполагает ни нативной обвязки для функций удалённого вызова, ни тем более разделяемого доступа к памяти. Зато HTTP предложил несколько очень удобных концепций для наращивания производительности серверов, такие как управление кэшированием из коробки и концепцию прозрачных прокси.
|
||||
|
||||
Тем не менее, *обычно* словосочетание «HTTP API» используется не просто в значении «любой API, использующий протокол HTTP»; говоря «HTTP API» мы *скорее* подразумеваем, что он используется не как дополнительный третий протокол транспортного уровня (как это происходит в GRPC и GraphQL), а именно как протокол уровня приложения, то есть составляющие протокола (такие как: URL, заголовки, HTTP-глаголы, статусы ответа, политики кэширования и т.д.) используются в соответствии с их семантикой, определённой в стандартах. *Обычно* также подразумевается, что в HTTP API использует какой-то из текстовых форматов передачи данных (JSON, XML) для описания вызовов.
|
||||
В итоге в середине 1990-х годов происходит постепенный отказ от RPC-фреймворков первого поколения в пользу нового подхода, который позднее Рой Филдинг в своей диссертации 2001 года обобщит под названием «Representational State Transfer» или «REST» (о чём мы поговорим чуть позже в соответствующей главе). В новой парадигме отношения между данными и операциями над ними переворачиваются с ног на голову:
|
||||
* клиент не вызывает процедуры на сервере с передачей параметров — клиент указывает серверу абстрактный адрес фрагмента данных (*ресурса*), к которому он хочет применить операцию;
|
||||
* сам список операций лимитирован и стандартизирован, семантика их чётко определена в стандарте;
|
||||
* клиент и сервер независимы и *принципиально* не имеют никакого разделяемого состояния; все необходимые для исполнения операции параметры должны быть переданы явно;
|
||||
* между клиентом и сервером может находиться множество промежуточных узлов сети (прокси и гейтвеев), что не влияет на протокол;
|
||||
* сервер размечает параметры кэширования передаваемых данных (представления ресурсов), клиент (и промежуточные прокси) имеют право кэшировать данные в соответствии с разметкой.
|
||||
|
||||
**NB**: отказ от архитектур, где клиент и сервер жёстко связаны, в пользу ресурсоориентированных stateless подходов собственно породил понятие дизайна клиент-серверного API, поскольку потребовалось зафиксировать *контракт* между клиентом и сервером. В парадигме ранних RPC-фреймворков говорить о дизайне API бессмысленно, поскольку написанный разработчиком код и есть API взаимодействия, а с нижележащими протоколами разработчику и вовсе не было нужды знакомиться.
|
||||
|
||||
Хотя новый подход оказался весьма удачным с точки зрения разработки высокопроизводительных серверов, проблемы неудобства работы с декларативными API из императивных языков никуда не делось. К тому же изначально простой стандарт быстро превратился в монстра Франкенштейна, сшитого из десятков разных подстандартов. Не думаем, что сильно ошибёмся, если скажем, что стандарт HTTP целиком со всеми многочисленными дополнительными RFC не знает полностью ни один человек.
|
||||
|
||||
Начиная с конца 2010-х годов мы наблюдаем расцвет RPC-технологий нового поколения — или, вернее было бы сказать, комбинированных технологий, которые одновременно и удобны в применении в императивных языках программирования (поставляются с обвязкой, позволяющей эффективно использовать кодогенерацию), и интероперабельны (работают поверх строго стандартизированных протоколов, которые не зависят от конкретного языка программирования), и масштабируемы (абстрагируют понятие ресурса и не предоставляют прямого доступа к памяти сервера). Фактически, на сегодня *идеологически* разница между современным API, следующим классическому архитектурному стилю REST, и современным RPC заключается лишь в разметке кэшируемых данных и принципах адресации: в первом случае единицей доступа является ресурс (а параметры операции передаются дополнительно), а во втором — имя операции (а адреса ресурсов, над которыми она выполняется, передаются дополнительно). Мы рассмотрим конкретные технологии разработки таких API в следующей главе, но отметим, что почти все они (за единственным заметным исключением в виде MQTT) работают поверх протокола HTTP.
|
||||
|
||||
Тем не менее, *обычно* словосочетание «HTTP API» используется не просто в значении «любой API, использующий протокол HTTP»; говоря «HTTP API» мы *скорее* подразумеваем, что он используется не как дополнительный третий протокол транспортного уровня, а именно как протокол уровня приложения, то есть составляющие протокола (такие как: URL, заголовки, HTTP-глаголы, статусы ответа, политики кэширования и т.д.) используются в соответствии с их семантикой, определённой в стандартах. *Обычно* также подразумевается, что в HTTP API использует какой-то из текстовых форматов передачи данных (JSON, XML) для описания вызовов.
|
||||
|
||||
В рамках настоящего раздела мы поговорим о дизайне сетевых API, обладающих следующими характеристиками:
|
||||
* протоколом взаимодействия является HTTP версий 1.1 и выше;
|
||||
|
@ -1,6 +1,17 @@
|
||||
### [Преимущества и недостатки HTTP API в сравнении с альтернативными технологиями][http-api-pros-and-cons]
|
||||
|
||||
По прочтению предыдущей главы у читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на формат ответа в JSON-RPC[ref JSON-RPC 2.0 Specification. Response object](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
Как мы обсудили в предыдущей главе, в настоящий момент выбор технологии для разработки таких API сводится к выбору либо ресурсоориентированного подхода (то, что принято называть «REST API»; напомним, мы будем использовать для таких API обозначение «HTTP API»), либо одного из современных RPC-протоколов. Как мы отмечали, *концептуально* разница не очень значительна; однако *технически* разные фреймворки используют протокол совершенно по-разному:
|
||||
|
||||
**Во-первых**, разные фреймворки опираются на разные форматы передаваемых данных:
|
||||
* REST API и некоторые RPC (JSON-RPC[ref JSON-RPC](https://www.jsonrpc.org/), GraphQL[ref GraphQL](https://graphql.org/)) полагаются в основном на формат JSON[ref JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) (опционально дополненный передачей бинарных файлов);
|
||||
* gRPC[ref gRPC](https://grpc.io), а также Apache Avro[ref Apache Avro](https://avro.apache.org/docs/) и другие специализированные RPC-протоколы полагаются на бинарные форматы (такие как Protocol Buffers[ref Protocol Buffers](https://protobuf.dev/), FlatBuffers[ref FlatBuffers](https://flatbuffers.dev/) и собственный формат Apache Avro);
|
||||
* наконец, некоторые RPC-протоколы (SOAP[ref SOAP](https://www.w3.org/TR/soap12/), XML-RPC[ref XML-RPC](http://xmlrpc.com/)) используют для передачи данных формат XML[ref Extensible Markup Language (XML)](https://www.w3.org/TR/xml/) (что многими разработчиками сегодня воспринимается скорее как устаревшая практика).
|
||||
|
||||
**Во-вторых**, существующие реализации различаются подходом к утилизации протокола HTTP:
|
||||
* либо клиент-серверное взаимодействие опирается на описанные в стандарте HTTP возможности — этот подход характеризует «REST API»;
|
||||
* либо HTTP утилизируется как транспорт, и поверх него выстроен дополнительный уровень абстракции (т.е. возможности HTTP, такие как номенклатура ошибок или заголовков, сознательно редуцируются до минимального уровня, а вся мета-информация переносится на уровень вышестоящего протокола) — этот подход характерен для RPC протоколов.
|
||||
|
||||
У читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: одни API полагаются на стандартную семантику HTTP, другие полностью от неё отказываются в пользу новоизобретённых стандартов, а третьи существуют где-то посередине. Например, если мы посмотрим на формат ответа в JSON-RPC[ref JSON-RPC 2.0 Specification. Response object](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
|
||||
```json
|
||||
HTTP/1.1 200 OK
|
||||
@ -15,9 +26,11 @@ HTTP/1.1 200 OK
|
||||
}
|
||||
```
|
||||
|
||||
сервер мог бы ответить просто `400 Bad Request` (с передачей идентификатора запроса, ну скажем, в заголовке `X-OurCoffeeAPI-RequestId`). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат.
|
||||
сервер мог бы ответить просто `400 Bad Request` (с передачей идентификатора запроса, ну скажем, в заголовке `X-JSONRPC2-RequestId`). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат.
|
||||
|
||||
Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций `XMLHttpRequest` в браузерах). Однако, новые варианты RPC-протоколов, использующих абсолютный минимум возможностей HTTP, продолжают появляться и сегодня. Мы можем попытаться выделить по крайней мере четыре группы причин, приводящих к такому размежеванию.
|
||||
Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций `XMLHttpRequest` в браузерах). Однако, новые варианты RPC-протоколов, использующих абсолютный минимум возможностей HTTP, продолжают появляться и сегодня.
|
||||
|
||||
Мы можем попытаться выделить по крайней мере три группы причин (помимо идеологических, описанных в предыдущей главе), приводящих к такому размежеванию.
|
||||
|
||||
##### Машиночитаемость метаданных
|
||||
|
||||
@ -49,14 +62,6 @@ HTTP/1.1 200 OK
|
||||
|
||||
Соображения выше распространяются не только на программное обеспечение, но и на его создателей. Представление разработчиков о HTTP API, увы, также фрагментировано. Практически любой программист как-то умеет работать с HTTP API, но редко при этом досконально знает стандарт или хотя бы консультируется с ним при написании кода. Это ведёт к тому, что добиться качественной и консистентной реализации логики работы с HTTP API может быть сложнее, нежели при использовании альтернативных технологий — причём это соображение справедливо как для партнёров-интеграторов, так и для самого провайдера API.
|
||||
|
||||
Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партнёра.
|
||||
|
||||
##### Идеология разработки
|
||||
|
||||
Современные HTTP API унаследовали парадигму разработки ещё с тех времён, когда по протоколу HTTP в основном передавали гипертекст. В ней считается, что HTTP-запрос представляет собой операцию, выполняемую над некоторым объектом (ресурсом), который идентифицируется с помощью URL. Большинство альтернативных технологий придерживаются других парадигм; чаще всего URL в них идентифицирует *функцию*, которую необходимо выполнить с передачей указанных параметров. Подобная семантика не то чтобы противоречит HTTP — выполнение удалённых процедур хорошо описывается протоколом — но делает использование стандартных возможностей протокола бессмысленным (например, `Range`-заголовки) или вовсе опасным (возникает неоднозначность интерпретации, скажем, заголовка `ETag`).
|
||||
|
||||
С точки зрения клиентской разработки следование парадигме HTTP часто требует реализации дополнительного слоя абстракции, который превращает вызов методов на объектах в HTTP-операции над нужными ресурсам. RPC-технологии в этом плане удобнее для интеграции. (Впрочем, любой достаточно сложный RPC API все равно потребует промежуточного уровня абстракции, а, например, GraphQL в нём нуждается изначально.)
|
||||
|
||||
##### Вопросы производительности
|
||||
|
||||
В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы:
|
||||
@ -95,3 +100,27 @@ HTTP/1.1 200 OK
|
||||
* взвесить накладные расходы на подготовку и внедрение инструментов чтения бинарных форматов и расшифровки бинарных протоколов против накладных расходов на неоптимальную передачу данных;
|
||||
* оценить, хватит ли вам качественного, но ограниченного в возможностях набора программного обеспечения, идущего в комплекте с альтернативным форматом, или вам важнее возможность использовать широкий спектр инструментов, умеющих работать с HTTP API, пусть они и не всегда высокого качества;
|
||||
* прикинуть, насколько дорого вам обойдутся разработчики, способные работать с этим форматом.
|
||||
|
||||
#### Выбор технологии разработки клиент-серверного API
|
||||
|
||||
Как мы видим из вышесказанного, HTTP API и альтернативные RPC-протоколы занимают разные ниши:
|
||||
* для публичных API архитектурный стиль REST является выбором по умолчанию, поскольку он (а) понятен максимально широкому кругу разработчиков, (б) позволяет разрабатывать клиентские приложения практически на любой платформе;
|
||||
* для узкоспециализированных API логично использовать узкоспециализированные фреймворки (например, Apache Avro для работы с Apache Hadoop).
|
||||
|
||||
Практика предоставления публичных API в формате, скажем, gRPC, постепенно набирает популярность, но пока ещё незначительна на общем фоне. Таким образом, проблема выбора технологии возникает только для непубличных API общего назначения. На сегодня этот выбор выглядит так:
|
||||
* REST API;
|
||||
* gRPC;
|
||||
* GraphQL;
|
||||
* множество технологий поменьше, на которых мы не будем останавливаться подробно.
|
||||
|
||||
gRPC является классической технологией второго поколения, сочетающей все перечисленные выше достоинства:
|
||||
* полагается на новейшие возможности протокола HTTP/2 и эффективный формат обмена данными Protobuf (последнее необязательно, но практически мы не встречали имплементаций gRPC API поверх других форматов данных);
|
||||
* разрабатывается компанией Google и поставляется с широким выбором разнообразных инструментов;
|
||||
* предлагает contract-first подход, в котором разработка API начинается с написания спецификации;
|
||||
* благодаря кодогенерации, позволяет удобно работать с протоколом на императивных языках программирования.
|
||||
|
||||
К недостаткам gRPC следует отнести сложность чтения и отладки (сообщения в формате Protobuf нечеловекочитаемые, а без `.proto`-файла и немашиночитаемые), слабую поддержку браузеров, меньшую распространённость (и отсюда более высокий порог вхождения для разработчиков) и потенциальную зависимость от Google. В остальном, gRPC вне всяких сомнений один из наиболее современных и производительных протоколов.
|
||||
|
||||
GraphQL представляет собой любопытный подход, который объединяет концепцию «ресурсов» в HTML (т.е. фокусируется на подробном описании форматов доступных данных), при этом предоставляя чрезвычайно богатый язык запросов для извлечения нужных наборов полей. Основная область его применения — это насыщенные разнородными данными предметные области (как, в общем-то, и следует из названия, GraphQL — скорее механизм распределённых запросов к абстрактному хранилищу данных, нежели парадигма разработки API). Предоставление внешних GraphQL API на сегодня скорее экзотика, поскольку с ростом количества данных и запросов GraphQL-сервисом становится очень сложно управлять[ref Mehta, S., Barodiya, K. Lessons learned from running GraphQL at scale](https://blog.dream11engineering.com/lessons-learned-from-running-graphql-at-scale-2ad60b3cefeb).
|
||||
|
||||
**NB**: теоретически, API может обладать двойным интерфейсом, например, и REST, и gRPC. Так как в основе всех современных фреймворков лежит формальное описание форматов данных, и эти форматы можно при желании транслировать друг в друга, такой мульти-API технически возможен. Практически же примеры таких API нам неизвестны — по-видимому, накладные расходы на поддержание двойного интерфейса не покрывают потенциальную выгоду от большего удобства для разработчиков.
|
@ -1,4 +1,4 @@
|
||||
### [О содержании раздела][sdk-toc]
|
||||
### [Терминология. Обзор технологий разработки SDK][sdk-toc]
|
||||
|
||||
Как мы отмечали во Введении, аббревиатура «SDK» («Software Development Kit»), как и многие из обсуждавшихся ранее терминов, не имеет конкретного значения. Считается, что SDK отличается от API тем, что помимо программных интерфейсов содержит и готовые инструменты для работы с ними. Определение это, конечно, лукавое, поскольку почти любая технология сегодня идёт в комплекте со своим набором инструментов.
|
||||
|
||||
@ -13,3 +13,11 @@
|
||||
Во избежание нагромождения подобных оборотов мы будем называть первый тип инструментов просто «SDK», а второй — «UI-библиотеки».
|
||||
|
||||
**NB**: вообще говоря, UI-библиотека может как включать в себя обёртку над клиент-серверным API, так и предоставлять чистый API к графическому движку. В рамках этой книги мы будем говорить, в основном, о первом варианте как о более общем случае (и более сложном с точки зрения проектирования API). Многие паттерны дизайна SDK, которые мы опишем далее, применимы и к «чистым» библиотекам без клиент-серверной составляющей.
|
||||
|
||||
#### Выбор фреймворка для разработки UI-компонентов
|
||||
|
||||
Поскольку UI — высокоуровневая абстракция над примитивами ОС, почти для всех платформ существуют специализированные фреймворки для разработки визуальных компонентов. Выбор такого фреймворка, увы, может быть непростым занятием. Например, в случае веб-платформы её низкоуровневость и популярность привели к тому, что на сегодня количество конкурирующих технологий, предоставляющих фреймворки для разработки SDK, превосходит всякое воображение. Можно упомянуть наиболее распространённые на сегодня React[ref React](https://react.dev/), Angular[ref Angular](https://angular.io/), Svelte[ref Svelte](https://svelte.dev/), Vue.js[ref Vue.js](https://vuejs.org/) и не сдающие своих позиций Bootstrap[ref Bootstrap](https://getbootstrap.com/) и Ember[ref Ember](https://emberjs.com/). Из перечисленных технологий наибольшее проникновение имеет React, но оно всё ещё измеряется в единицах процентов[ref How Many Websites Use React in 2023? (Usage Statistics)](https://increditools.com/react-usage-statistics/). При этом компоненты на «чистом» JavaScript/CSS при этом часто критикуются как неудобные к использованию в перечисленных фреймворках, так как каждый из них реализует весьма строгие методологии. Примерно такая же ситуация наблюдается, например, с разработкой визуальных компонентов для Windows. Вопрос «на каком фреймворке разрабатывать компоненты для этих платформ», увы, не имеет простого ответа — фактически, вам придётся измерять рынки и принимать решения по каждому фреймворку отдельно.
|
||||
|
||||
Лучше дела обстоят с современными мобильными платформами (а также MacOS), которые гораздо более гомогенны. Однако здесь возникает другая проблема — современные приложения, как правило, поддерживают сразу несколько таких платформ, что приводит к дублированию кода (и номенклатуры API).
|
||||
|
||||
Решением этой проблемы может быть использование кросс-платформенных мобильных (React Native[ref React Native](https://reactnative.dev/), Flutter[ref Flutter](https://flutter.dev/), Xamarin[ref Xamarin](https://dotnet.microsoft.com/en-us/apps/xamarin)) и десктопных фреймворков (JavaFX[ref JavaFX](https://openjfx.io/), QT[ref QT](https://www.qt.io/)), а также узкоспециализированных решений для конкретных задач (например, Unity[ref Unity](https://docs.unity3d.com/Manual/index.html) для разработки игр). Несомненным преимуществом таких технологий является скорость разработки и универсальность (как кода, так и программистов). Недостатки также достаточно очевидны — от таких приложений может быть сложно добиться оптимальной производительности, и к ним часто неприменимы многие стандартные инструменты, доступные для конкретной платформы; например, отладка и профайлинг могут быть затруднены. На сегодня скорее наблюдается паритет между двумя этими подходами (несколько фактически независимых приложений, написанных на поддерживаемых платформой языках vs. одно кросс-плафторменное приложение).
|
Loading…
Reference in New Issue
Block a user