1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-05-31 22:09:37 +02:00

Proofreading

This commit is contained in:
Sergey Konstantinov 2023-06-17 17:41:51 +03:00
parent 1539171ae3
commit c24c62ccdb
2 changed files with 30 additions and 32 deletions

View File

@ -2,13 +2,13 @@
In the previous chapters, we have attempted to outline theoretical rules and illustrate them with practical examples. However, understanding the principles of designing change-proof APIs requires practice above all else. The ability to anticipate future growth problems comes from a handful of grave mistakes once made. While it is impossible to foresee everything, one can develop a certain technical intuition.
Therefore, in the following chapters, we will test the robustness [our study API](#api-design-annex) from the previous Section, examining it from various perspectives to perform a “variational analysis” of our interfaces. More specifically, we will apply a “What If?” question to every entity, as if we are to provide a possibility to write an alternate implementation of every piece of logic.
Therefore, in the following chapters, we will test the robustness of [our study API](#api-design-annex) from the previous Section, examining it from various perspectives to perform a “variational analysis” of our interfaces. More specifically, we will apply a “What If?” question to every entity, as if we are to provide a possibility to write an alternate implementation of every piece of logic.
**NB**. In our examples, the interfaces will be constructed in a manner allowing for dynamic real-time linking of different entities. In practice, such integrations usually imply writing an ad hoc server-side code in accordance with specific agreements made with specific partners. But for educational purposes, we will pursue more abstract and complicated ways. Dynamic real-time linking is more typical in complex program constructs like operating system APIs or embeddable libraries; giving educational examples based on such sophisticated systems would be too inconvenient.
**NB**. In our examples, the interfaces will be constructed in a manner allowing for dynamic real-time linking of different entities. In practice, such integrations usually imply writing *ad hoc* server-side code in accordance with specific agreements made with specific partners. But for educational purposes, we will pursue more abstract and complicated ways. Dynamic real-time linking is more typical in complex program constructs like operating system APIs or embeddable libraries; giving educational examples based on such sophisticated systems would be too inconvenient.
Let's start with the basics. Imagine that we haven't exposed any other functionality but searching for offers and making orders, thus providing an API of two methods: `POST /offers/search` and `POST /orders`.
Let's start with the basics. Imagine that we haven't exposed any other functionality but searching for offers and making orders, thus providing an API with two methods: `POST /offers/search` and `POST /orders`.
Let us make the next logical step there and suppose that partners will wish to dynamically plug their own coffee machines (operating some previously unknown types of API) into our platform. To allow doing so, we have to negotiate a callback format that would allow us to call partners' APIs and expose two new endpoints providing the following capabilities:
Let us take the next logical step and suppose that partners will wish to dynamically plug their own coffee machines (operating some previously unknown types of API) into our platform. To allow doing so, we have to negotiate a callback format that would allow us to call partners' APIs and expose two new endpoints providing the following capabilities:
* Registering new API types in the system
* Providing the list of the coffee machines and their API types.
@ -18,9 +18,9 @@ For example, we might provide a second API family (the partner-bound one) with t
// 1. Register a new API type
PUT /v1/api-types/{api_type}
{
"order_execution_endpoint": {
// Callback function description
}
"order_execution_endpoint": {
// Callback function description
}
}
```
@ -29,40 +29,38 @@ PUT /v1/api-types/{api_type}
// with their API types
PUT /v1/partners/{partnerId}/coffee-machines
{
"coffee_machines": [{
"api_type",
"location",
"supported_recipes"
}, …]
"coffee_machines": [{
"api_type",
"location",
"supported_recipes"
}, …]
}
```
So the mechanics are like that:
So the mechanics are like this:
* A partner registers their API types, coffee machines, and supported recipes.
* With each incoming order, our server will call the callback function, providing the order data in the stipulated format.
Now the partners might dynamically plug their coffee machines in and get the orders. But we now will do the following exercise:
Now the partners might dynamically plug their coffee machines in and get the orders. But now we will do the following exercise:
* Enumerate all the implicit assumptions we have made
* Enumerate all the implicit coupling mechanisms we need to have the platform functioning properly.
It may look like there are no such things in our API since it's quite simple and basically just describes making some HTTP call but that's not true.
It may seem like there are no such things in our API since it's quite simple and basically just describes making some HTTP calls, but that's not true.
1. It is implied that every coffee machine supports every order option like varying the beverage volume.
2. There is no need to display some additional data to the end-user regarding coffee being brewed on these new coffee machines.
2. There is no need to display additional data to the end-user regarding coffee being brewed on these new coffee machines.
3. The price of the beverage doesn't depend on the selected partner or coffee machine type.
We have written down this list having one purpose in mind: we need to understand, how exactly will we make these implicit arrangements explicit if we need that. For example, if different coffee machines provide different functionality — let's say, some of them are capable of brewing fixed beverage volumes only — what would change in our API?
We have written down this list having one purpose in mind: we need to understand how exactly we will make these implicit arrangements explicit if we need to. For example, if different coffee machines provide different functionality — let's say, some of them are capable of brewing fixed beverage volumes only — what would change in our API?
The universal approach to making such amendments is: to consider the existing interface as a reduction of some more general one like if some parameters were set to defaults and therefore omitted. So making a change is always a three-step process:
The universal approach to making such amendments is to consider the existing interface as a reduction of some more general one, as if some parameters were set to defaults and therefore omitted. So making a change is always a three-step process:
1. Explicitly define the programmatical contract *as it works right now*.
2. Extend the functionality: add a new method allowing for tackling those restrictions set in the previous paragraph.
3. Pronounce the existing interfaces (those defined in \#1) being “helpers” to new ones (those defined in \#2) which sets some options to default values.
More specifically, if we talk about changing available order options, we should do the following.
2. Extend the functionality: add a new method that allows for tackling the restrictions set in the previous paragraph.
3. Pronounce the existing interfaces (those defined in \#1) as “helpers” to the new ones (those defined in \#2) that pre-fill some options with default values.
More specifically, if we talk about changing available order options, we should do the following:
1. Describe the current state. All coffee machines, plugged via the API, must support three options: sprinkling with cinnamon, changing the volume, and contactless delivery.
2. Add new “with-options” endpoint:
2. Add a new “with options” endpoint:
```
PUT /v1/partners/{partner_id}⮠
/coffee-machines-with-options
@ -79,18 +77,18 @@ More specifically, if we talk about changing available order options, we should
}
```
3. Pronounce `PUT /coffee-machines` endpoint as it now stands in the protocol being equivalent to calling `PUT /coffee-machines-with-options` if we pass those three options to it (sprinkling with cinnamon, changing the volume, contactless delivery) and therefore being a partial case — a helper to a more general call.
3. Pronounce the `PUT /coffee-machines` endpoint as it currently stands in the protocol as equivalent to calling `PUT /coffee-machines-with-options` if we pass those three options to it (sprinkling with cinnamon, changing the volume, contactless delivery) and therefore being a partial case — a helper to a more general call.
Usually, just adding a new optional parameter to the existing interface is enough; in our case, adding non-mandatory `options` to the `PUT /coffee-machines` endpoint.
**NB**. When we talk about defining the contract as it works right now, we're talking about *internal* agreements. We must have asked partners to support those three options while negotiating the interaction format. If we had failed to do so from the very beginning, and now are defining these in a course of expanding the public API, it's a very strong claim to break backward compatibility, and we should never do that (see the previous chapter).
**NB**. When we talk about defining the contract as it works right now, we're referring to *internal* agreements. We must have asked partners to support those three options while negotiating the interaction format. If we had failed to do so from the very beginning and are now defining them during the expansion of the public API, it's a very strong claim to break backward compatibility, and we should never do that (see the previous chapter).
#### Limits of Applicability
Though this exercise looks very simple and universal, its consistent usage is possible only if the hierarchy of entities is well-designed from the very beginning and, which is more important, the vector of the further API expansion is clear. Imagine that after some time passed, the options list got new items; let's say, adding syrup or a second espresso shot. We are totally capable of expanding the list — but not the defaults. So the “default” `PUT /coffee-machines` interface will eventually become totally useless because the default set of three options will not only be any longer of use but will also look ridiculous: why these three options, what are the selection criteria? In fact, the defaults and the method list will be reflecting the historical stages of our API development, and that's totally not what you'd expect from the helpers and defaults nomenclature.
Though this exercise appears to be simple and universal, its consistent usage is only possible if the hierarchy of entities is well-designed from the very beginning and, more importantly, if the direction of further API expansion is clear. Imagine that after some time has passed, the options list has new items, such as adding syrup or a second espresso shot. We are fully capable of expanding the list, but not the defaults. As a result, the “default” `PUT /coffee-machines` interface will eventually become completely useless because the default set of three options will no longer be useful and will appear ridiculous: why these three options, what are the selection criteria? In fact, the defaults and the method list reflect the historical stages of our API development, which is not what one would expect from the helpers and defaults nomenclature.
Alas, this dilemma can't be easily resolved. On one hand, we want developers to write neat and laconic code, so we must provide useful helpers and defaults. On the other hand, we can't know in advance which sets of options will be the most useful after several years of expanding the API.
Alas, this dilemma can't be easily resolved. On one hand, we want developers to write neat and concise code, so we must provide useful helpers and defaults. On the other hand, we can't know in advance which sets of options will be the most useful after several years of API evolution.
**NB**. We might mask this problem in the following manner: one day gather all these oddities and re-define all the defaults with one single parameter. For example, introduce a special method like `POST /use-defaults {"version": "v2"}` which would overwrite all the defaults with more suitable values. That will ease the learning curve, but your documentation will become even worse after that.
**NB**. We might mask this problem in the following manner: one day gather all these oddities and re-define all the defaults with a single parameter. For example, introduce a special method like `POST /use-defaults {"version": "v2"}` that would overwrite all the defaults with more suitable values. This would ease the learning curve, but it would make your documentation even worse.
In the real world, the only viable approach to somehow tackle the problem is the weak entity coupling, which we will discuss in the next chapter.
In the real world, the only viable approach to somehow tackle the problem is weak entity coupling, which we will discuss in the next chapter.

View File

@ -19,7 +19,7 @@
PUT /v1/api-types/{api_type}
{
"order_execution_endpoint": {
// Описание функции обратного вызова
// Callback function description
}
}
```
@ -30,12 +30,12 @@ PUT /v1/api-types/{api_type}
PUT /v1/partners/{partnerId}/coffee-machines
{
"coffee_machines": [{
"id",
"api_type",
"location",
"supported_recipes"
}, …]
}
```
Таким образом механика следующая: