mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-11 11:02:05 +02:00
proofreading
This commit is contained in:
parent
c24c62ccdb
commit
f20d0fda78
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
804
docs/API.en.html
804
docs/API.en.html
File diff suppressed because one or more lines are too long
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.
@ -3873,17 +3873,17 @@ let status = api.getStatus(order.id);
|
||||
<pre><code>let order = api.createOrder();
|
||||
let status;
|
||||
while (true) {
|
||||
try {
|
||||
status = api.getStatus(order.id);
|
||||
} catch (e) {
|
||||
if (e.httpStatusCode != 404 ||
|
||||
timeoutExceeded()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
status = api.getStatus(order.id);
|
||||
} catch (e) {
|
||||
if (e.httpStatusCode != 404 ||
|
||||
timeoutExceeded()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status) {
|
||||
…
|
||||
…
|
||||
}
|
||||
</code></pre>
|
||||
<p>Мы полагаем, что можно не уточнять, что писать код, подобный вышеприведённому, ни в коем случае нельзя. Уж если вы действительно предоставляете нестрого консистентный API, то либо операция <code>createOrder</code> в SDK должна быть асинхронной и возвращать результат только по готовности всех реплик, либо политика перезапросов должна быть скрыта внутри операции <code>getStatus</code>.</p>
|
||||
@ -3955,7 +3955,7 @@ object.observe('widthchange', observerFunction);
|
||||
PUT /v1/api-types/{api_type}
|
||||
{
|
||||
"order_execution_endpoint": {
|
||||
// Описание функции обратного вызова
|
||||
// Callback function description
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
@ -3964,12 +3964,12 @@ PUT /v1/api-types/{api_type}
|
||||
PUT /v1/partners/{partnerId}/coffee-machines
|
||||
{
|
||||
"coffee_machines": [{
|
||||
"id",
|
||||
"api_type",
|
||||
"location",
|
||||
"supported_recipes"
|
||||
}, …]
|
||||
}
|
||||
|
||||
</code></pre>
|
||||
<p>Таким образом механика следующая:</p>
|
||||
<ul>
|
||||
@ -4048,7 +4048,7 @@ POST /v1/recipes
|
||||
<pre><code>"product_properties": {
|
||||
// "l10n" — стандартное сокращение
|
||||
// для "localization"
|
||||
"l10n" : [{
|
||||
"l10n": [{
|
||||
"language_code": "en",
|
||||
"country_code": "US",
|
||||
"name",
|
||||
@ -5116,6 +5116,7 @@ Authorization: Bearer <token>
|
||||
<li>техническая поддержка внешних пользователей осуществляется по остаточному принципу.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Разработчики внутренних сервисов часто ломают обратную совместимость или выпускают новые мажорные версии, совершенно не заботясь о последствиях этих действий для внешних партнёрах.</li>
|
||||
</ul>
|
||||
<p>Всё это приводит к тому, что наличие внешнего API зачастую работает не в плюс компании, а в минус: фактически, вы предоставляете крайне критически и скептически настроенной аудитории очень плохой продукт. Если у вас нет ресурсов на грамотное развитие API как продукта для внешних пользователей — лучше за него не браться совсем.</p>
|
||||
<h5><a href="#chapter-52-paragraph-5" id="chapter-52-paragraph-5" class="anchor">5. API = площадка для рекламы</a></h5>
|
||||
|
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
@ -1,8 +1,8 @@
|
||||
### [Strong Coupling and Related Problems][back-compat-strong-coupling]
|
||||
|
||||
To demonstrate the strong coupling problematics let us move to *really interesting* things. Let's continue our “variation analysis”: what if the partners wish to offer not only the standard beverages but their own unique coffee recipes to end-users? The catch is that the partner API as we described it in the previous chapter does not expose the very existence of the partner network to the end user, and thus describes a simple case. Once we start providing methods to alter the core functionality, not just API extensions, we will soon face next-level problems.
|
||||
To demonstrate the problems of strong coupling, let's move on to *interesting* topics. Let's continue our “variation analysis”: what if partners wish to offer their own unique coffee recipes to end users in addition to the standard beverages? The challenge is that the partner API, as described in the previous chapter, does not expose the very existence of the partner network to the end user, thus presenting a simple case. However, once we start providing methods to modify the core functionality, not just API extensions, we will soon face next-level problems.
|
||||
|
||||
So, let us add one more endpoint for registering the partner's own recipe:
|
||||
So, let's add one more endpoint for registering the partner's own recipe:
|
||||
|
||||
```
|
||||
// Adds new recipe
|
||||
@ -14,21 +14,21 @@ POST /v1/recipes
|
||||
"description",
|
||||
"default_volume"
|
||||
// Other properties to describe
|
||||
// the beverage to end-user
|
||||
// the beverage to an end user
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At first glance, again, it looks like a reasonably simple interface, explicitly decomposed into abstraction levels. But let us imagine the future — what would happen with this interface when our system evolves further?
|
||||
At first glance, this appears to be a reasonably simple interface, explicitly decomposed into abstraction levels. But let's imagine the future and consider what would happen to this interface as our system evolves further.
|
||||
|
||||
The first problem is obvious to those who read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter thoroughly: product properties must be localized. That will lead us to the first change:
|
||||
The first problem is obvious to those who thoroughly read the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter: product properties must be localized. This leads us to the first change:
|
||||
|
||||
```
|
||||
"product_properties": {
|
||||
// "l10n" is the standard abbreviation
|
||||
// for "localization"
|
||||
"l10n" : [{
|
||||
"l10n": [{
|
||||
"language_code": "en",
|
||||
"country_code": "US",
|
||||
"name",
|
||||
@ -37,25 +37,25 @@ The first problem is obvious to those who read the “[Describing Final Interfac
|
||||
}
|
||||
```
|
||||
|
||||
And here the first big question arises: what should we do with the `default_volume` field? From one side, that's an objective property measured in standardized units, and it's being passed to the program execution engine. On the other side, in countries like the United States, we had to specify beverage volumes not like “300 ml,” but “10 fl oz.” We may propose two solutions:
|
||||
* Either the partner provides the corresponding number only, and we will make readable descriptions on our own behalf, or
|
||||
* The partner provides both the number and all of its localized representations.
|
||||
And here arises the first big question: what should we do with the `default_volume` field? On one hand, it's an objective property measured in standardized units to be passed to the program execution engine. On the other hand, in countries like the United States, beverage volumes are specified as “10 fl oz” rather than “300 ml.” We can propose two solutions:
|
||||
* Either the partner provides only the corresponding number and we will make readable descriptions ourselves, or
|
||||
* The partner provides both the number and all its localized representations.
|
||||
|
||||
The flaw in the first option is that a partner might be willing to use the service in some new country or language — and will be unable to do so until the API supports them. The flaw in the second option is that it works with predefined volumes only, so you can't order an arbitrary beverage volume. So the very first step we've made effectively has us trapped.
|
||||
The flaw in the first option is that a partner might be willing to use the service in a new country or language, but they will be unable to do so until the API is localized to support these new territories. The flaw in the second option is that it only works with predefined volumes, so ordering an arbitrary beverage volume will not be possible. The very first step we've taken effectively has had us trapped.
|
||||
|
||||
The localization flaws are not the only problem with this API. We should ask ourselves a question — *why* do we really need these `name` and `description`? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return them back in the `/v1/search` method response, but that's not a proper answer: why do we really return these strings from `search`?
|
||||
The localization flaws are not the only problem with this API. We should ask ourselves a question: *why* do we really need these `name` and `description` fields? They are simply non-machine-readable strings with no specific semantics. At first glance, we need them to return in the `/v1/search` method response, but that's not a proper answer as it only leads to another question: why do we actually return these strings from `search`?
|
||||
|
||||
The correct answer lies a way beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing beverage type. Probably the `name` and `description` fields are simply two designations of the beverage for a user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). It actually means that we set the requirements to the API based on some specific design. But *what if* a partner is making their own UI for their own app? Not only they might not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name”, it implies some restrictions: it has recommended length which is optimal to some specific UI, and it must look consistently on the search results page. Indeed, the “our best quality™ coffee” or “Invigorating Morning Freshness®” designations would look very weird in-between “Cappuccino,” “Lungo,” and “Latte.”
|
||||
The correct answer lies beyond this specific interface. We need them *because some representation exists*. There is a UI for choosing a beverage type. The `name` and `description` fields are probably two designations of the beverage for the user to read, a short one (to be displayed on the search results page) and a long one (to be displayed in the extended product specification block). This means we are setting the API requirements based on some specific visual design. But *what if* a partner is creating their own UI for their own app? Not only might they not actually need two descriptions, but we are also *deceiving* them. The `name` is not “just a name” as it implies certain restrictions: it has a recommended length that is optimal for a specific UI, and it must look consistent on the search results page. Indeed, designations like “our best quality™ coffee” or “Invigorating Morning Freshness®” would look out of place among “Cappuccino,” “Lungo,” and “Latte.”
|
||||
|
||||
There is also another side to this story. As UIs (both ours' and partners') tend to evolve, new visual elements will be eventually introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The `product_properties` entity will become a scrapyard for tons of optional fields, and learning how setting what field results in what effects in the UI will be an interesting quest, full of probes and mistakes.
|
||||
There is also another aspect to consider. As UIs (both ours and partners') evolve, new visual elements will eventually be introduced. For example, a picture of the beverage, its energy value, allergen information, etc. The `product_properties` entity will become a scrapyard for numerous optional fields, and learning how to set each field and its effects in the UI will be an interesting journey filled with trial and error.
|
||||
|
||||
The problems we're facing are the problems of *strong coupling*. Each time we offer an interface like described above, we in fact prescript implementing one entity (recipe) based on implementations of other entities (UI layout, localization rules). This approach disrespects the very basic principle of the “top to bottom” API design because **low-level entities must not define high-level ones**.
|
||||
The problems we're facing are the problems of *strong coupling*. Each time we offer an interface as described above, we effectively dictate the implementation of one entity (recipe) based on the implementations of other entities (UI layout, localization rules). This approach disregards the fundamental principle of “top to bottom” API design because **low-level entities should not define high-level ones**.
|
||||
|
||||
#### The Rule of Contexts
|
||||
|
||||
To make things worse, let us state that the inverse principle is also correct: high-level entities must not define low-level ones as well, since that simply isn't their responsibility. The exit from this logical labyrinth is that high-level entities must *define a context*, which other objects are to interpret. To properly design the interfaces for adding a new recipe we shouldn't try to find a better data format; we need to understand what contexts, both explicit and implicit, exist in our subject area.
|
||||
To exacerbate matters, let us state that the inverse principle is also true: high-level entities should not define low-level ones as well since it is not their responsibility. The way out of this logical labyrinth is that high-level entities should *define a context* for other objects to interpret. To properly design the interfaces for adding a new recipe we should not attempt to find a better data format. Instead, we need to understand the explicit and implicit contexts that exist in our subject area.
|
||||
|
||||
We have already noted a localization context. There is some set of languages and regions we support in our API, and there are the requirements — what exactly partners must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:
|
||||
We have already identified a localization context. There is a set of languages and regions supported by our API, and there are requirements for what partners must provide to make the API work in a new region. Specifically, there must be a formatting function to represent beverage volume somewhere in our API code, either internally or within an SDK:
|
||||
|
||||
```
|
||||
l10n.volume.format = function(
|
||||
@ -71,7 +71,7 @@ l10n.volume.format = function(
|
||||
*/
|
||||
```
|
||||
|
||||
To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use through the partner API. Like this:
|
||||
To ensure our API works correctly with a new language or region, the partner must either define this function or indicate which pre-existing implementation to use through the partner API, like this:
|
||||
|
||||
```
|
||||
// Add a general formatting rule
|
||||
@ -85,8 +85,8 @@ PUT /formatters/volume/ru
|
||||
// in the “US” region
|
||||
PUT /formatters/volume/ru/US
|
||||
{
|
||||
// in the US, we need to recalculate
|
||||
// the number, then add a postfix
|
||||
// In the US, we need to recalculate
|
||||
// the number and add a postfix
|
||||
"value_transform": {
|
||||
"action": "divide",
|
||||
"divisor": 30
|
||||
@ -95,20 +95,19 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
```
|
||||
|
||||
so the above-mentioned `l10n.volume.format` function implementation might retrieve the formatting rules for the new language-region pair and use them.
|
||||
so the aforementioned `l10n.volume.format` function implementation can retrieve the formatting rules for the new language-region pair and utilize them.
|
||||
|
||||
**NB**: we are more than aware that such a simple format isn't enough to cover real-world localization use cases, and one either relies on existing libraries or designs a sophisticated format for such templating, which takes into account such things as grammatical cases and rules of rounding numbers up or allow defining formatting rules in a form of function code. The example above is simplified for purely educational purposes.
|
||||
|
||||
Let us deal with the `name` and `description` problem then. To lower the coupling level there, we need to formalize (probably just to ourselves) a “layout” concept. We are asking for providing the `name` and `description` fields not because we just need them, but for representing them in some specific user interface. This specific UI might have an identifier or a semantic name.
|
||||
**NB**: we are well aware that such a simple format is not sufficient to cover real-world localization use cases, and one would either rely on existing libraries or design a sophisticated format for such templating, which takes into account various aspects such as grammatical cases and rules for rounding numbers or allows defining formatting rules in the form of function code. The example above is simplified for purely educational purposes.
|
||||
|
||||
Let's address the `name` and `description` problem. To reduce the coupling level, we need to formalize (probably just for ourselves) a “layout” concept. We request the provision of the `name` and `description` fields not because we theoretically need them but to present them in a specific user interface. This particular UI might have an identifier or a semantic name associated with it:
|
||||
|
||||
```
|
||||
GET /v1/layouts/{layout_id}
|
||||
{
|
||||
"id",
|
||||
// We would probably have lots of layouts,
|
||||
// so it's better to enable extensibility
|
||||
// from the beginning
|
||||
// Since we will likely have numerous
|
||||
// layouts, it's better to enable
|
||||
// extensibility from the beginning
|
||||
"kind": "recipe_search",
|
||||
// Describe every property we require
|
||||
// to have this layout rendered properly
|
||||
@ -116,11 +115,11 @@ GET /v1/layouts/{layout_id}
|
||||
// Since we learned that `name`
|
||||
// is actually a title for a search
|
||||
// result snippet, it's much more
|
||||
// convenient to have explicit
|
||||
// convenient to have an explicit
|
||||
// `search_title` instead
|
||||
"field": "search_title",
|
||||
"view": {
|
||||
// Machine-readable description
|
||||
// A machine-readable description
|
||||
// of how this field is rendered
|
||||
"min_length": "5em",
|
||||
"max_length": "20em",
|
||||
@ -135,7 +134,7 @@ GET /v1/layouts/{layout_id}
|
||||
}
|
||||
```
|
||||
|
||||
So the partner may decide, which option better suits them. They can provide mandatory fields for the standard layout:
|
||||
Thus, the partner can decide which option better suits their needs. They can provide mandatory fields for the standard layout:
|
||||
|
||||
```
|
||||
PUT /v1/recipes/{id}/properties/l10n/{lang}
|
||||
@ -144,9 +143,9 @@ PUT /v1/recipes/{id}/properties/l10n/{lang}
|
||||
}
|
||||
```
|
||||
|
||||
or create a layout of their own and provide the data fields it requires, or they may ultimately design their own UI and don't use this functionality at all, defining neither layouts nor corresponding data fields.
|
||||
Alternatively, they can create their own layout and provide the data fields it requires, or they may choose to design their own UI and not use this functionality at all, thereby defining neither layouts nor corresponding data fields.
|
||||
|
||||
Then our interface would ultimately look like this:
|
||||
Ultimately, our interface would look like this:
|
||||
|
||||
```
|
||||
POST /v1/recipes
|
||||
@ -155,7 +154,7 @@ POST /v1/recipes
|
||||
{ "id" }
|
||||
```
|
||||
|
||||
This conclusion might look highly counter-intuitive, but lacking any fields in a `Recipe` simply tells us that this entity possesses no specific semantics of its own, and is simply an identifier of a context; a method to point out where to look for the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:
|
||||
This conclusion might seem highly counter-intuitive, but the absence of fields in a `Recipe` simply tells us that this entity possesses no specific semantics of its own. It serves solely as an identifier of a context, a way to indicate where to find the data needed by other entities. In the real world, we should implement a builder endpoint capable of creating all the related contexts with a single request:
|
||||
|
||||
```
|
||||
POST /v1/recipe-builder
|
||||
@ -166,11 +165,11 @@ POST /v1/recipe-builder
|
||||
"default_volume",
|
||||
"l10n"
|
||||
},
|
||||
// Create all the desirable layouts
|
||||
// Create all the desired layouts
|
||||
"layouts": [{
|
||||
"id", "kind", "properties"
|
||||
}],
|
||||
// Add all the formatters needed
|
||||
// Add all the required formatters
|
||||
"formatters": {
|
||||
"volume": [
|
||||
{
|
||||
@ -183,13 +182,14 @@ POST /v1/recipe-builder
|
||||
}
|
||||
]
|
||||
},
|
||||
// Other actions needed to be done
|
||||
// to register new recipe in the system
|
||||
// Other actions needed
|
||||
// to register a new recipe
|
||||
// in the system
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
We should also note that providing a newly created entity identifier by the requesting side isn't exactly the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, we're risking getting lots of collisions on recipe names used by different partners, so we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter.
|
||||
We should also note that providing a newly created entity identifier from the requesting side is not the best practice. However, since we decided from the very beginning to keep recipe identifiers semantically meaningful, we have to live on with this convention. Obviously, there is a risk of encountering collisions with recipe names used by different partners. Therefore, we actually need to modify this operation: either a partner must always use a pair of identifiers (e.g., the recipe id plus the partner's own id), or we need to introduce composite identifiers, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter.
|
||||
|
||||
```
|
||||
POST /v1/recipes/custom
|
||||
@ -210,19 +210,19 @@ POST /v1/recipes/custom
|
||||
|
||||
Also note that this format allows us to maintain an important extensibility point: different partners might have both shared and isolated namespaces. Furthermore, we might introduce special namespaces (like `common`, for example) to allow editing standard recipes (and thus organizing our own recipes backoffice).
|
||||
|
||||
**NB**: a mindful reader might have noted that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter with regards to the “program” and “program run” entities. Indeed, we might do it without the `program-matcher` endpoint and make it this way:
|
||||
**NB**: a mindful reader might have noticed that this technique was already used in our API study much earlier in the “[Separating Abstraction Levels](#api-design-separating-abstractions)” chapter regarding the “program” and “program run” entities. Indeed, we can propose an interface for retrieving commands to execute a specific recipe without the `program-matcher` endpoint, and instead, do it this way:
|
||||
|
||||
```
|
||||
GET /v1/recipes/{id}/run-data/{api_type}
|
||||
→
|
||||
{ /* A description, how to
|
||||
{ /* A description of how to
|
||||
execute a specific recipe
|
||||
using a specified API type */ }
|
||||
```
|
||||
|
||||
Then developers would have to make this trick to get coffee prepared:
|
||||
Then developers would have to make this trick to get the beverage prepared:
|
||||
* Learn the API type of the specific coffee machine.
|
||||
* Get the execution description, as stated above.
|
||||
* Depending on the API type, run some specific commands.
|
||||
* Retrieve the execution description as described above.
|
||||
* Based on the API type, execute specific commands.
|
||||
|
||||
Obviously, such an interface is absolutely unacceptable, simply because in the majority of use cases developers don't care at all, which API type the specific coffee machine runs. To avoid the necessity of introducing such bad interfaces we created a new “program” entity, which constitutes merely a context identifier, just like a “recipe” entity does. A `program_run_id` entity is also organized in this manner, it also possesses no specific properties, being *just* a program run identifier.
|
||||
Obviously, such an interface is completely unacceptable because, in the majority of use cases, developers do not care at all about which API type the specific coffee machine exposes. To avoid the need for introducing such poor interfaces we created a new “program” entity, which serves solely as a context identifier, just like a “recipe” entity does. Similarly, the `program_run_id` entity is also organized in the same manner, without possessing any specific properties and representing *just* a program run identifier.
|
@ -28,7 +28,7 @@ POST /v1/recipes
|
||||
"product_properties": {
|
||||
// "l10n" — стандартное сокращение
|
||||
// для "localization"
|
||||
"l10n" : [{
|
||||
"l10n": [{
|
||||
"language_code": "en",
|
||||
"country_code": "US",
|
||||
"name",
|
||||
|
Loading…
x
Reference in New Issue
Block a user