mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-03-29 20:51:01 +02:00
fresh build
This commit is contained in:
parent
0b2a4d945e
commit
d19dd1d044
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
@ -250,7 +250,9 @@ a.anchor {
|
||||
The book is dedicated to designing APIs: how to build the architecture
|
||||
properly, from a high-level planning down to final interfaces.
|
||||
</p>
|
||||
<p>Illustrations by Maria Konstantinova</p>
|
||||
<p>
|
||||
Illustrations by Maria Konstantinova<br><a href="https://www.instagram.com/art.mari.ka/">https://www.instagram.com/art.mari.ka/</a>
|
||||
</p>
|
||||
|
||||
<img class="cc-by-nc-img" src="data:image/png;base64,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">
|
||||
<p class="cc-by-nc">
|
||||
@ -579,7 +581,7 @@ GET /functions
|
||||
// Operation type:
|
||||
// * set_cup
|
||||
// * grind_coffee
|
||||
// * shed_water
|
||||
// * pour_water
|
||||
// * discard_cup
|
||||
"type": "set_cup",
|
||||
// Arguments available to each operation.
|
||||
@ -1019,6 +1021,7 @@ The invalid price error is resolvable: client could obtain a new price offer and
|
||||
<pre><code>{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"coffee_machine_type": "drip_coffee_maker",
|
||||
"coffee_machine_brand",
|
||||
"place_name": "The Chamomile",
|
||||
@ -1069,7 +1072,7 @@ The invalid price error is resolvable: client could obtain a new price offer and
|
||||
// Place data
|
||||
"place": { "name", "location" },
|
||||
// Coffee machine properties
|
||||
"coffee-machine": { "brand", "type" },
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
// Route data
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"offers": {
|
||||
@ -1197,9 +1200,9 @@ str_replace(needle, replace, haystack)
|
||||
<h5><a href="#chapter-11-paragraph-8" name="chapter-11-paragraph-8" class="anchor">8. Use globally unique identifiers</a></h5>
|
||||
<p>It's considered good form to use globally unique strings as entity identifiers, either semantic (i.e. "lungo" for beverage types) or random ones (i.e. <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). It might turn out to be extremely useful if you need to merge data from several sources under single identifier.</p>
|
||||
<p>In general, we tend to advice using urn-like identifiers, e.g. <code>urn:order:<uuid></code> (or just <code>order:<uuid></code>). That helps a lot in dealing with legacy systems with different identifiers attached to the same entity. Namespaces in urns help to understand quickly which identifier is used, and is there a usage mistake.</p>
|
||||
<p>One important implication: <strong>never use increasing numbers as external identifiers</strong>. Apart from abovementioned reasons, it allows counting how many entities of each types there are in the system. You competitors will be able to calculate a precise number of orders you have each day, for example.</p>
|
||||
<p>One important implication: <strong>never use increasing numbers as external identifiers</strong>. Apart from abovementioned reasons, it allows counting how many entities of each type there are in the system. You competitors will be able to calculate a precise number of orders you have each day, for example.</p>
|
||||
<p><strong>NB</strong>: this book often use short identifiers like "123" in code examples; that's for reading the book on small screens convenience, do not replicate this practice in a real-world API.</p>
|
||||
<h5><a href="#chapter-11-paragraph-9" name="chapter-11-paragraph-9" class="anchor">9. Clients must always know full system state</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-9" name="chapter-11-paragraph-9" class="anchor">9. System state must be observable by clients</a></h5>
|
||||
<p>This rule could be reformulated as ‘don't make clients guess’.</p>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Creates an order and returns its id
|
||||
@ -1353,7 +1356,7 @@ PATCH /v1/orders/123
|
||||
<li>introducing pagination and field value length limits;</li>
|
||||
<li>stopping saving bytes in all other cases.</li>
|
||||
</ul>
|
||||
<p><strong>In second</strong>, shortening response sizes will backfire exactly with sploiling collaborative editing: one client won't see the changes the other client have made. Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should always do this unless response size affects performance.</p>
|
||||
<p><strong>In second</strong>, shortening response sizes will backfire exactly with sploiling collaborative editing: one client won't see the changes the other client has made. Generally speaking, in 9 cases out of 10 it is better to return a full entity state from any modifying operation, sharing the format with read access endpoint. Actually, you should always do this unless response size affects performance.</p>
|
||||
<p><strong>In third</strong>, this approach might work if you need to rewrite a field's value. But how to unset the field, return its value to the default state? For example, how to <em>remove</em> <code>client_phone_number_ext</code>?</p>
|
||||
<p>In such cases special values are often being used, like <code>null</code>. But as we discussed above, this is a defective practice. Another variant is prohibiting non-required fields, but that would pose considerable obstacles in a way of expanding the API.</p>
|
||||
<p><strong>Better</strong>: one of the following two strategies might be used.</p>
|
||||
@ -1456,7 +1459,7 @@ X-Idempotency-Token: <token>
|
||||
→ 409 Conflict
|
||||
</code></pre>
|
||||
<p>— the server found out that a different token was used in creating revision 124, which means an access conflict.</p>
|
||||
<p>Furthermore, adding idempotency tokens not only resolves the issue, but also makes possible to make an advanced optimization. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return <code>200 OK</code> instead of <code>409 Conflict</code>. This logics dramatically improves user experience, being fully backwards compatible and avoiding conflict resolving code fragmentation.</p>
|
||||
<p>Furthermore, adding idempotency tokens not only resolves the issue, but also makes advanced optimizations possible. If the server detects an access conflict, it could try to resolve it, ‘rebasing’ the update like modern version control systems do, and return <code>200 OK</code> instead of <code>409 Conflict</code>. This logics dramatically improves user experience, being fully backwards compatible and avoiding conflict resolving code fragmentation.</p>
|
||||
<p>Also, be warned: clients are bad at implementing idempotency tokens. Two problems are common:</p>
|
||||
<ul>
|
||||
<li>you can't really expect that clients generate truly random tokens — they may share the same seed or simply use weak algorithms or entropy sources; therefore you must put constraints on token checking: token must be unique to specific user and resource, not globally;</li>
|
||||
@ -1701,6 +1704,13 @@ GET /v1/records?cursor=<cursor value>
|
||||
</code></pre>
|
||||
<p>One advantage of this approach is the possibility to keep initial request parameters (i.e. <code>filter</code> in our example) embedded into the cursor itself, thus not copying them in follow-up requests. It might be especially actual if the initial request prepares full dataset, for example, moving it from a ‘cold’ storage to a ‘hot’ one (then <code>cursor</code> might simply contain the encoded dataset id and the offset).</p>
|
||||
<p>There are several approaches to implementing cursors (for example, making single endpoint for initial and follow-up requests, returning the first data portion in the first response). As usual, the crucial part is maintaining consistency across all such endpoints.</p>
|
||||
<p><strong>NB</strong>: some sources discourage this approach because in this case user can't see a list of all pages and can't choose an arbitrary one. We should note here that:</p>
|
||||
<ul>
|
||||
<li>such a case (pages list and page selection) exists if we deal with user interfaces; we could hardly imagine a <em>program</em> interface which needs to provide an access to random data pages;</li>
|
||||
<li>if we still talk about an API to some application, which has a ‘paging’ user control, then a proper approach would be to prepare ‘paging’ data on server, including generating links to pages;</li>
|
||||
<li>cursor-based solution doesn't prohibit using <code>offset</code>/<code>limit</code>; nothing could stop us from creating a dual interface, which might serve both <code>GET /items?cursor=…</code> and <code>GET /items?offset=…&limit=…</code> requests;</li>
|
||||
<li>finally, if there is a necessity to provide an access to arbitrary pages in user interface, we should ask ourselves a question, which problem is being solved that way; probably, users use this functionality to find something: a specific element on the list, or the position they ended while working with the list last time; probably, we should provide more convenient controls to solve those tasks than accessing data pages by their indexes.</li>
|
||||
</ul>
|
||||
<p><strong>Bad</strong>:</p>
|
||||
<pre><code>// Returns a limited number of records
|
||||
// sorted by a specified field in a specified order
|
||||
@ -1772,11 +1782,11 @@ GET /v1/record-views/{id}?cursor={cursor}
|
||||
"field": "position.latitude",
|
||||
"error_type": "constraint_violation",
|
||||
"constraints": {
|
||||
"min": -180,
|
||||
"max": 180
|
||||
"min": -90,
|
||||
"max": 90
|
||||
},
|
||||
"message":
|
||||
"'position.latitude' value must fall within [-180, 180] interval"
|
||||
"'position.latitude' value must fall within [-90, 90] interval"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1872,7 +1882,7 @@ POST /v1/orders
|
||||
</code></pre>
|
||||
<p>You may note that in this setup the error can't resolved in one step: this situation must be elaborated over, and either order calculation parameters must be changed (discounts should not be counted against the minimal order sum), or a special type of error must be introduced.</p>
|
||||
<h5><a href="#chapter-11-paragraph-19" name="chapter-11-paragraph-19" class="anchor">19. No results is a result</a></h5>
|
||||
<p>If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found .</p>
|
||||
<p>If a server processed a request correctly and no exceptional situation occurred — there must be no error. Regretfully, an antipattern is widespread — of throwing errors when zero results are found.</p>
|
||||
<p><strong>Bad</strong></p>
|
||||
<pre><code>POST /search
|
||||
{
|
||||
@ -1903,7 +1913,7 @@ POST /v1/orders
|
||||
<p>It is important to understand that user's language and user's jurisdiction are different things. Your API working cycle must always store user's location. It might be stated either explicitly (requests contain geographical coordinates) or implicitly (initial location-bound request initiates session creation which stores the location), bit no correct localization is possible in absence of location data. In most cases reducing the location to just a country code is enough.</p>
|
||||
<p>The thing is that lots of parameters potentially affecting data formats depend not on language, but user location. To name a few: number formatting (integer and fractional part delimiter, digit groups delimiter), date formatting, first day of week, keyboard layout, measurement units system (which might be non-decimal!), etc. In some situations you need to store two locations: user residence location and user ‘viewport’. For example, if US citizen is planning a European trip, it's convenient to show prices in local currency, but measure distances in miles and feet.</p>
|
||||
<p>Sometimes explicit location passing is not enough since there are lots of territorial conflicts in a world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. Author of this books once had to implement a ‘state A territory according to state B official position’ concept.</p>
|
||||
<p><strong>Important</strong>: mark a difference between localization for end users and localization for developers. Take a look at the example in #12 rule: <code>localized_message</code> is meant for the user; the app should show it if there is no specific handler for this error exists in code. This message must be written in user's language and formatted according to user's location. But <code>details.checks_failed[].message</code> is meant to be read by developers examining the problem. So it must be written and formatted in a manner which suites developers best. In a software development world it usually means ‘in English’.</p>
|
||||
<p><strong>Important</strong>: mark a difference between localization for end users and localization for developers. Take a look at the example in rule #19: <code>localized_message</code> is meant for the user; the app should show it if there is no specific handler for this error exists in code. This message must be written in user's language and formatted according to user's location. But <code>details.checks_failed[].message</code> is meant to be read by developers examining the problem. So it must be written and formatted in a manner which suites developers best. In a software development world it usually means ‘in English’.</p>
|
||||
<p>Worth mentioning is that <code>localized_</code> prefix in the example is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs.</p>
|
||||
<p>And one more thing: all strings must be UTF-8, no exclusions.</p><div class="page-break"></div><h3><a href="#chapter-12" class="anchor" name="chapter-12">Chapter 12. Annex to Section I. Generic API Example</a></h3>
|
||||
<p>Let's summarize the current state of our API study.</p>
|
||||
@ -1924,7 +1934,7 @@ POST /v1/orders
|
||||
// Place data
|
||||
"place": { "name", "location" },
|
||||
// Coffee machine properties
|
||||
"coffee-machine": { "brand", "type" },
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
// Route data
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"offers": {
|
||||
|
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
@ -248,10 +248,12 @@ a.anchor {
|
||||
</p>
|
||||
<p>
|
||||
Эта книга посвящена проектированию API: как правильно выстроить
|
||||
архитектуру, начиная с высокоуровневого планирования из заканчивая
|
||||
архитектуру, начиная с высокоуровневого планирования и заканчивая
|
||||
деталями реализации конкретных интерфейсов.
|
||||
</p>
|
||||
<p>Иллюстрации: Мария Константинова</p>
|
||||
<p>
|
||||
Иллюстрации: Мария Константинова<br><a href="https://www.instagram.com/art.mari.ka/">https://www.instagram.com/art.mari.ka/</a>
|
||||
</p>
|
||||
|
||||
<img class="cc-by-nc-img" src="data:image/png;base64,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">
|
||||
<p class="cc-by-nc">
|
||||
@ -296,7 +298,7 @@ a.anchor {
|
||||
<li>обратная совместимость нарушена ноль раз за последние две тысячи лет.</li>
|
||||
</ul>
|
||||
<p>Отличие древнеримского акведука от хорошего API состоит лишь в том, что API предлагает <em>программный</em> контракт. Для связывания двух областей необходимо написать некоторый <em>код</em>. Цель этой книги — помочь вам разработать API, так же хорошо выполняющий свою задачу, как и древнеримский акведук.</p>
|
||||
<p>Акведук также хорошо иллюстрирует другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не может контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.</p>
|
||||
<p>Акведук также хорошо иллюстрирует другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не можете контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.</p>
|
||||
<p>Поэтому проектирование API налагает на вас несколько большую ответственность. <strong>API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок</strong>.</p><div class="page-break"></div><h3><a href="#chapter-3" class="anchor" name="chapter-3">Глава 3. Критерии качества API</a></h3>
|
||||
<p>Прежде чем излагать рекомендации, нам следует определиться с тем, что мы считаем «хорошим» API, и какую пользу мы получаем от того, что наше API «хорошее».</p>
|
||||
<p>Начнём со второго вопроса. Очевидно, «хорошесть» API определяется в первую очередь тем, насколько он помогает разработчикам решать стоящие перед ними задачи. (Можно резонно возразить, что решение задач, стоящих перед разработчиками, не обязательно влечёт за собой выполнение целей, которые мы ставим перед собой, предлагая разработчикам API. Однако манипуляция общественным мнением не входит в область интересов автора этой книги: здесь и далее предполагается, что API существует в первую очередь для того, чтобы разработчики решали с его помощью свои задачи, а не ради каких-то не декларируемых явно целей.)</p>
|
||||
@ -356,7 +358,7 @@ Cache-Control: no-cache
|
||||
<li>телом ответа является JSON, состоящий из единственного поля <code>error_message</code>; отсутствие значения поля означает, что его значением является именно то, что в этом поле и ожидается — в данном случае какое-то сообщение об ошибке.</li>
|
||||
</ul>
|
||||
<p>Здесь термин «клиент» означает «приложение, установленное на устройстве пользователя, использующее рассматриваемое API». Приложение может быть как нативным, так и веб-приложением. Термины «агент» и «юзер-агент» являются синонимами термина «клиент».</p>
|
||||
<p>Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеют значения.</p>
|
||||
<p>Ответ (частично или целиком) и тело запроса могут быть опущены, если в контексте обсуждаемого вопроса их содержание не имеет значения.</p>
|
||||
<p>Возможна сокращённая запись вида: <code>POST /some-resource</code> <code>{…,"some_parameter",…}</code> → <code>{ "operation_id" }</code>; тело запроса и/или ответа может опускаться аналогично полной записи.</p>
|
||||
<p>Чтобы сослаться на это описание будут использоваться выражения типа «метод <code>POST /v1/bucket/{id}/some-resource</code>» или, для простоты, «метод <code>some-resource</code>» или «метод <code>bucket/some-resource</code>» (если никаких других <code>some-resource</code> в контексте главы не упоминается и перепутать не с чем).</p>
|
||||
<p>Помимо HTTP API-нотации мы будем активно использовать C-подобный псевдокод — точнее будет сказать, JavaScript или Python-подобный, поскольку нотации типов мы будем опускать. Мы предполагаем, что подобного рода императивные конструкции достаточно читабельны, и не будем здесь описывать грамматику подробно.</p><div class="page-break"></div><h2><a href="#section-2" class="anchor" name="section-2">Раздел I. Проектирование API</a></h2><h3><a href="#chapter-7" class="anchor" name="chapter-7">Глава 7. Пирамида контекстов API</a></h3>
|
||||
@ -463,7 +465,7 @@ GET /v1/orders/{id}
|
||||
<p>И зададимся вопросом, каким образом разработчик определит, что заказ клиента готов. Допустим, мы сделаем так: добавим в рецепт лунго эталонный объём, а в состояние заказа — количество уже налитого кофе. Тогда разработчику нужно будет проверить совпадение этих двух цифр, чтобы убедиться, что кофе готов.</p>
|
||||
<p>Такое решение выглядит интуитивно плохим, и это действительно так: оно нарушает все вышеперечисленные принципы.</p>
|
||||
<p><strong>Во-первых</strong>, для решения задачи «заказать лунго» разработчику нужно обратиться к сущности «рецепт» и выяснить, что у каждого рецепта есть объём. Далее, нужно принять концепцию, что приготовление кофе заканчивается в тот момент, когда объём сравнялся с эталонным. Нет никакого способа об этой конвенции догадаться: она неочевидна и её нужно найти в документации. При этом никакой пользы для разработчика в этом знании нет.</p>
|
||||
<p><strong>Во-вторых</strong>, мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим представить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков.</p>
|
||||
<p><strong>Во-вторых</strong>, мы автоматически получаем проблемы, если захотим варьировать размер кофе. Допустим, в какой-то момент мы захотим предоставить пользователю выбор, сколько конкретно миллилитров лунго он желает. Тогда нам придётся проделать один из следующих трюков.</p>
|
||||
<p>Вариант 1: мы фиксируем список допустимых объёмов и заводим фиктивные рецепты типа <code>/recipes/small-lungo</code>, <code>recipes/large-lungo</code>. Почему фиктивные? Потому что рецепт один и тот же, меняется только объём. Нам придётся либо тиражировать одинаковые рецепты, отличающиеся только объёмом, либо вводить какое-то «наследование» рецептов, чтобы можно было указать базовый рецепт и только переопределить объём.</p>
|
||||
<p>Вариант 2: мы модифицируем интерфейс, объявляя объём кофе, указанный в рецепте, значением по умолчанию; при размещении заказа мы разрешаем указать объём, отличный от эталонного:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
@ -572,7 +574,7 @@ GET /functions
|
||||
// Тип операции
|
||||
// * set_cup — поставить стакан
|
||||
// * grind_coffee — смолоть кофе
|
||||
// * shed_water — пролить воду
|
||||
// * pour_water — пролить воду
|
||||
// * discard_cup — утилизировать стакан
|
||||
"type": "set_cup",
|
||||
// Допустимые аргументы для каждой операции
|
||||
@ -677,7 +679,7 @@ GET /sensors
|
||||
</ul>
|
||||
<p>Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается. Обработчик <code>run</code> сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:</p>
|
||||
<ul>
|
||||
<li>вызвать <code>POST /execute</code> физического API кофе-машины с передачей внутреннего идентификатор программ — для машин, поддерживающих API первого типа;</li>
|
||||
<li>вызвать <code>POST /execute</code> физического API кофе-машины с передачей внутреннего идентификатора программы — для машин, поддерживающих API первого типа;</li>
|
||||
<li>инициировать создание рантайма для работы с API второго типа.</li>
|
||||
</ul>
|
||||
<p>Уровень рантаймов API второго типа, исходя из общих соображений, будет скорее всего непубличным, и мы плюс-минус свободны в его имплементации. Самым простым решением будет реализовать виртуальную state-машину, которая создаёт «рантайм» (т.е. stateful контекст исполнения) для выполнения программы и следит за его состоянием.</p>
|
||||
@ -726,7 +728,7 @@ GET /sensors
|
||||
<p><strong>NB</strong>: в имплементации связки <code>orders</code> → <code>match</code> → <code>run</code> → <code>runtimes</code> можно пойти одним из двух путей: </p>
|
||||
<ul>
|
||||
<li>либо обработчик <code>POST /orders</code> сам обращается к доступной информации о рецепте, кофе-машине и программе и формирует stateless-запрос, в котором указаны все нужные данные (тип API кофе-машины и список команд в частности);</li>
|
||||
<li>либо в запросе содержатся только идентификаторы, и имплементация методов сами обратятся за нужными данными через какие-то внутренние API.</li>
|
||||
<li>либо в запросе содержатся только идентификаторы, и следующие обработчики в цепочке сами обратятся за нужными данными через какие-то внутренние API.</li>
|
||||
</ul>
|
||||
<p>Оба варианта имеют право на жизнь; какой из них выбрать — зависит от деталей реализации.</p>
|
||||
<h4>Изоляция уровней абстракции</h4>
|
||||
@ -749,7 +751,7 @@ GET /sensors
|
||||
<li>можно кэшировать статус своего уровня и обновлять его по получению обратного вызова или события.
|
||||
В частности, низкоуровневый цикл исполнения рантайма для машин второго рода очевидно должен быть независимым и обновлять свой статус в фоне, не дожидаясь явного запроса статуса.</li>
|
||||
</ul>
|
||||
<p>Обратите внимание, что здесь фактически происходит следующее: на каждом уровне абстракции есть какой-то свой статус (заказа, рантайма, сенсоров), который сформулирован в терминах соответствующий этому уровню абстракции предметной области. Запрет «перепрыгивания» уровней приводит к тому, что нам необходимо дублировать статус на каждом уровне независимо.</p>
|
||||
<p>Обратите внимание, что здесь фактически происходит следующее: на каждом уровне абстракции есть какой-то свой статус (заказа, рантайма, сенсоров), который сформулирован в терминах соответствующей этому уровню абстракции предметной области. Запрет «перепрыгивания» уровней приводит к тому, что нам необходимо дублировать статус на каждом уровне независимо.</p>
|
||||
<p>Рассмотрим теперь, каким образом через наши уровни абстракции «прорастёт» операция отмены заказа. В этом случае цепочка вызовов будет такой:</p>
|
||||
<ul>
|
||||
<li>пользователь вызовет метод <code>POST /v1/orders/{id}/cancel</code>;</li>
|
||||
@ -879,7 +881,7 @@ GET /sensors
|
||||
</ol>
|
||||
<p>Если внимательно посмотреть на каждый объект, то мы увидим, что, в итоге, каждый объект оказался в смысле своей ответственности составным. Например, <code>program</code> будет оперировать данными высшего уровня (рецепт и кофе-машина), дополняя их терминами своего уровня (идентификатор запуска). Это совершенно нормально: API должно связывать контексты.</p>
|
||||
<h4>Сценарии использования</h4>
|
||||
<p>На этом уровне, когда наше API уже в целом понятно устроено и спроектированы, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача — взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.</p>
|
||||
<p>На этом уровне, когда наше API уже в целом понятно устроено и спроектировано, мы должны поставить себя на место разработчика и попробовать написать код. Наша задача — взглянуть на номенклатуру сущностей и понять, как ими будут пользоваться.</p>
|
||||
<p>Представим, что нам поставили задачу, пользуясь нашим кофейным API, разработать приложение для заказа кофе. Какой код мы напишем?</p>
|
||||
<p>Очевидно, первый шаг — нужно предоставить пользователю возможность выбора, чего он, собственно хочет. И первый же шаг обнажает неудобство использования нашего API: никаких методов, позволяющих пользователю что-то выбрать в нашем API нет. Разработчику придётся сделать что-то типа такого:</p>
|
||||
<ul>
|
||||
@ -967,7 +969,7 @@ app.display(offers);
|
||||
"cursor"
|
||||
}
|
||||
</code></pre>
|
||||
<p>Поступая так, мы не только помогаем разработчику понять, когда ему надо обновить цены, но и решаем UX-задачу: как показать пользователю, что «счастливый час» скоро закончится. Идентификатор предложения может при этом быть stateful (фактически, аналогом сессии пользователя) или stateless (если мы точно знаем, до какого времени действительна цены, мы может просто закодировать это время в идентификаторе).</p>
|
||||
<p>Поступая так, мы не только помогаем разработчику понять, когда ему надо обновить цены, но и решаем UX-задачу: как показать пользователю, что «счастливый час» скоро закончится. Идентификатор предложения может при этом быть stateful (фактически, аналогом сессии пользователя) или stateless (если мы точно знаем, до какого времени действительна цена, мы может просто закодировать это время в идентификаторе).</p>
|
||||
<p>Альтернативно, кстати, можно было бы разделить функциональность поиска по заданным параметрам и получения предложений, т.е. добавить эндпойнт, только актуализирующий цены в конкретных кофейнях.</p>
|
||||
<h4>Обработка ошибок</h4>
|
||||
<p>Сделаем ещё один небольшой шаг в сторону улучшения жизни разработчика. А каким образом будет выглядеть ошибка «неверная цена»?</p>
|
||||
@ -1009,7 +1011,7 @@ app.display(offers);
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>Получив такую ошибку, клиент должен проверить её род (что-то с предложением), проверить конкретную причину ошибки (срок жизни оффера истёк) и отправить повторный запрос цены. При этом если бы <code>checks_failed</code> показал другую причину ошибки — например, указанный <code>offer_id</code> не принадлежит данному пользователю — действия клиента были бы иными (отправить пользователя повторно авторизоваться, а затем перезапросить цену). Если же обработка такого рода ошибок в коде не предусмотрено — следует показать пользователю сообщение <code>localized_message</code> и вернуться к обработке ошибок по умолчанию.</p>
|
||||
<p>Получив такую ошибку, клиент должен проверить её род (что-то с предложением), проверить конкретную причину ошибки (срок жизни оффера истёк) и отправить повторный запрос цены. При этом если бы <code>checks_failed</code> показал другую причину ошибки — например, указанный <code>offer_id</code> не принадлежит данному пользователю — действия клиента были бы иными (отправить пользователя повторно авторизоваться, а затем перезапросить цену). Если же обработка такого рода ошибок в коде не предусмотрена — следует показать пользователю сообщение <code>localized_message</code> и вернуться к обработке ошибок по умолчанию.</p>
|
||||
<p>Важно также отметить, что неустранимые ошибки в моменте для клиента бесполезны (не зная причины ошибки клиент не может ничего разумного предложить пользователю), но это не значит, что у них не должно быть расширенной информации: их все равно будет просматривать разработчик, когда будет исправлять эту проблему в коде. Подробнее об этом в пп. 12-13 следующей главы.</p>
|
||||
<h4>Декомпозиция интерфейсов. Правило «7±2»</h4>
|
||||
<p>Исходя из нашего собственного опыта использования разных API, мы можем, не колеблясь, сказать, что самая большая ошибка проектирования сущностей в API (и, соответственно, головная боль разработчиков) — чрезмерная перегруженность интерфейсов полями, методами, событиями, параметрами и прочими атрибутами сущностей.</p>
|
||||
@ -1019,6 +1021,7 @@ app.display(offers);
|
||||
<pre><code>{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine_id",
|
||||
// Тип кофе-машины
|
||||
"coffee_machine_type": "drip_coffee_maker",
|
||||
// Марка кофе-машины
|
||||
@ -1074,7 +1077,7 @@ app.display(offers);
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
"coffee-machine": { "brand", "type" },
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
// Предложения напитков
|
||||
@ -1200,9 +1203,9 @@ str_replace(needle, replace, haystack)
|
||||
<h5><a href="#chapter-11-paragraph-8" name="chapter-11-paragraph-8" class="anchor">8. Используйте глобально уникальные идентификаторы</a></h5>
|
||||
<p>Хороший тон при разработке API — использовать для идентификаторов сущностей глобально уникальные строки, либо семантичные (например, "lungo" для видов напитков), либо случайные (например <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID-4</a>). Это может чрезвычайно пригодиться, если вдруг придётся объединять данные из нескольких источников под одним идентификатором.</p>
|
||||
<p>Мы вообще склонны порекомендовать использовать идентификаторы в urn-подобном формате, т.е. <code>urn:order:<uuid></code> (или просто <code>order:<uuid></code>), это сильно помогает с отладкой legacy-систем, где по историческим причинам есть несколько разных идентификаторов для одной и той же сущности — тогда неймспейсы в urn помогут быстро понять, что это за идентификатор и нет ли здесь ошибки использования.</p>
|
||||
<p>Отдельное важное следствие: <strong>не используете инкрементальные номера как идентификаторы</strong>. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.</p>
|
||||
<p>Отдельное важное следствие: <strong>не используйте инкрементальные номера как идентификаторы</strong>. Помимо вышесказанного, это плохо ещё и тем, что ваши конкуренты легко смогут подсчитать, сколько у вас в системе каких сущностей и тем самым вычислить, например, точное количество заказов за каждый день наблюдений.</p>
|
||||
<p><strong>NB</strong>: в этой книге часто используются короткие идентификаторы типа "123" в примерах — это для удобства чтения на маленьких экранах, повторять эту практику в реальном API не надо.</p>
|
||||
<h5><a href="#chapter-11-paragraph-9" name="chapter-11-paragraph-9" class="anchor">9. Клиент всегда должен знать полное состояние системы</a></h5>
|
||||
<h5><a href="#chapter-11-paragraph-9" name="chapter-11-paragraph-9" class="anchor">9. Состояние системы должно быть понятно клиенту</a></h5>
|
||||
<p>Правило можно ещё сформулировать так: не заставляйте клиент гадать.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт заказ и возвращает его id
|
||||
@ -1264,7 +1267,7 @@ GET /v1/users/{id}/orders
|
||||
"cup_absence": false
|
||||
}
|
||||
</code></pre>
|
||||
<p>— то разработчику потребуется вычислить флаг <code>!beans_absence && !cup_absence</code> ⇔ <code>!(beans_absence || cup_absence)</code>, а вот этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».</p>
|
||||
<p>— то разработчику потребуется вычислить флаг <code>!beans_absence && !cup_absence</code> ⇔ <code>!(beans_absence || cup_absence)</code>, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».</p>
|
||||
<h5><a href="#chapter-11-paragraph-11" name="chapter-11-paragraph-11" class="anchor">11. Избегайте неявного приведения типов</a></h5>
|
||||
<p>Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:</p>
|
||||
<pre><code>POST /v1/orders
|
||||
@ -1351,14 +1354,14 @@ PATCH /v1/orders/123
|
||||
<li>не предусмотрены ограничения на размер значений полей;</li>
|
||||
<li>передаются бинарные данные (графика, аудио, видео и т.д.).</li>
|
||||
</ul>
|
||||
<p>Во всех трёх случаях передача части полей в лучше случае замаскирует проблему, но не решит. Более оправдан следующий подход:</p>
|
||||
<p>Во всех трёх случаях передача части полей в лучшем случае замаскирует проблему, но не решит. Более оправдан следующий подход:</p>
|
||||
<ul>
|
||||
<li>для «тяжёлых» данных сделать отдельные эндпойнты;</li>
|
||||
<li>ввести пагинацию и лимитирование значений полей;</li>
|
||||
<li>на всём остальном не пытаться экономить.</li>
|
||||
</ul>
|
||||
<p><strong>Во-вторых</strong>, экономия размера ответа выйдет боком как раз при совместном редактировании: один клиент не будет видеть, какие изменения внёс другой. Вообще в 9 случаях из 10 (а фактически — всегда, когда размер ответа не оказывает значительного влияния на производительность) во всех отношениях лучше из любой модифицирующей операции возвращать полное состояние сущности в том же формате, что и из операции доступа на чтение.</p>
|
||||
<p><strong>В-третьих</strong>, этот подход может как-то работать при необходимость перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как <em>удалить</em> <code>client_phone_number_ext</code>?</p>
|
||||
<p><strong>В-третьих</strong>, этот подход может как-то работать при необходимости перезаписать поле. Но что делать, если поле требуется сбросить к значению по умолчанию? Например, как <em>удалить</em> <code>client_phone_number_ext</code>?</p>
|
||||
<p>Часто в таких случаях прибегают к специальным значениям, которые означают удаление поля, например, <code>null</code>. Но, как мы разобрали выше, это плохая практика. Другой вариант — запрет необязательных полей, но это существенно усложняет дальнейшее развитие API.</p>
|
||||
<p><strong>Хорошо</strong>: можно применить одну из двух стратегий.</p>
|
||||
<p><strong>Вариант 1</strong>: разделение эндпойнтов. Редактируемые поля группируются и выносятся в отдельный эндпойнт. Этот подход также хорошо согласуется <a href="#chapter-10">с принципом декомпозиции</a>, который мы рассматривали в предыдущем разделе.</p>
|
||||
@ -1708,8 +1711,15 @@ POST /v1/records/list
|
||||
GET /v1/records?cursor=<значение курсора>
|
||||
{ "records", "cursor" }
|
||||
</code></pre>
|
||||
<p>Достоинством схемы с курсором является возможно зашифровать в самом курсоре данные исходного запроса (т.е. <code>filter</code> в нашем примере), и таким образом не дублировать его в последующих запросах. Это может быть особенно актуально, если инициализирующий запрос готовит полный массив данных, например, перенося его из «холодного» хранилища в горячее.</p>
|
||||
<p>Достоинством схемы с курсором является возможность зашифровать в самом курсоре данные исходного запроса (т.е. <code>filter</code> в нашем примере), и таким образом не дублировать его в последующих запросах. Это может быть особенно актуально, если инициализирующий запрос готовит полный массив данных, например, перенося его из «холодного» хранилища в горячее.</p>
|
||||
<p>Вообще схему с курсором можно реализовать множеством способов (например, не разделять первый и последующие запросы данных), главное — выбрать какой-то один.</p>
|
||||
<p><strong>NB</strong>: в некоторых источниках такой подход, напротив, не рекомендуется, по следующей причине: пользователю невозможно показать список страниц и дать возможность выбрать произвольную. Здесь следует отметить, что:</p>
|
||||
<ul>
|
||||
<li>подобный кейс — список страниц и выбор страниц — существует только для пользовательских интерфейсов; представить себе API, в котором действительно требуется доступ к случайным страницам данных мы можем с очень большим трудом;</li>
|
||||
<li>если же мы всё-таки говорим об API приложения, которое содержит элемент управления с постраничной навигацией, то наиболее правильный подход — подготавливать данные для этого элемента управления на стороне сервера, в т.ч. генерировать ссылки на страницы;</li>
|
||||
<li>подход с курсором не означает, что <code>limit</code>/<code>offset</code> использовать нельзя — ничто не мешает сделать двойной интерфейс, который будет отвечать и на запросы вида <code>GET /items?cursor=…</code>, и на запросы вида <code>GET /items?offset=…&limit=…</code>;</li>
|
||||
<li>наконец, если возникает необходимость предоставлять доступ к произвольной странице в пользовательском интерфейсе, то следует задать себе вопрос, какая проблема тем самым решается; вероятнее всего с помощью этой функциональности пользователь что-то ищет — определенный элемент списка или может быть позицию, на которой он закончил работу со списком в прошлый раз; возможно, следует предоставить для этих задач более удобные элементы управления, нежели перебор страниц.</li>
|
||||
</ul>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Возвращает указанный limit записей,
|
||||
// отсортированных по полю sort_by
|
||||
@ -1781,11 +1791,11 @@ GET /v1/record-views/{id}?cursor={cursor}
|
||||
"field": "position.latitude",
|
||||
"error_type": "constraint_violation",
|
||||
"constraints": {
|
||||
"min": -180,
|
||||
"max": 180
|
||||
"min": -90,
|
||||
"max": 90
|
||||
},
|
||||
"message":
|
||||
"'position.latitude' value must fall in [-180, 180] interval"
|
||||
"'position.latitude' value must fall in [-90, 90] interval"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1910,7 +1920,7 @@ POST /v1/orders
|
||||
<p>Важно понимать, что язык пользователя и юрисдикция, в которой пользователь находится — разные вещи. Цикл работы вашего API всегда должен хранить локацию пользователя. Либо она задаётся явно (в запросе указываются географические координаты), либо неявно (первый запрос с географическими координатами инициировал создание сессии, в которой сохранена локация) — но без локации корректная локализация невозможна. В большинстве случаев локацию допустимо редуцировать до кода страны.</p>
|
||||
<p>Дело в том, что множество параметров, потенциально влияющих на работу API, зависят не от языка, а именно от расположения пользователя. В частности, правила форматирования чисел (разделители целой и дробной частей, разделители разрядов) и дат, первый день недели, раскладка клавиатуры, система единиц измерения (которая к тому же может оказаться не десятичной!) и так далее. В некоторых ситуациях необходимо хранить две локации: та, в которой пользователь находится, и та, которую пользователь сейчас просматривает. Например, если пользователь из США планирует туристическую поездку в Европу, то цены ему желательно показывать в местной валюте, но отформатированными согласно правилам американского письма.</p>
|
||||
<p>Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».</p>
|
||||
<p><strong>Важно</strong>: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 12 сообщение <code>localized_message</code> адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение <code>details.checks_failed[].message</code> написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.</p>
|
||||
<p><strong>Важно</strong>: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 19 сообщение <code>localized_message</code> адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение <code>details.checks_failed[].message</code> написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.</p>
|
||||
<p>Следует отметить, что индикация, какие сообщения следует показать пользователю, а какие написаны для разработчика, должна, разумеется, быть явной конвенцией вашего API. В примере для этого используется префикс <code>localized_</code>.</p>
|
||||
<p>И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.</p><div class="page-break"></div><h3><a href="#chapter-12" class="anchor" name="chapter-12">Глава 12. Приложение к разделу I. Модельное API</a></h3>
|
||||
<p>Суммируем текущее состояние нашего учебного API.</p>
|
||||
@ -1931,7 +1941,7 @@ POST /v1/orders
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
"coffee-machine": { "brand", "type" },
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
// Предложения напитков
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user