1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-05-25 22:08:06 +02:00

style fix

This commit is contained in:
Sergey Konstantinov 2023-03-04 22:44:09 +02:00 committed by GitHub
parent 9ea53dade3
commit e4b7c39def
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 44 deletions

View File

@ -1,50 +1,52 @@
### Isolating Responsibility Areas
Based on the previous chapter, we understand that the abstraction hierarchy in our hypothetical project would look like that:
* the user level (those entities users directly interact with and which are formulated in terms, understandable by users: orders, coffee recipes);
In the previous chapter, we concluded that the hierarchy of abstractions in our hypothetical project would comprise:
* the user level (the entities formulated in terms understandable by users and acted upon by them: orders, coffee recipes);
* the program execution control level (the entities responsible for transforming orders into machine commands);
* the runtime level for the second API kind (the entities describing the command execution state machine).
We are now to define each entity's responsibility area: what's the reasoning in keeping this entity within our API boundaries; what operations are applicable to the entity directly (and which are delegated to other objects). In fact, we are to apply the “why”-principle to every single API entity.
We are now to define each entity's responsibility area: what's the reasoning for keeping this entity within our API boundaries; what operations are applicable to the entity directly (and which are delegated to other objects). In fact, we are to apply the “why”-principle to every single API entity.
To do so we must iterate all over the API and formulate in subject area terms what every object is. Let us remind that the abstraction levels concept implies that each level is some interim subject area per se; a step we take in the journey from describing a task in the first connected context terms (“a lungo ordered by a user”) to the second connect context terms (“a command performed by a coffee machine”).
To do so, we must iterate all over the API and formulate in subject area terms what every object is. Let us remind that the abstraction levels concept implies that each level is some interim subject area per se; a step we take in the journey from describing a task in terms belonging to the first connected context (“a lungo ordered by a user”) terms belonging to the second connected context (“a command performed by a coffee machine”).
As for our fictional example, it would look as follows.
1. User-level entities.
* An `order` describes some logical unit in app-user interaction. An `order` might be:
* created;
* checked for its status;
* retrieved;
* canceled;
* created
* checked for its status
* retrieved
* canceled.
* A `recipe` describes an “ideal model” of some coffee beverage type, e.g. its customer properties. A `recipe` is an immutable entity for us, which means we could only read it.
* A `coffee-machine` is a model of a real-world device. We must be able to retrieve the coffee machine's geographical location and the options it supports from this model (which will be discussed below).
2. Program execution control level entities.
2. Program execution control-level entities.
* A `program` describes a general execution plan for a coffee machine. Programs could only be read.
* The `programs/matcher` entity is capable of coupling a `recipe` and a `program`, which in fact means “to retrieve a dataset needed to prepare a specific recipe on a specific coffee machine.
* A `programs/run` entity describes a single fact of running a program on a coffee machine. A `run` might be:
* initialized (created);
* checked for its status;
* The `programs/matcher` entity is capable of coupling a `recipe` and a `program`, which in fact means retrieving a dataset needed to prepare a specific recipe on a specific coffee machine.
* The `programs/run` entity describes a single fact of running a program on a coffee machine. A `run` might be:
* initialized (created)
* checked for its status
* canceled.
3. Runtime-level entities.
* A `runtime` describes a specific execution data context, i.e. the state of each variable. `runtime` might be:
* initialized (created);
* checked for its status;
* A `runtime` describes a specific execution data context, i.e. the state of each variable. A `runtime` might be:
* initialized (created)
* checked for its status
* terminated.
If we look closely at the entities, we may notice that each entity turns out to be a composite. For example, a `program` will operate high-level data (`recipe` and `coffee-machine`), enhancing them with its subject area terms (`program_run_id` for instance). This is totally fine: connecting contexts is what APIs do.
#### Use Case Scenarios
At this point, when our API is in general clearly outlined and drafted, we must put ourselves into the developer's shoes and try writing code. Our task is to look at the entity nomenclature and make some estimates regarding their future usage.
At this point, when our API is in general clearly outlined and drafted, we must put ourselves into the developer's shoes and try writing code. Our task is to look at the entity nomenclature and make some guesses regarding their future usage.
So, let us imagine we've got a task to write an app for ordering a coffee, based on our API. What code would we write?
Obviously, the first step is offering a choice to a user, to make them point out what they want. And this very first step reveals that our API is quite inconvenient. There are no methods allowing for choosing something. A developer has to implement these steps:
Obviously, the first step is offering a choice to a user, to make them point out what they want. And this very first step reveals that our API is quite inconvenient. There are no methods allowing for choosing something. Developers have to implement these steps:
* retrieve all possible recipes from the `GET /v1/recipes` endpoint;
* retrieve a list of all available coffee machines from the `GET /v1/coffee-machines` endpoint;
* write a code that traverses all this data.
If we try writing pseudocode, we will get something like that:
```
// Retrieve all possible recipes
let recipes =
@ -70,13 +72,14 @@ let matchingCoffeeMachines =
app.display(matchingCoffeeMachines);
```
As you see, developers are to write a lot of redundant code (to say nothing about the difficulties of implementing spatial indexes). Besides, if we take into consideration our Napoleonic plans to cover all coffee machines in the world with our API, then we need to admit that this algorithm is just a waste of resources on retrieving lists and indexing them.
As you see, developers are to write a lot of redundant code (to say nothing about the complexity of implementing spatial indexes). Besides, if we take into consideration our Napoleonic plans to cover all coffee machines in the world with our API, then we need to admit that this algorithm is just a waste of computatonal resources on retrieving lists and indexing them.
The necessity of adding a new endpoint for searching becomes obvious. To design such an interface we must imagine ourselves being UX designers, and think about how an app could try to arouse users' interest. Two scenarios are evident:
* display all cafes in the vicinity and the types of coffee they offer (a “service discovery” scenario) — for new users or just users with no specific tastes;
* display all cafes in the vicinity and the types of coffee they offer (a “service discovery” scenario) — for new users or just users with no specific preferences;
* display nearby cafes where a user could order a particular type of coffee — for users seeking a certain beverage type.
Then our new interface would look like this:
```
POST /v1/offers/search
{
@ -106,9 +109,10 @@ Here:
* an `offer` — is a marketing bid: on what conditions a user could have the requested coffee beverage (if specified in the request), or some kind of a marketing offer — prices for the most popular or interesting products (if no specific preference was set);
* a `place` — is a spot (café, restaurant, street vending machine) where the coffee machine is located; we never introduced this entity before, but it's quite obvious that users need more convenient guidance to find a proper coffee machine than just geographical coordinates.
**NB**. We could have enriched the existing `/coffee-machines` endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea because these two types of rankings imply different usage features and scenarios. Furthermore, enriching the search with “offers” pulls this functionality out of the `coffee-machines` namespace: the fact of getting offers to prepare specific beverages in specific conditions is a key feature to users, with specifying the coffee machine being just a part of an offer.
**NB**. We could have enriched the existing `/coffee-machines` endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea because these two types of rankings imply different usage features and scenarios. Furthermore, enriching the search with “offers” pulls this functionality out of the `coffee-machines` namespace: the fact of getting offers to prepare specific beverages in specific conditions is a key feature to users, with specifying the coffee machine being just a part of an offer. And users actually rarely care about coffee machine models.
Coming back to the code developers are writing, it would now look like that:
```
// Searching for offers
// matching a user's intent
@ -122,13 +126,14 @@ app.display(offers);
Methods similar to the newly invented `offers/search` one are called *helpers*. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By “facilitating” we mean not only reducing wordiness (getting rid of “boilerplates”) but also helping developers to avoid common problems and mistakes.
For instance, let's consider the order price question. Our search function returns some “offers” with prices. But “price” is volatile; coffee could cost less during “happy hours,” for example. Developers could make a mistake thrice while implementing this functionality:
* cache search results on a client device for too long (as a result, the price will always be nonactual);
* contrary to previous, call search method excessively just to actualize prices, thus overloading the network and the API servers;
* cache search results on a client device for too long (as a result, the price will always be outdated);
* contrary to previous, call the search endpoint excessively just to actualize prices, thus overloading the network and the API servers;
* create an order with an invalid price (therefore deceiving a user, displaying one sum, and debiting another).
To solve the third problem we could demand including the displayed price in the order creation request, and return an error if it differs from the actual one. (In fact, any API working with money *shall* do so.) But it isn't helping with the first two problems and makes the user experience degrade. Displaying the actual price is always a much more convenient behavior than displaying errors upon pressing the “place an order” button.
To solve the third problem we could demand including the displayed price in the order creation request, and return an error if it differs from the actual one. (In fact, any API working with money *shall* do so.) But it isn't helping with the first two problems and deteriorates the user experience. Displaying the actual price is always a much more convenient behavior than displaying errors upon pressing the “place an order” button.
One solution is to provide a special identifier to an offer. This identifier must be specified in an order creation request.
```
{
"results": [
@ -151,7 +156,7 @@ One solution is to provide a special identifier to an offer. This identifier mus
```
By doing so we're not only helping developers to grasp the concept of getting the relevant price, but also solving a UX task of telling users about “happy hours.”
As an alternative, we could split endpoints: one for searching, another one for obtaining offers. This second endpoint would only be needed to actualize prices in the specified places.
As an alternative, we could split endpoints: one for searching, another one for obtaining offers. This second endpoint would only be needed to actualize prices if needed.
#### Error Handling
@ -164,11 +169,9 @@ POST /v1/orders
{ "message": "Invalid price" }
```
Formally speaking, this error response is enough: users get the “Invalid price” message, and they have to repeat the order. But from a UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all.
Formally speaking, this error response is enough: users get the “Invalid price” message, and they have to repeat the order. But from the UX point of view that would be a horrible decision: the user hasn't made any mistakes, and this message isn't helpful at all.
The main rule of error interfaces in the APIs is: an error response must help a client to understand *what to do with this error*. All other stuff is unimportant: if the error response was machine-readable, there would be no need for the user-readable message.
An error response content must address the following questions:
The main rule of error interfaces in the APIs is that an error response must help a client to understand *what to do with this error*. An error response content must address the following questions:
1. Which party is the problem's source: client or server?
HTTP APIs traditionally employ the `4xx` status codes to indicate client problems, `5xx` to indicate server problems (with the exception of the `404` code, which is an uncertainty status).
@ -176,11 +179,12 @@ An error response content must address the following questions:
3. If the error is caused by a client, is it resolvable, or not?
The invalid price error is resolvable: a client could obtain a new price offer and create a new order with it. But if the error occurred because of a mistake in the client code, then eliminating the cause is impossible, and there is no need to make the user push the “place an order” button again: this request will never succeed.
**NB**: here and throughout we indicate resolvable problems with the `409 Conflict` code, and unresolvable ones with the `400 Bad Request` code.
4. If the error is resolvable, then what's the kind of problem? Obviously, a client couldn't resolve a problem it's unaware of. For every resolvable problem, some *code* must be written (reobtaining the offer in our case), so a list of error descriptions must exist.
5. If the same kind of errors arise because of different parameters being invalid, then which parameter value is wrong exactly?
6. Finally, if some parameter value is unacceptable, then what values are acceptable?
4. If the error is resolvable then what's the kind of problem? Obviously, a client couldn't resolve a problem it's unaware of. For every resolvable problem, developers must *write some code* (reobtaining the offer in our case), so there must be a list of possible error reasons and the corresponding field in the error response.
5. If the same kind of errors arise because of different parameters being invalid then which parameter value is wrong exactly?
6. Finally, if some parameter value is unacceptable then what values are acceptable?
In our case, the price mismatch error should look like this:
```
409 Conflict
{
@ -199,9 +203,9 @@ In our case, the price mismatch error should look like this:
}
```
After getting this error, a client is to check the error's kind (“some problem with offer”), check the specific error reason (“order lifetime expired”), and send an offer retrieving request again. If the `checks_failed` field indicated another error reason (for example, the offer isn't bound to the specified user), client actions would be different (re-authorize the user, then get a new offer). If there were no error handlers for this specific reason, a client would show the `localized_message` to the user, and invoke the standard error recovery procedure.
After getting this error, a client is to check the error's kind (“some problem with offer”), check the specific error reason (“order lifetime expired”), and send an offer retrieving request again. If the `checks_failed` field indicated another error reason (for example, the offer isn't bound to the specified user), client actions would be different (re-authorize the user, then get a new offer). If there was no error handler for this specific reason, a client should show the `localized_message` to the user, and invoke the standard error recovery procedure.
It is also worth mentioning that unresolvable errors are useless to a user at the time (since the client couldn't react usefully to unknown errors), but it doesn't mean that providing extended error data is excessive. A developer will read it when fixing the error in the code. Also, check paragraphs 12 and 13 in the next chapter.
It is also worth mentioning that unresolvable errors are useless to a user at the time when error occurs (since the client couldn't react meaningfully to unknown errors), but it doesn't mean that providing extended error data is excessive. A developer will read it while fixing the issue in their code.
#### Decomposing Interfaces. The “7±2” Rule
@ -209,9 +213,10 @@ Out of our own API development experience, we can tell without any doubt that th
Meanwhile, there is the “Golden Rule” of interface design (applicable not only to APIs but almost to anything): humans could comfortably keep 7±2 entities in short-term memory. Manipulating a larger number of chunks complicates things for most humans. The rule is also known as the [“Miller's law”](https://en.wikipedia.org/wiki/Working_memory#Capacity).
The only possible method of overcoming this law is decomposition. Entities should be grouped under a single designation at every concept level of the API, so developers are never to operate more than 10 entities at a time.
The only possible method of overcoming this law is decomposition. Entities should be grouped under a single designation at every concept level of the API, so developers are never to operate more than a reasonable amount of entities (let's say, ten) at a time.
Let's take a look at the the coffee machine search function response in our API. To ensure an adequate UX of the app, quite bulky datasets are required:
Let's take a look at a simple example: what the coffee machine search function returns. To ensure an adequate UX of the app, quite bulky datasets are required.
```
{
"results": [{
@ -248,9 +253,10 @@ Let's take a look at a simple example: what the coffee machine search function r
}
```
This approach is quite normal, alas; could be found in almost every API. As we see, the number of entities' fields exceeds recommended 7, and even 9. Fields are being mixed into one single list, often with similar prefixes.
This approach is regretfully quite usual, and could be found in almost every API. As we see, the number of entities' fields exceeds recommended seven, and even nine. Fields are being mixed into one single list, grouped by a common prefix.
In this situation, we are to split this structure into data domains: which fields are logically related to a single subject area. In our case we may identify at least 7 data clusters:
* data regarding a place where the coffee machine is located;
* properties of the coffee machine itself;
* route data;
@ -299,8 +305,8 @@ Let's try to group it together:
}
```
Such decomposed API is much easier to read than a long sheet of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, a `place` and a `route` could be joined in a single `location` structure, or an `offer` and a `pricing` might be combined into some generalized object.
Such a decomposed API is much easier to read than a long list of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, a `place` and a `route` could be nested fields under a synthetic `location` property, or an `offer` and a `pricing` might be combined into some generalized object.
It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands: “here is the place description of no interest to me right now, no need to traverse deeper.” If the data fields needed to complete some action are scattered all over different composites, the readability doesn't improve but degrades.
It is important to say that readability is achieved not only by mere grouping the entities. Decomposing must be performed in such a manner that a developer, while reading the interface, instantly understands, “Here is the place description of no interest to me right now, no need to traverse deeper.” If the data fields needed to complete some action are scattered all over different composites, the readability doesn't improve and even degrades.
Proper decomposition also helps with extending and evolving the API. We'll discuss the subject in Section II.
Proper decomposition also helps with extending and evolving an API. We'll discuss the subject in Section II.

View File

@ -103,7 +103,7 @@ POST /v1/offers/search
* `offer` — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
* `place` — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофемашину.
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофемашины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофемашина — лишь одно из них. `/v1/offers/search` — более логичное имя для такого эндпойнта.
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофемашины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофемашина — лишь одно из них, не самое важное.
Вернёмся к коду, который напишет разработчик. Теперь он будет выглядеть примерно так:
```
@ -166,9 +166,7 @@ POST /v1/orders
С формальной точки зрения такой ошибки достаточно: пользователю будет показано сообщение «неверная цена», и он должен будет повторить заказ. Конечно, это будет очень плохое решение с точки зрения UX (пользователь ведь не совершал никаких ошибок, да и толку ему от этого сообщения никакого).
Главное правило интерфейсов ошибок в API таково: из содержимого ошибки клиент должен в первую очередь понять, *что ему делать с этой ошибкой*. Всё остальное вторично; если бы ошибка была программно читаема, мы могли бы вовсе не снабжать её никаким сообщением для пользователя.
Содержимое ошибки должно отвечать на следующие вопросы:
Главное правило интерфейсов ошибок в API таково: из содержимого ошибки клиент должен в первую очередь понять, *что ему делать с этой ошибкой*. Содержимое ошибки должно отвечать на следующие вопросы:
1. На чьей стороне ошибка — сервера или клиента?
В HTTP API для индикации источника проблемы традиционно используются коды ответа: `4xx` проблема клиента, `5xx` проблема сервера (за исключением «статуса неопределённости» `404`).